Làm thế nào để tìm dòng trùng lặp trong nhiều tệp lớn?


9

Tôi có ~ 30k tập tin. Mỗi tệp chứa ~ 100k dòng. Một dòng không chứa khoảng trắng. Các dòng trong một tệp riêng lẻ được sắp xếp và sao chép miễn phí.

Mục tiêu của tôi: Tôi muốn tìm tất cả các dòng trùng lặp trên hai hoặc nhiều tệp và cả tên của các tệp có chứa các mục trùng lặp.

Một giải pháp đơn giản sẽ là thế này:

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

Và sau đó tôi sẽ chạy:

grep 'duplicated entry' *.words

Bạn có thấy một cách hiệu quả hơn?

Câu trả lời:


13

Vì tất cả các tệp đầu vào đã được sắp xếp, chúng tôi có thể bỏ qua bước sắp xếp thực tế và chỉ sử dụng sort -mđể hợp nhất các tệp lại với nhau.

Trên một số hệ thống Unix (theo hiểu biết của tôi chỉ có Linux), có thể đủ để làm

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

để có được các dòng trùng lặp được ghi vào tệp dupes.txt.

Để tìm những tập tin mà những dòng này đến từ đâu, sau đó bạn có thể làm

grep -Fx -f dupes.txt *.words

Điều này sẽ hướng dẫn grepcoi các dòng trong dupes.txt( -f dupes.txt) là các mẫu chuỗi cố định ( -F). grepcũng sẽ yêu cầu toàn bộ dòng khớp hoàn hảo từ đầu đến cuối ( -x). Nó sẽ in tên tập tin và dòng đến thiết bị đầu cuối.

Unice Linux (hoặc thậm chí nhiều tệp hơn )

Trên một số hệ thống Unix, 30000 tên tệp sẽ mở rộng thành một chuỗi quá dài để chuyển sang một tiện ích duy nhất (nghĩa là sort -m *.wordssẽ thất bại với Argument list too longhệ thống OpenBSD của tôi). Ngay cả Linux cũng sẽ phàn nàn về điều này nếu số lượng tệp lớn hơn nhiều.

Tìm bản sao

Điều này có nghĩa là trong trường hợp chung (điều này cũng sẽ hoạt động với nhiều hơn 30000 tệp), người ta phải "chunk" việc sắp xếp:

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 

Ngoài ra, tạo tmpfilemà không có 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 {} +

Điều này sẽ tìm thấy tất cả các tệp trong thư mục hiện tại (hoặc bên dưới) có tên trùng khớp *.words. Đối với một đoạn có kích thước phù hợp của các tên này tại một thời điểm, kích thước được xác định bởi xargs/ find, nó sẽ hợp nhất chúng lại với nhau thành tmpfiletệp được sắp xếp . Nếu tmpfileđã tồn tại (đối với tất cả trừ đoạn đầu tiên), tệp này cũng được hợp nhất với các tệp khác trong đoạn hiện tại. Tùy thuộc vào độ dài tên tệp của bạn và độ dài tối đa được phép của một dòng lệnh, điều này có thể yêu cầu nhiều hơn hoặc nhiều hơn 10 lần chạy tập lệnh nội bộ ( find/ xargssẽ tự động thực hiện việc này).

shKịch bản "nội bộ" ,

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

sử dụng sort -o tmpfileđể xuất ra tmpfile(điều này sẽ không ghi đè tmpfilengay cả khi đây cũng là đầu vào sort) và -mđể thực hiện hợp nhất. Trong cả hai nhánh, "$@"sẽ mở rộng thành một danh sách các tên tệp được trích dẫn riêng được chuyển đến tập lệnh từ findhoặc xargs.

Sau đó, chỉ cần chạy uniq -dtrên tmpfileđể có được tất cả các dòng được sao chép:

uniq -d tmpfile >dupes.txt

Nếu bạn thích nguyên tắc "DRY" ("Đừng lặp lại chính mình"), bạn có thể viết tập lệnh nội bộ dưới dạng

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

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

hoặc là

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

Họ đến từ đâu vậy?

Vì những lý do tương tự như trên, chúng tôi không thể sử dụng grep -Fx -f dupes.txt *.wordsđể tìm nơi các bản sao này đến từ đâu, vì vậy thay vào đó chúng tôi sử dụng findlại:

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

Vì không có quá trình xử lý "phức tạp" nào được thực hiện, chúng tôi có thể gọi greptrực tiếp từ đó -exec. Các -exectùy chọn có một lệnh tiện ích và sẽ đặt tên được tìm thấy trong {}. Với +lúc kết thúc, findsẽ đặt như nhiều đối số thay {}vì hỗ trợ vỏ hiện tại trong mỗi lời gọi của tiện ích.

Để hoàn toàn chính xác, người ta có thể muốn sử dụng một trong hai

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

hoặc là

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

để chắc chắn rằng tên tệp luôn được bao gồm trong đầu ra từ grep.

Biến thể đầu tiên sử dụng grep -Hđể luôn xuất ra tên tệp phù hợp. Biến thể cuối cùng sử dụng thực tế grepsẽ bao gồm tên của tệp phù hợp nếu có nhiều hơn một tệp được đưa ra trên dòng lệnh.

Điều này quan trọng vì đoạn tên cuối cùng được gửi đến greptừ findthực tế có thể chỉ chứa một tên tệp duy nhất, trong trường hợp đó grepsẽ không đề cập đến nó trong kết quả của nó.


Tài liệu khen thưởng:

Mổ xẻ find+ xargs+ shlệnh:

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'sẽ chỉ tạo một danh sách các tên đường dẫn từ thư mục hiện tại (hoặc bên dưới) trong đó mỗi tên đường dẫn là một tệp thông thường ( -type f) và có một thành phần tên tệp ở cuối phù hợp *.words. Nếu chỉ tìm kiếm thư mục hiện tại , người ta có thể thêm -maxdepth 1sau ., trước -type f.

-print0sẽ đảm bảo rằng tất cả các tên đường dẫn được tìm thấy được xuất ra với một ký tự \0( nul) là dấu phân cách. Đây là một ký tự không hợp lệ trong đường dẫn Unix và nó cho phép chúng tôi xử lý tên đường dẫn ngay cả khi chúng có chứa các ký tự dòng mới (hoặc những thứ kỳ lạ khác).

findống đầu ra của nó để xargs.

xargs -0sẽ đọc \0danh sách tên đường dẫn được phân tách và sẽ thực thi tiện ích đã cho lặp đi lặp lại với các khối này, đảm bảo rằng tiện ích được thực thi với các đối số vừa đủ để không khiến shell phàn nàn về danh sách đối số quá dài, cho đến khi không còn đầu vào nữa từ find.

Tiện ích được gọi bởi xargsshvới một tập lệnh được đưa ra trên dòng lệnh dưới dạng một chuỗi sử dụng -ccờ của nó .

Khi gọi sh -c '...some script...'với các đối số theo sau, các đối số sẽ có sẵn cho tập lệnh $@, ngoại trừ đối số đầu tiên sẽ được đặt vào $0(đây là "tên lệnh" mà bạn có thể phát hiện ra, ví dụ topnếu bạn đủ nhanh). Đây là lý do tại sao chúng ta chèn chuỗi shlàm đối số đầu tiên sau khi kết thúc tập lệnh thực tế. Chuỗi shlà một đối số giả và có thể là bất kỳ từ nào (một số dường như thích _hoặc sh-find).


Vào cuối tập lệnh shell đầu tiên của bạn, việc sử dụng là fi' shgì?
dan

@danielAzuelos Đây filà phần cuối của iftuyên bố trong shtập lệnh shell "nội bộ" . Kết 'thúc kịch bản shell đó (toàn bộ tập lệnh là một chuỗi trích dẫn đơn). Các shsẽ được chuyển đến các kịch bản nội bộ trong $0(không thuộc $@, mà sẽ chứa tên tập tin). Trong trường hợp này, shchuỗi đó thực sự có thể là bất kỳ từ nào. Nếu rời khỏi shở cuối, tên tệp đầu tiên sẽ được truyền vào $0và sẽ không phải là một phần của quá trình xử lý mà tập lệnh shell bên trong đang thực hiện.
Kusalananda

8

Các dòng trong một tệp riêng lẻ được sắp xếp và sao chép miễn phí.

Điều đó có nghĩa là bạn có thể tìm thấy một số sử dụng cho sort -m:

 -m, --merge
        merge already sorted files; do not sort

Sự thay thế rõ ràng khác để làm điều này sẽ là đơn giản awkđể thu thập các dòng trong một mảng và đếm chúng. Nhưng như @ dave_thndry_085 đã nhận xét, 3 000 triệu dòng đó (hoặc nhiều dòng duy nhất có) có thể sẽ chiếm một lượng bộ nhớ đáng kể để lưu trữ, do đó có thể không hoạt động tốt.


3

Với awk, bạn có thể nhận được tất cả các dòng lặp lại trong tất cả các tệp trong một lệnh ngắn:

$ awk '_[$0]++' *.words

Nhưng nó sẽ lặp lại các dòng nếu một dòng tồn tại 3 lần trở lên.
Có một giải pháp để chỉ nhận được bản sao đầu tiên:

$ awk '_[$0]++==1' *.words

Nó sẽ khá nhanh (nếu lặp lại ít) nhưng sẽ ăn rất nhiều bộ nhớ để giữ tất cả các dòng trong bộ nhớ. Có thể, tùy thuộc vào các tệp thực tế của bạn và lặp lại, trước tiên hãy thử với 3 hoặc bốn tệp.

$ awk '_[$0]++==1' [123]*.words

Nếu không, bạn có thể làm:

$ sort -m *.words | uniq -d

Mà sẽ in các dòng lặp lại uniq.


2
+1 chosort -m * | uniq -d
Jeff Schaller

awk có thể tránh được sự lặp lại với 'x[$0]++==1'nhưng thực sự sẽ cần rất nhiều bộ nhớ; nếu các đường 3G có giá trị riêng biệt 1G và nếu nhu cầu awk của bạn nói 50 byte cho mục nhập băm, ánh xạ chuỗi (có lẽ là ngắn) thành giá trị chưa xác định, đó là 50GB. Đối với đầu vào được sắp xếp, bạn có thể làm uniq -dthủ công với awk '$0==p&&n++==1;$0!=p{p=$0;n=1}'nhưng tại sao phải bận tâm?
dave_thndry_085

@ dave_thndry_085 Cảm ơn ==1ý tưởng về ý tưởng tuyệt vời.
Isaac

Giả sử 30000 tệp có 100000 dòng gồm 80 ký tự và không trùng lặp , điều này sẽ yêu cầu awklưu trữ 2,4E11 byte (223 GiB).
Kusalananda

sort -m *.words | uniq -dlàm việc tuyệt vời Sau quá trình tôi chạy grepđể tìm một tập tin có chứa một mục trùng lặp. Bạn có thấy một cách để in ít nhất một tên tệp có chứa một mục trùng lặp không?
Lars Schneider

3

Tối ưu hóa sort+ uniqgiải pháp:

sort --parallel=30000 *.words | uniq -d
  • --parallel=N - thay đổi số lượng các loại chạy đồng thời thành N
  • -d, --repeated - chỉ in các dòng trùng lặp, một dòng cho mỗi nhóm
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.