grep với mẫu từ một tệp (3.2Gb) khớp trong tệp khác (4.8Gb)


7

Tôi có hai tập tin văn bản. Một là tệp văn bản có tên, địa chỉ email và các trường khác. Một số dòng từ file1:

John:myemail@gmail.com:johnson123:22hey
Erik:thatwhatsup@gmail.com:johnson133:22hey
Robert:whatsup@gmail.com:johnson123:21hey
Johnnny:bro@gmail.com:johnson123:22hey

Cái kia chỉ chứa địa chỉ email. Ví dụ từ file2:

1@gmail.com
rsdoge@gmail.com
mynameiscurt@hotmail.com
myemail@gmail.com

Tôi muốn đầu ra là mỗi dòng đầy đủ file1có địa chỉ email file2. Ví dụ: myemail@gmail.comlà trong file2, vì vậy tôi muốn xem dòng sau từ file1:

John:myemail@gmail.com:johnson123:22hey

Có cách nào dễ dàng để tìm kiếm file1và xuất các dòng khớp với "danh sách địa chỉ email" file2không?

Tôi đã tìm kiếm GIỜ, nhưng các tìm kiếm Google của tôi (và các tìm kiếm StackOverflow) cùng với những nỗ lực trên dòng lệnh cho đến nay vẫn chưa có hiệu quả.

Các lệnh tôi đã thử và nghĩ sẽ hoạt động:

fgrep -f file2.txt file1.txt > matched.txt
grep -F -f ....
grep -F -x -f file1 file2 > common 

v.v., nhưng tất cả đều có grep memory exhausted- các tệp tôi khớp là 4,8 GB ( file1) và 3,2 GB ( file2chỉ chứa các địa chỉ email). Tôi giả sử bộ nhớ bị cạn kiệt với các lệnh này. Tôi đã tìm thấy một phương thức sử dụng findđể thực thi các lệnh mượt mà hơn tôi đoán, nhưng không làm cho nó hoạt động.

tldr ; cần phải khớp file2với file1và nếu có một dòng từ file2đó khớp với một dòng trong file1, hãy xuất nó. Các tập tin lớn và tôi cần một cách an toàn để không sử dụng hết bộ nhớ.

Cảm ơn bạn, đã tìm kiếm cả ngày cho điều này và đã thử nghiệm, không muốn từ bỏ (5 giờ +).


8
Dữ liệu này là một ứng cử viên để đưa vào cơ sở dữ liệu.
Kusalananda

Ý anh là gì?
Axel Tobieson

4
Ý tôi là vì các tập dữ liệu quá lớn, nên có thể hiệu quả hơn khi để một công cụ cơ sở dữ liệu thực hiện truy vấn thay vì sử dụng các công cụ dòng lệnh Unix để thực hiện. Bây giờ tôi mới xem xét cách đọc dữ liệu vào SQLite hoặc MySQL để xem liệu tôi có thể truy vấn nó một cách hiệu quả hay không, nhưng nó sẽ bị trễ ở đây vì vậy tôi không biết liệu mình có thời gian để làm gì đó thật không. Những người khác có thể bước vào với các giải pháp khác.
Kusalananda

Được rồi, nó ở định dạng .txt.
Axel Tobieson

1
Đúng. Tôi nghĩ rằng tôi đã nhận được một số câu trả lời tuyệt vời nhưng tôi đã áp dụng câu trả lời của Costas và khiến nó hoạt động.! :)
Axel Tobieson

Câu trả lời:


7

Thật khó để vận hành một tệp lớn nhưng bạn có thể thực hiện theo 3 bước:

  1. Sắp xếp file1 theo trường thứ hai

    sort -k2,2 -t: file1 >file1.sorted
    
  2. Sắp xếp tập tin2

    sort file2 >file2.sorted
    
  3. Tham gia 2 tệp theo trường email

    join -t: -2 2 file2.sorted file1.sorted -o 2.1,0,2.3,2.4 >matched.txt
    

Bạn hoàn toàn không tính đến tài khoản :có thể xảy ra ở phần địa phương của địa chỉ email.
Anthon

@Anthon Đó là điểm yếu của định dạng được sử dụng để lưu trữ dữ liệu, chắc chắn
Score_Under

5

Tôi đang gửi câu trả lời thứ hai cho câu hỏi này (đây là một vấn đề thú vị). Giải pháp này hoàn toàn khác với giải pháp SQLite của tôi và từ các giải pháp sort+ trông khá hứa hẹn joinbắt đầu xuất hiện:

Sử dụng phương pháp ban đầu của bạn với grep -f, nhưng theo nghĩa đen là cắt giảm vấn đề một chút. Chúng ta hãy chia "tệp truy vấn", file2thành các phần có thể quản lý bằng cách sử dụng split.

Các splittiện ích có thể chia một file thành nhiều file nhỏ hơn dựa trên một số dòng.

Tệp 3,2 Gb có độ dài dòng trung bình 20 ký tự có khoảng 172.000.000 dòng (trừ khi tôi đã mắc lỗi số học). Việc chia thành 2000 tệp 85000 dòng trên mỗi tệp là có thể thực hiện được.

Vì thế,

$ mkdir testing
$ cd testing
$ split -l 85000 -a 4 ../file2

Các -a 4tùy chọn cho splitsử dụng bốn nhân vật sau khi ban đầu xđể tạo ra các tên tập tin cho các tập tin mới. Các tập tin sẽ được gọi xaaaa, xaaabvv

Sau đó chạy bản gốc grep -ftrên:

for f in x????; do
  grep -F -f "$f" ../file1
done

Điều này có thể giúp grepgiữ tập hợp các mẫu truy vấn nhỏ hơn nhiều trong bộ nhớ.

CẬP NHẬT : Với 145.526.885 dòng, sử dụng split -l 72000 -a 4để tạo khoảng 2000 tệp.

Nhớ xóa testingthư mục mỗi lần bạn cố gắng tạo một tập hợp các tệp tách mới.

Lưu ý rằng các tệp tách từ câu trả lời này có thể được sử dụng riêng lẻ làm đầu vào cho bất kỳ câu trả lời nào khác mà bạn có thể nhận được cho câu hỏi này.


đánh giá cao sự giúp đỡ, tôi đang cố gắng - sẽ cho bạn biết nếu nó hoạt động. còn tệp 3,2 Gb ở mức 145,526.885 (145,5m)
Axel Tobieson

@AxelTobieson Ở đó, tôi nghĩ rằng tôi đã nhận được nó ngay bây giờ. Xin lỗi vì sự nhầm lẫn của tôi. Không đảm bảo nó sẽ làm việc mặc dù. Các giải pháp sắp xếp + tham gia có lẽ cũng tốt.
Kusalananda

@AxelTobieson Tôi đã nhận được một số phản hồi và có phiên bản tốt hơn của câu trả lời ngay bây giờ.
Kusalananda

4

Câu trả lời của Costas có lẽ là vấn đề chính xác nhất được đưa ra bởi vì bạn có một trường có tỷ lệ trùng khớp 100%.

Nhưng nếu vấn đề của bạn thực sự xảy ra với hàng triệu regexps trong hàng tỷ dòng, thì GNU Parallel có một mô tả về cách thực hiện điều đó: https://www.gnu.org/software/abul/man.html#EXAMPLE:-Grepping -n-lines-for-m-normal-biểu thức

Giải pháp đơn giản nhất để grep một tệp lớn cho nhiều regexps là:

grep -f regexps.txt bigfile

Hoặc nếu biểu thức chính là các chuỗi cố định:

grep -F -f regexps.txt bigfile

Có 3 yếu tố giới hạn: CPU, RAM và I / O đĩa.

RAM rất dễ đo: Nếu quá trình grep chiếm phần lớn bộ nhớ trống của bạn (ví dụ: khi chạy trên cùng), thì RAM là một yếu tố hạn chế.

CPU cũng dễ dàng đo lường: Nếu grep chiếm> 90% CPU ở trên cùng, thì CPU là một yếu tố hạn chế và song song hóa sẽ tăng tốc độ này.

Khó có thể xem liệu I / O của đĩa là yếu tố giới hạn hay không, và tùy thuộc vào hệ thống đĩa, nó có thể nhanh hơn hoặc chậm hơn để song song hóa. Cách duy nhất để biết chắc chắn là kiểm tra và đo lường.

Yếu tố giới hạn: RAM

Bigfile grep -f regexs.txt bình thường hoạt động bất kể kích thước của bigfile, nhưng nếu regexps.txt quá lớn, nó không thể vừa với bộ nhớ, thì bạn cần phải tách nó ra.

grep -F chiếm khoảng 100 byte RAM và grep mất khoảng 500 byte RAM cho mỗi 1 byte regrec. Vì vậy, nếu regexps.txt là 1% RAM của bạn, thì nó có thể quá lớn.

Nếu bạn có thể chuyển đổi biểu thức chính quy của mình thành các chuỗi cố định, hãy làm điều đó. Ví dụ: nếu các dòng bạn đang tìm kiếm trong bigfile tất cả trông giống như:

ID1 foo bar baz Identifier1 quux
fubar ID2 foo bar baz Identifier2

sau đó regexps.txt của bạn có thể được chuyển đổi từ:

ID1.*Identifier1
ID2.*Identifier2

vào:

ID1 foo bar baz Identifier1
ID2 foo bar baz Identifier2

Bằng cách này, bạn có thể sử dụng grep -F, chiếm ít hơn 80% bộ nhớ và nhanh hơn nhiều.

Nếu nó vẫn không vừa trong bộ nhớ, bạn có thể làm điều này:

parallel --pipepart -a regexps.txt --block 1M grep -F -f - -n bigfile |
sort -un | perl -pe 's/^\d+://'

1M nên là bộ nhớ trống của bạn chia cho số lõi và chia cho 200 cho grep -F và 1000 cho grep bình thường. Trên GNU / Linux, bạn có thể làm:

free=$(awk '/^((Swap)?Cached|MemFree|Buffers):/ { sum += $2 }
          END { print sum }' /proc/meminfo)
percpu=$((free / 200 / $(parallel --number-of-cores)))k

parallel --pipepart -a regexps.txt --block $percpu --compress grep -F -f - -n bigfile |
sort -un | perl -pe 's/^\d+://'

Nếu bạn có thể sống với các dòng trùng lặp và thứ tự sai, thì nhanh hơn để làm:

parallel --pipepart -a regexps.txt --block $percpu --compress grep -F -f - bigfile

Yếu tố giới hạn: CPU

Nếu CPU là song song hệ số giới hạn thì nên thực hiện trên biểu thức chính quy:

cat regexp.txt | parallel --pipe -L1000 --round-robin --compress grep -f - -n bigfile |
sort -un | perl -pe 's/^\d+://'

Lệnh sẽ khởi động một grep trên mỗi CPU và đọc bigfile một lần cho mỗi CPU, nhưng vì điều đó được thực hiện song song, tất cả các lần đọc ngoại trừ lần đầu tiên sẽ được lưu trong bộ nhớ cache. Tùy thuộc vào kích thước của regrec.txt, có thể sử dụng nhanh hơn - chặn 10m thay vì -L1000.

Một số hệ thống lưu trữ hoạt động tốt hơn khi đọc song song nhiều khối. Điều này đúng với một số hệ thống RAID và một số hệ thống tệp mạng. Để song song việc đọc bigfile:

parallel --pipepart --block 100M -a bigfile -k --compress grep -f regexp.txt

Điều này sẽ chia bigfile thành các khối 100MB và chạy grep trên mỗi khối này. Để song song cả việc đọc bigfile và regapi.txt, hãy kết hợp cả hai bằng cách sử dụng --fifo:

parallel --pipepart --block 100M -a bigfile --fifo cat regexp.txt \
\| parallel --pipe -L1000 --round-robin grep -f - {}

Nếu một dòng khớp với nhiều biểu thức chính, dòng có thể được nhân đôi.

Vấn đề lớn hơn

Nếu vấn đề quá lớn để giải quyết bằng cách này, có lẽ bạn đã sẵn sàng cho Lucene.


2

Từ chối trách nhiệm quan trọng: Tôi đã kiểm tra điều này trên dữ liệu được cung cấp trong câu hỏi. Việc tải một vài gigabyte dữ liệu vào cơ sở dữ liệu SQLite có thể mất nhiều thời gian. Việc truy vấn bằng cách sử dụng hai trường văn bản có thể không hiệu quả. Hiệu suất đĩa có thể là yếu tố. V.v.

Sau đây shkịch bản sẽ tạo ra cơ sở dữ liệu SQLLite database.db(file này sẽ bị xóa nếu nó đã tồn tại), tạo các bảng qadrdata, và nạp dữ liệu vào hai bảng ( file1vào datafile2ra qadr). Sau đó nó sẽ tạo ra một chỉ mục trên data.adr.

#!/bin/sh

address_file="file2"
data_file="file1"

database="database.db"

rm -f "$database"

sqlite3 "$database" <<END_SQL
CREATE TABLE qadr ( adr TEXT );
CREATE TABLE data ( name TEXT, adr TEXT, tag1 TEXT, tag2 TEXT );
.separator :
.import "$data_file" data
.import "$address_file" qadr
VACUUM;
CREATE UNIQUE INDEX adri ON data(adr);
VACUUM;
END_SQL

Việc tạo chỉ mục giả định rằng các địa chỉ trong file1là duy nhất (nghĩa là trường giới hạn thứ hai :là duy nhất). Nếu chúng không phải, sau đó loại bỏ UNIQUEkhỏi CREATE INDEXcâu lệnh (lý tưởng, chúng là duy nhất và lý tưởng, các dòng trong file2cũng là duy nhất).

Tôi chưa bao giờ làm việc với SQLite và những lượng dữ liệu này, nhưng tôi biết rằng việc nhập nhiều gigabyte vào MongoDB và MySQL có thể rất chậm và việc tạo chỉ mục cũng có thể tốn thời gian. Vì vậy, về cơ bản, tôi đang nói rằng tôi chỉ đang ném cái này ra cho một người có nhiều dữ liệu để kiểm tra.

Sau đó, đây là vấn đề của một truy vấn đơn giản:

$ sqlite3 database.db 'SELECT data.* FROM data JOIN qadr ON (data.adr = qadr.adr)'
John|myemail@gmail.com|johnson123|22hey

hoặc thậm chí có thể chỉ

$ sqlite3 database.db 'SELECT * FROM data NATURAL JOIN qadr'
John|myemail@gmail.com|johnson123|22hey

Ai đó có nhiều kiến ​​thức về SQLite chắc chắn sẽ đưa ra nhận xét mang tính xây dựng về điều này.


1
Chỉ cần sử dụng :như một ngăn cách là một simlistic quá mức. A :có thể nằm trong phần cục bộ của một địa chỉ email hợp lệ.
Anthon

1
@Anthon Không biết điều đó. Điều này sẽ yêu cầu gửi dữ liệu tới một số định dạng trước khi nhập, có thể yêu cầu phân tích cú pháp và xác thực địa chỉ email. Tôi sẽ xem xét rằng ngoài phạm vi những gì tôi sẵn sàng làm cho câu hỏi cụ thể này. Các câu trả lời khác có thể có kết quả hơn nếu đây là trường hợp (hoặc thậm chí bất kể có địa chỉ kỳ lạ trong danh sách).
Kusalananda

1
các :dấu phân cách có thể được cố định một cách dễ dàng với awk hoặc perl. chia thành một mảng bằng cách sử dụng: như dấu phân cách. nếu mảng có 4 trường, sử dụng nó như là. nếu nó có 5 trường, hãy nối các trường 2 & 3 với :, xóa trường 3, sau đó sử dụng. "sử dụng" có thể đơn giản như đầu ra với các dấu phân cách TAB và chuyển thành sqlite để nhập. hoặc trích dẫn chính xác và CSV. hoặc json hoặc XML. BTW, với các tệp có kích thước này, tôi có xu hướng sử dụng postgresql hoặc mysql thay vì sqlite.
cas

2

Nếu bạn cần tránh một giải pháp DB (không chắc tại sao, đó có vẻ là ý tưởng tốt nhất với tôi), bạn có thể thực hiện bằng cách sắp xếp hai tệp trên địa chỉ email và sau đó sử dụng joinlệnh, gần đúng với những gì DB sẽ làm.

Đây là những gì tôi đã làm:

sort -t: +1 file1 -o file1
sort file2 -o file2
join -t: -o 1.1,1.2,1.3,1.4 -1 2 file1 file2

Điều đó dường như làm đúng với dữ liệu mẫu của bạn. Nó sắp xếp các tập tin tại chỗ . Nếu bạn không muốn điều đó, hãy thay đổi -otùy chọn trên sorts thành tên tệp tạm thời và sau đó sử dụng tên trong liên kết. Ngoài ra, nếu bạn thực sự có 4 trường khác trong tệp đầu tiên, bạn phải tính đến trường đó trong -otùy chọn join.

Để biết thêm chi tiết, tham khảo trang người đàn ông.


Bạn hoàn toàn không tính đến việc: có thể xảy ra ở phần cục bộ của địa chỉ email.
Anthon

1

Một cái gì đó như thế này sẽ hoạt động, nhưng tôi không chắc đó là một ý tưởng tốt tùy thuộc vào trường hợp sử dụng của bạn (chưa được kiểm tra):

while read f2line
do
  f1=$(grep $line file1)

  [[ ! -z $f1 ]] && echo $f1line 
done < file2

Một giải pháp khả thi khác nếu bạn muốn có thêm phương pháp một lớp (nhanh chóng thử nghiệm bên dưới):

grep . file2 | xargs -i^ grep ^ file1

Mà mang lại:

root@7Z233W1 (/tmp)# cat f1
John:myemail@gmail.com:johnson123:22hey
Erik:thatwhatsup@gmail.com:johnson133:22hey
Robert:whatsup@gmail.com:johnson123:21hey
Johnnny:bro@gmail.com:johnson123:22hey

root@7Z233W1 (/tmp)# cat f2
1@gmail.com
rsdoge@gmail.com
mynameiscurt@hotmail.com
myemail@gmail.com

root@7Z233W1 (/tmp)# grep . f2 | xargs -i^ grep ^ f1
John:myemail@gmail.com:johnson123:22hey

1
Giải pháp thứ hai có vẻ hợp lý hơn khi giải pháp đầu tiên thực hiện một grep trên mỗi dòng trong tệp 3,2 Gb.
Kusalananda

0

Đây là phiên bản tập lệnh của Kusalananda, sử dụng perlđể chuyển đổi file1từ :tách thành TAB tách ra trước khi đưa vào sqlite3.

Tập perllệnh nhúng kiểm tra xem có 5 trường chứ không phải 4. Nếu có, nó nối thêm trường 3 vào trường 2 (khôi phục lại trường :đã được xóa bởi autosplit), sau đó xóa trường 3.

#!/bin/sh

address_file="file2"
data_file="file1"

database="database.db"

rm -f "$database"

sqlite3 "$database" <<END_SQL
CREATE TABLE qadr ( adr TEXT );
CREATE TABLE data ( name TEXT, adr TEXT, tag1 TEXT, tag2 TEXT );
.mode line
.import "$address_file" qadr
END_SQL

perl -F: -lane 'if (@F == 5) {
    $F[1] .= ":" . $F[2];  # perl arrays are zero-based
    delete $F[2];
  };
  print join("\t",@F);' $data_file | 
    sqlite3 "$database" -separator $'\t' '.import /dev/stdin data'


sqlite3 "$database" <<END_SQL
VACUUM;
CREATE UNIQUE INDEX adri ON data(adr);
VACUUM;
END_SQL

IMO, sqlite không phù hợp với cơ sở dữ liệu lớn này. Tôi khuyên bạn nên sử dụng mysqlhoặc postgresqlthay vào đó. Đối với loại nhiệm vụ này, mysqltốc độ thô có thể làm cho nó trở thành lựa chọn tốt hơn - nhanh hơn đối với những thứ đơn giản như thế này nhưng postgresql nhanh hơn nhiều đối với các tác vụ phức tạp hơn - theo kinh nghiệm của tôi, pg là "thông minh nhanh" (nghĩa là nó có thể đạt được khối lượng lớn cải thiện tốc độ trong các nhiệm vụ phức tạp bằng cách làm việc thông minh thay vì làm việc chăm chỉ), mysql "câm nhanh" (nghĩa là nó làm việc chăm chỉ, không có nhiều khả năng để làm việc thông minh).

Tập lệnh ở trên có thể dễ dàng được điều chỉnh để hoạt động với các máy khách psqlhoặc mysqldòng lệnh thay vì sqlite3, nhưng tôi sẽ sửa đổi các CREATE TABLElệnh để sử dụng kích thước cố định CHARACTER(size)thay vì TEXT, đó sizelà một phỏng đoán hợp lý về kích thước tối đa cho mỗi trường là gì - ví dụ có thể 255 ký tự cho adrtrường và 10-50 ký tự cho các trường khác.

một tối ưu hóa có thể là cẩn thận chọn kích thước trường sao cho mỗi bản ghi là số chia đều cho kích thước khối của ổ đĩa của bạn (có tính đến chi phí trên mỗi bản ghi của mysql / postgresql). 512 byte phải tốt cho tất cả các kích thước khối phổ biến. làm cho các trường bất kỳ kích thước nào bạn cần và thêm một trường bổ sung, không sử dụng CHARACTER(size)để tạo sự khác biệt. Mục đích của việc này là để các bản ghi không bao giờ vượt qua một ranh giới khối, do đó, công cụ db chỉ phải đọc trong một khối đĩa để lấy tất cả dữ liệu cho một bản ghi đã cho (thực tế, nó sẽ đọc nhiều bản ghi trong một khối với hầu hết các kích thước khối hiện tại, nhưng điều đó chỉ giúp hiệu suất, không thể làm tổn thương nó).

https://dba.stackexchange.com/ có lẽ là trang web tốt nhất để tìm kiếm hoặc hỏi thông tin về tối ưu hóa kích thước bản ghi.

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.