Cara cepat menemukan baris dalam satu file yang tidak ada di file lain?


241

Saya memiliki dua file besar (set nama file). Sekitar 30.000 baris di setiap file. Saya mencoba menemukan cara cepat untuk menemukan baris di file1 yang tidak ada di file2.

Misalnya, jika ini file1:

line1
line2
line3

Dan ini file2:

line1
line4
line5

Maka hasil / output saya harus:

line2
line3

Ini bekerja:

grep -v -f file2 file1

Tapi itu sangat, sangat lambat saat digunakan pada file besar saya.

Saya menduga ada cara yang baik untuk melakukan ini menggunakan diff (), tetapi output harus hanya garis, tidak ada yang lain, dan saya tidak bisa menemukan saklar untuk itu.

Adakah yang bisa membantu saya menemukan cara cepat untuk melakukan ini, menggunakan bash dan binari linux dasar?

EDIT: Untuk menindaklanjuti pertanyaan saya sendiri, ini adalah cara terbaik yang saya temukan sejauh ini menggunakan diff ():

diff file2 file1 | grep '^>' | sed 's/^>\ //'

Tentunya harus ada cara yang lebih baik?


1
Anda dapat mencoba ini jika lebih cepat:awk 'NR==FNR{a[$0];next}!($0 in a)' file2 file1 > out.txt
Kent


4
Terima kasih telah memberi tahu tentang grep -v -f file2 file1
Rahul Prasad


Cara sederhana dengan set alat tereduksi:, cat file1 file2 file2 | sort | uniq --uniquelihat jawaban saya di bawah ini.
Ondra Žižka

Jawaban:


233

Anda dapat mencapai ini dengan mengontrol pemformatan baris lama / baru / tidak berubah dalam diffoutput GNU :

diff --new-line-format="" --unchanged-line-format=""  file1 file2

File input harus disortir agar ini berfungsi. Dengan bash(dan zsh) Anda dapat mengurutkan di tempat dengan penggantian proses <( ):

diff --new-line-format="" --unchanged-line-format="" <(sort file1) <(sort file2)

Dalam baris baru dan tidak berubah di atas ditekan, sehingga hanya diubah (yaitu garis yang dihapus dalam kasus Anda) adalah output. Anda juga dapat menggunakan beberapa diffpilihan yang solusi lain tidak menawarkan, seperti -imengabaikan kasus, atau berbagai pilihan spasi ( -E, -b, -vdll) untuk pencocokan kurang ketat.


Penjelasan

Opsi --new-line-format, --old-line-formatdan --unchanged-line-formatbiarkan Anda mengontrol cara diffmemformat perbedaan, mirip dengan printfpenentu format. Opsi-opsi ini memformat masing-masing baris baru (ditambahkan), lama (dihapus) dan tidak berubah . Menyetel satu untuk mengosongkan "" mencegah keluaran dari jalur semacam itu.

Jika Anda terbiasa dengan format diff terpadu , Anda dapat membuatnya sebagian dengan:

diff --old-line-format="-%L" --unchanged-line-format=" %L" \
     --new-line-format="+%L" file1 file2

The %Lspecifier adalah garis yang bersangkutan, dan kami awalan masing-masing dengan "+" "-" atau "", seperti diff -u (catatan bahwa itu hanya perbedaan output, itu tidak memiliki --- +++dan @@garis-garis di bagian atas setiap perubahan dikelompokkan). Anda juga dapat menggunakan ini untuk melakukan hal-hal lain yang bermanfaat seperti jumlah setiap baris dengan %dn.


The diffMetode (bersama dengan saran lain commdan join) hanya menghasilkan output yang diharapkan dengan diurutkan masukan, meskipun Anda dapat menggunakan <(sort ...)untuk mengurutkan di tempat. Berikut awkskrip sederhana (nawk) (terinspirasi oleh skrip yang ditautkan ke dalam jawaban Konsolebox) yang menerima file input yang dipesan secara sewenang-wenang, dan menampilkan baris yang hilang sesuai urutannya pada file1.

# output lines in file1 that are not in file2
BEGIN { FS="" }                         # preserve whitespace
(NR==FNR) { ll1[FNR]=$0; nl1=FNR; }     # file1, index by lineno
(NR!=FNR) { ss2[$0]++; }                # file2, index by string
END {
    for (ll=1; ll<=nl1; ll++) if (!(ll1[ll] in ss2)) print ll1[ll]
}

Ini menyimpan seluruh konten file1 baris demi baris dalam array yang diindeks nomor-baris ll1[], dan seluruh konten file2 baris demi baris dalam array asosiatif yang diindeks baris-konten ss2[]. Setelah kedua file dibaca, beralihlah ll1dan gunakan inoperator untuk menentukan apakah baris dalam file1 ada di file2. (Ini akan memiliki output yang berbeda dengan diffmetode ini jika ada duplikat.)

Jika file-file tersebut cukup besar sehingga menyimpan keduanya menyebabkan masalah memori, Anda dapat menukar CPU dengan memori dengan hanya menyimpan file1 dan menghapus kecocokan sepanjang jalan saat file2 dibaca.

BEGIN { FS="" }
(NR==FNR) {  # file1, index by lineno and string
  ll1[FNR]=$0; ss1[$0]=FNR; nl1=FNR;
}
(NR!=FNR) {  # file2
  if ($0 in ss1) { delete ll1[ss1[$0]]; delete ss1[$0]; }
}
END {
  for (ll=1; ll<=nl1; ll++) if (ll in ll1) print ll1[ll]
}

Di atas menyimpan seluruh isi file1 dalam dua array, satu diindeks dengan nomor baris ll1[], satu diindeks oleh konten baris ss1[]. Kemudian saat file2 dibaca, setiap baris yang cocok dihapus dari ll1[]dan ss1[]. Pada akhirnya baris yang tersisa dari file1 adalah output, mempertahankan urutan asli.

Dalam hal ini, dengan masalah seperti yang disebutkan, Anda juga dapat membagi dan menaklukkan menggunakan GNU split(pemfilteran adalah ekstensi GNU), dijalankan berulang dengan potongan file1 dan membaca file2 sepenuhnya setiap kali:

split -l 20000 --filter='gawk -f linesnotin.awk - file2' < file1

Perhatikan penggunaan dan penempatan -makna stdinpada gawkbaris perintah. Ini disediakan oleh splitdari file1 dalam potongan 20.000 baris per-doa.

Untuk pengguna pada sistem non-GNU, ada hampir pasti coreutils GNU paket Anda dapat memperoleh, termasuk di OSX sebagai bagian dari Apel Xcode alat yang menyediakan GNU diff, awk, meskipun hanya POSIX / BSD splitdaripada versi GNU.


1
Ini melakukan persis apa yang saya butuhkan, dalam sebagian kecil dari waktu yang diambil oleh grep yang sangat besar. Terima kasih!
Niels2000

1
Menemukan halaman manual gnu
Juto

beberapa dari kita tidak pada gnu [OS X bsd di sini ...] :)
rogerdpack

1
Saya berasumsi Anda bermaksud untuk diff: secara umum file input akan berbeda, 1 dikembalikan oleh diffdalam hal itu. Anggap itu bonus ;-) Jika Anda menguji dalam skrip shell 0 dan 1 adalah kode keluar yang diharapkan, 2 menunjukkan masalah.
mr.spuratic

1
@ mr.spuratic ah ya, sekarang saya menemukannya di man diff. Terima kasih!
Archeosudoerus

246

The comm perintah (singkatan dari "umum") mungkin bergunacomm - compare two sorted files line by line

#find lines only in file1
comm -23 file1 file2 

#find lines only in file2
comm -13 file1 file2 

#find lines common to both files
comm -12 file1 file2 

The manfile sebenarnya cukup mudah dibaca untuk ini.


6
Bekerja dengan sempurna di OSX.
pisaruk

41
Persyaratan untuk input yang disortir mungkin harus disorot.
tripleee

21
commjuga memiliki opsi untuk memverifikasi input yang disortir, --check-order(yang tampaknya tetap dilakukan, tetapi opsi ini akan menyebabkan kesalahan alih-alih melanjutkan). Tetapi untuk mengurutkan file, cukup lakukan: com -23 <(sort file1) <(sort file2)dan seterusnya
michael

Saya membandingkan file yang dibuat di Windows dengan file yang dibuat di Linux dan sepertinya commtidak berfungsi sama sekali. Perlu beberapa saat bagi saya untuk mengetahui bahwa ini tentang ujung garis: bahkan garis yang terlihat identik dianggap berbeda jika mereka memiliki ujung garis yang berbeda. Perintah dos2unixini dapat digunakan untuk mengubah ujung garis CRLF menjadi LF saja.
ZeroOne

23

Seperti konsolebox yang disarankan, solusi poster grep

grep -v -f file2 file1

sebenarnya berfungsi dengan baik (cepat) jika Anda hanya menambahkan -Fopsi, untuk memperlakukan pola sebagai string tetap, bukan ekspresi reguler. Saya memverifikasi ini pada sepasang ~ 1000 daftar file baris yang harus saya bandingkan. Dengan -Fitu butuh 0,031 s (nyata), sementara tanpa itu butuh 2,278 s (nyata), ketika mengarahkan ulang keluaran grep ke wc -l.

Tes-tes ini juga termasuk -xsakelar, yang merupakan bagian penting dari solusi untuk memastikan akurasi total dalam kasus-kasus di mana file2 berisi baris-baris yang cocok dengan bagian, tetapi tidak semua, satu atau lebih baris dalam file1.

Jadi solusi yang tidak memerlukan input untuk diurutkan, cepat, fleksibel (sensitivitas huruf, dll) adalah:

grep -F -x -v -f file2 file1

Ini tidak berfungsi dengan semua versi grep, misalnya gagal di macOS, di mana baris dalam file 1 akan ditampilkan sebagai tidak ada di file 2, meskipun itu, jika cocok dengan baris lain yang merupakan substring dari itu . Atau Anda dapat menginstal GNU grep di macOS untuk menggunakan solusi ini.


Ya, itu bekerja tetapi bahkan dengan -Fini tidak skala baik.
Molomby

ini tidak secepat itu, saya menunggu 5 menit untuk 2 file dari ~ 500k baris sebelum menyerah
cahen

sebenarnya, cara ini masih lebih lambat daripada cara comm, karena cara ini dapat menangani file yang tidak disortir dan kemudian diseret oleh unsorting, comm mengambil keuntungan dari pengurutan
workplaylifecycle

@ workplaylifecycle Anda perlu menambahkan waktu untuk menyortir yang mungkin menjadi hambatan untuk ukuran yang sangat besar file2.
pertama

Namun, grep dengan -xopsi tersebut ternyata menggunakan lebih banyak memori. Dengan file2180M kata yang berisi 6-10 byte proses saya Killedmenggunakan mesin RAM 32GB ...
pertama

11

Bagaimana kecepatan sebagai sort dan diff?

sort file1 -u > file1.sorted
sort file2 -u > file2.sorted
diff file1.sorted file2.sorted

1
Terima kasih telah mengingatkan saya tentang perlunya mengurutkan file sebelum melakukan diff. sort + diff JAUH lebih cepat.
Niels2000

4
one liner ;-) diff <(sort file1 -u) <(sort file2 -u)
steveinatorx

11

Jika Anda kekurangan "alat mewah", misalnya dalam beberapa distribusi Linux minimal, ada solusi dengan adil cat, sortdan uniq:

cat includes.txt excludes.txt excludes.txt | sort | uniq --unique

Uji:

seq 1 1 7 | sort --random-sort > includes.txt
seq 3 1 9 | sort --random-sort > excludes.txt
cat includes.txt excludes.txt excludes.txt | sort | uniq --unique

# Output:
1
2    

Ini juga relatif cepat, dibandingkan dengan grep.


1
Catatan - beberapa implementasi tidak akan mengenali --uniqueopsi. Anda harus dapat menggunakan opsi POSIX standar untuk ini:| uniq -u
AndrewF

1
Dalam contoh, dari mana "2" berasal?
Niels2000

1
@ Niels2000, seq 1 1 7membuat angka dari 1, dengan selisih 1, hingga 7, yaitu 1 2 3 4 5 6 7. Dan di sana ada 2 Anda!
Eirik Lygre

5
$ join -v 1 -t '' file1 file2
line2
line3

The -tmemastikan bahwa itu membandingkan seluruh baris, jika Anda memiliki ruang dalam beberapa baris.


Seperti comm, joinmengharuskan kedua jalur input untuk diurutkan pada bidang di mana Anda menjalankan operasi gabungan.
tripleee

4

Anda dapat menggunakan Python:

python -c '
lines_to_remove = set()
with open("file2", "r") as f:
    for line in f.readlines():
        lines_to_remove.add(line.strip())

with open("f1", "r") as f:
    for line in f.readlines():
        if line.strip() not in lines_to_remove:
            print(line.strip())
'

4

Gunakan combinedari moreutilspaket, utilitas set yang mendukung not, and, or, xoroperasi

combine file1 not file2

yaitu memberi saya baris yang ada di file1 tetapi tidak di file2

ATAU beri saya baris dalam file1 baris minus di file2

Catatan: combine mengurutkan dan menemukan baris unik di kedua file sebelum melakukan operasi apa pun tetapi difftidak. Jadi, Anda mungkin menemukan perbedaan antara output diffdan combine.

Jadi sebenarnya Anda katakan

Temukan baris yang berbeda di file1 dan file2 dan kemudian beri saya baris di file1 dikurangi baris di file2

Dalam pengalaman saya, ini jauh lebih cepat daripada opsi lain


2

Menggunakan fgrep atau menambahkan opsi -F untuk grep bisa membantu. Tetapi untuk perhitungan yang lebih cepat Anda bisa menggunakan Awk.

Anda dapat mencoba salah satu metode Awk ini:

http://www.linuxquestions.org/questions/programming-9/grep-for-huge-files-826030/#post4066219


2
+1 Ini adalah satu-satunya jawaban yang tidak memerlukan input untuk diurutkan. Meskipun tampaknya OP senang dengan persyaratan itu, itu merupakan kendala yang tidak dapat diterima dalam banyak skenario dunia nyata.
tripleee

1

Cara saya biasanya melakukan ini adalah menggunakan --suppress-common-linesflag, meskipun perhatikan bahwa ini hanya berfungsi jika Anda melakukannya dalam format berdampingan.

diff -y --suppress-common-lines file1.txt file2.txt


0

Saya menemukan bahwa bagi saya menggunakan pernyataan normal jika dan untuk loop bekerja dengan sempurna.

for i in $(cat file2);do if [ $(grep -i $i file1) ];then echo "$i found" >>Matching_lines.txt;else echo "$i missing" >>missing_lines.txt ;fi;done

2
Lihat DontReadLinesWithFor . Selain itu, kode ini akan berperilaku sangat buruk jika salah satu grephasil Anda diperluas ke beberapa kata, atau jika salah satu file2entri Anda dapat diperlakukan oleh shell sebagai gumpalan.
Charles Duffy
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.