Shell-Script

如何在許多大文件中查找重複行?

  • May 6, 2021

我有大約 30k 個文件。每個文件包含約 100k 行。一行不包含空格。單個文件中的行被排序和免費複製。

我的目標:我想找到兩個或多個文件中的所有重複行,以及包含重複條目的文件的名稱。

一個簡單的解決方案是:

cat *.words | sort | uniq -c | grep -v -F '1 '

然後我會執行:

grep 'duplicated entry' *.words

你看到更有效的方法了嗎?

由於所有輸入文件都已經排序,我們可以繞過實際的排序步驟,僅sort -m用於將文件合併在一起。

在某些 Unix 系統上(據我所知只有Linux),這可能就足夠了

sort -m *.words | uniq -d >dupes.txt

將重複的行寫入文件dupes.txt

要查找這些行來自哪些文件,您可以執行以下操作

grep -Fx -f dupes.txt *.words

這將指示將( ) 中grep的行視為固定字元串模式( )。還要求整行從頭到尾完美匹配 ( )。它將文件名和行列印到終端。dupes.txt``-f dupes.txt``-F``grep``-x

非 Linux Unices(甚至更多文件)

在某些 Unix 系統上,30000 個文件名將擴展為一個字元串,該字元串太長而無法傳遞給單個實用程序(這意味著sort -m *.words將失敗Argument list too long,並在我的 OpenBSD 系統上執行此操作)。如果文件數量大得多,即使 Linux 也會抱怨這一點。

尋找騙子

這意味著在一般情況下(這也適用於超過30000 個文件),必須“分塊”排序:

rm -f tmpfile
find . -type f -name '*.words' -print0 |
xargs -0 sh -c '
   if [ -f tmpfile ]; then
       sort -o tmpfile -m tmpfile "$@"
   else
       sort -o tmpfile -m "$@"
   fi' sh 

或者,創建tmpfile沒有xargs

rm -f tmpfile
find . -type f -name '*.words' -exec sh -c '
   if [ -f tmpfile ]; then
       sort -o tmpfile -m tmpfile "$@"
   else
       sort -o tmpfile -m "$@"
   fi' sh {} +

這將在目前目錄(或以下)中找到名稱匹配的所有文件*.words。對於一次適當大小的這些名稱塊,其大小由xargs/確定find,它將它們合併到排序tmpfile文件中。如果tmpfile已經存在(除了第一個塊之外的所有文件),該文件也將與目前塊中的其他文件合併。根據文件名的長度和命令行的最大允許長度,這可能需要內部腳本的 10 多次單獨執行(find/xargs將自動執行此操作)。

“內部”sh腳本,

if [ -f tmpfile ]; then
   sort -o tmpfile -m tmpfile "$@"
else
   sort -o tmpfile -m "$@"
fi

用於sort -o tmpfile輸出到(即使這也是 的輸入也tmpfile不會覆蓋)和進行合併。在這兩個分支中,將擴展為從或傳遞給腳本的單獨引用的文件名列表。tmpfile``sort``-m``"$@"``find``xargs

然後,繼續執行uniq -dtmpfile獲取所有重複的行:

uniq -d tmpfile >dupes.txt

如果您喜歡“DRY”原則(“不要重複自己”),您可以將內部腳本編寫為

if [ -f tmpfile ]; then
   t=tmpfile
else
   t=/dev/null
fi

sort -o tmpfile -m "$t" "$@"

或者

t=tmpfile
[ ! -f "$t" ] && t=/dev/null
sort -o tmpfile -m "$t" "$@"

哪兒來的?

出於與上述相同的原因,我們無法grep -Fx -f dupes.txt *.words找到這些重複的來源,因此我們find再次使用:

find . -type f -name '*.words' \
   -exec grep -Fx -f dupes.txt {} +

由於沒有“複雜”的處理要完成,我們可以grep直接呼叫 from -exec。該-exec選項採用實用程序命令並將找到的名稱放在{}. 在+最後,find將在每次呼叫實用程序時放置{}目前 shell 支持的盡可能多的參數。

為了完全正確,人們可能想要使用

find . -type f -name '*.words' \
   -exec grep -H -Fx -f dupes.txt {} +

或者

find . -type f -name '*.words' \
   -exec grep -Fx -f dupes.txt /dev/null {} +

確保文件名始終包含在grep.

第一個變體用於grep -H始終輸出匹配的文件名。如果在命令行上給出了多個文件grep,則最後一個變體使用將包含匹配文件的名稱的事實。

grep這很重要,因為發送到from的最後一塊文件名find實際上可能只包含一個文件名,在這種情況下grep,不會在結果中提及它。


獎勵材料:

剖析find++命令xargssh

find . -type f -name '*.words' -print0 |
xargs -0 sh -c '
   if [ -f tmpfile ]; then
       sort -o tmpfile -m tmpfile "$@"
   else
       sort -o tmpfile -m "$@"
   fi' sh 

find . -type f -name '*.words'將簡單地從目前目錄(或以下)生成路徑名列表,其中每個路徑名是正常文件( -type f) 的路徑名,並且在末尾有一個文件名組件匹配*.words. 如果只搜尋目前目錄,可以-maxdepth 1., before之後添加-type f

-print0將確保所有找到的路徑名都以\0( nul) 字元作為分隔符輸出。這是一個在 Unix 路徑中無效的字元,它使我們能夠處理路徑名,即使它們包含換行符(或其他奇怪的東西)。

find將其輸出通過管道傳輸到xargs.

xargs -0將讀取\0- 分隔的路徑名列表,並使用其中的塊重複執行給定的實用程序,確保使用足夠的參數執行實用程序,不會導致 shell 抱怨參數列表太長,直到沒有更多輸入從find.

呼叫的實用程序xargs是在命令行上使用其標誌sh作為字元串給出的腳本。-c

sh -c '...some script...'使用後面的參數呼叫時,參數將可用於腳本中$@除了第一個參數,它將被放置在中$0(這是你可能會發現的“命令名稱”,例如top,如果你足夠快的話)。這就是為什麼我們在實際腳本結束後插入字元串sh作為第一個參數的原因。該字元串sh是一個虛擬參數,可以是任何單個單詞(有些人似乎更喜歡_or sh-find)。

引用自:https://unix.stackexchange.com/questions/423478