วิธีที่รวดเร็วในการค้นหาบรรทัดในไฟล์หนึ่งที่ไม่ได้อยู่ในอีก?


241

ฉันมีไฟล์ขนาดใหญ่สองไฟล์ (ชุดชื่อไฟล์) ประมาณ 30,000 บรรทัดในแต่ละไฟล์ ฉันพยายามค้นหาวิธีที่รวดเร็วในการค้นหาบรรทัดใน file1 ที่ไม่ปรากฏใน file2

ตัวอย่างเช่นถ้านี่คือ file1:

line1
line2
line3

และนี่คือ file2:

line1
line4
line5

ดังนั้นผลลัพธ์ / ผลลัพธ์ของฉันควรเป็น:

line2
line3

งานนี้:

grep -v -f file2 file1

แต่มันช้ามากเมื่อใช้กับไฟล์ขนาดใหญ่ของฉัน

ฉันสงสัยว่ามีวิธีที่ดีในการทำเช่นนี้โดยใช้ diff () แต่ผลลัพธ์ควรเป็นเพียงแค่เส้นไม่มีอะไรอื่นและฉันไม่สามารถหาสวิตช์ได้

ใครช่วยฉันหาวิธีที่รวดเร็วในการทำเช่นนี้โดยใช้ทุบตีและไบนารีลินุกซ์ขั้นพื้นฐาน?

แก้ไข: เพื่อติดตามคำถามของฉันนี้เป็นวิธีที่ดีที่สุดที่ฉันได้พบโดยใช้ diff ():

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

แน่นอนต้องมีวิธีที่ดีกว่า


1
คุณสามารถลองสิ่งนี้ถ้ามันเร็วกว่า:awk 'NR==FNR{a[$0];next}!($0 in a)' file2 file1 > out.txt
เคนท์

โดยไม่ต้องใช้ความรวดเร็ว: stackoverflow.com/questions/4366533/…
Ciro Santilli 法轮功冠状病病六四事件法轮功

4
ขอบคุณที่บอกเกี่ยวกับ grep -v -f file2 file1
Rahul Prasad

1
ดูเพิ่มเติม: วิธีที่เร็วที่สุดที่จะหาสายของไฟล์จากไฟล์อื่นขนาดใหญ่ในทุบตี
codeforester

วิธีง่ายๆในการลดชุดเครื่องมือ: cat file1 file2 file2 | sort | uniq --uniqueดูคำตอบของฉันด้านล่าง
Ondra Žižka

คำตอบ:


233

คุณสามารถทำได้โดยการควบคุมการจัดรูปแบบของบรรทัดเก่า / ใหม่ / ไม่เปลี่ยนแปลงในdiffเอาต์พุตGNU :

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

ไฟล์อินพุตควรถูกเรียงลำดับเพื่อให้ทำงานได้ ด้วยbash(และzsh) คุณสามารถเรียงลำดับแบบแทนที่ด้วยการทดแทนกระบวนการ<( ):

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

ในสายใหม่และไม่เปลี่ยนแปลงข้างต้นจะถูกระงับดังนั้นการเปลี่ยนแปลงเท่านั้น(เช่นสายที่ถูกลบออกในกรณีของคุณ) จะถูกส่งออก นอกจากนี้คุณยังสามารถใช้diffตัวเลือกสองสามอย่างที่โซลูชันอื่นไม่เสนอเช่น-iเพื่อละเว้นตัวพิมพ์เล็กหรือตัวเลือกช่องว่างต่าง ๆ ( -E, -bและ-vอื่น ๆ ) สำหรับการจับคู่ที่เข้มงวดน้อยกว่า


คำอธิบาย

ตัวเลือก--new-line-format, --old-line-formatและ--unchanged-line-formatช่วยให้คุณสามารถควบคุมวิธีการdiffรูปแบบที่แตกต่างกันคล้ายกับ printfspecifiers รูปแบบ ตัวเลือกเหล่านี้จัดรูปแบบใหม่ (เพิ่ม), เก่า (ลบ) และไม่เปลี่ยนแปลงบรรทัดตามลำดับ การตั้งค่าหนึ่งให้ว่าง "" ป้องกันการแสดงผลของบรรทัดประเภทนั้น

หากคุณคุ้นเคยกับรูปแบบdiffแบบรวมคุณสามารถสร้างใหม่ได้บางส่วนด้วย:

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

ตัว%Lระบุเป็นบรรทัดที่มีปัญหาและเรานำหน้าแต่ละคำด้วย "+" "-" หรือ "" เช่นdiff -u (โปรดทราบว่ามันจะให้ผลลัพธ์ที่แตกต่างเท่านั้นมันขาด--- +++และ@@บรรทัดที่ด้านบนของการเปลี่ยนแปลงที่จัดกลุ่มไว้แต่ละรายการ) นอกจากนี้คุณยังสามารถใช้ในการทำสิ่งที่มีประโยชน์อื่น ๆ เช่นจำนวนแต่ละบรรทัด%dnด้วย


diffวิธี (พร้อมกับคำแนะนำอื่น ๆcommและjoin) เพียง แต่ผลิตการส่งออกที่คาดว่าจะมีการจัดเรียงการป้อนข้อมูลแม้ว่าคุณจะสามารถใช้<(sort ...)ในการจัดเรียงในสถานที่ ต่อไปนี้เป็นawkสคริปต์ (nawk) แบบง่าย ๆ(ได้รับแรงบันดาลใจจากสคริปต์ที่ลิงก์ไปยังในคำตอบของ Konsolebox) ซึ่งยอมรับไฟล์อินพุตที่ได้รับคำสั่งตามอำเภอใจและเอาต์พุตบรรทัดที่ขาดหายไปตามลำดับที่เกิดขึ้นใน

# 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]
}

ร้านค้านี้เนื้อหาทั้งหมดของ file1 ละบรรทัดเป็นเส้นจำนวนอาร์เรย์จัดทำดัชนีll1[]และเนื้อหาทั้งหมดของ file2 ss2[]ละบรรทัดเป็นเส้นเนื้อหาการจัดทำดัชนีอาเรย์ หลังจากอ่านไฟล์ทั้งสองแล้วให้วนซ้ำll1และใช้inโอเปอเรเตอร์เพื่อตรวจสอบว่ามีบรรทัดใน file1 อยู่ใน file2 หรือไม่ (สิ่งนี้จะมีเอาท์พุทที่แตกต่างกันกับdiffวิธีการถ้ามีซ้ำกัน)

ในกรณีที่ไฟล์มีขนาดใหญ่พอที่จัดเก็บทั้งคู่ทำให้เกิดปัญหาหน่วยความจำคุณสามารถแลกเปลี่ยน CPU สำหรับหน่วยความจำได้โดยการจัดเก็บเฉพาะไฟล์ 1 และการลบการจับคู่ไปพร้อมกันตามที่อ่านไฟล์ 2

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]
}

ด้านบนจัดเก็บเนื้อหาทั้งหมดของ file1 ในสองอาร์เรย์หนึ่งรายการที่จัดทำดัชนีโดยหมายเลขบรรทัดll1[]หนึ่งรายการที่จัดทำดัชนีตามเนื้อหาss1[]รายการ แล้วเป็น file2 จะอ่านแต่ละบรรทัดจับคู่ถูกลบออกจากและll1[] ss1[]ในตอนท้ายบรรทัดที่เหลือจาก file1 จะถูกส่งออกรักษาคำสั่งเดิม

ในกรณีนี้ด้วยปัญหาตามที่ระบุไว้คุณสามารถแบ่งและพิชิตโดยใช้ GNU split(การกรองเป็นส่วนขยาย GNU) เรียกใช้ซ้ำด้วยชิ้นส่วนของไฟล์ 1 และอ่านไฟล์ 2 ทุกครั้ง:

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

สังเกตการใช้งานและการจัดวาง-ความหมายstdinในgawkบรรทัดคำสั่ง สิ่งนี้จัดทำโดยsplitจาก file1 เป็นชิ้นละ 20,000 บรรทัดต่อการเรียกใช้

สำหรับผู้ใช้ในระบบที่ไม่ GNU มีเกือบแน่นอน coreutils GNU แพคเกจคุณสามารถได้รับรวมทั้งใน OSX เป็นส่วนหนึ่งของแอปเปิ้ล Xcodeเครื่องมือซึ่งมี GNU diff, awkแต่เพียง POSIX / BSD splitมากกว่ารุ่น GNU


1
สิ่งนี้ทำในสิ่งที่ฉันต้องการในเวลาเพียงไม่กี่นาทีโดย grep มหาศาล ขอบคุณ!
Niels2000

1
พบmanpage gnu
Juto

พวกเราบางคนไม่ได้อยู่ที่ gnu [OS X bsd ที่นี่ ... ] :)
rogerdpack

1
ฉันคิดว่าคุณหมายถึงdiff: โดยทั่วไปไฟล์อินพุตจะแตกต่างกัน 1 จะถูกส่งกลับโดยdiffในกรณีนั้น พิจารณาโบนัส ;-) หากคุณกำลังทดสอบในเชลล์สคริปต์ 0 และ 1 เป็นรหัสออกที่คาดไว้ 2 หมายถึงปัญหา
mr.spuratic

1
@ mr.spuratic man diffอาใช่ตอนนี้ผมพบว่ามันใน ขอบคุณ!
Archeosudoerus

246

สื่อสารคำสั่ง (ย่อมาจาก "คนธรรมดา") อาจจะมีประโยชน์comm - 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 

manไฟล์เป็นจริงค่อนข้างอ่านได้สำหรับเรื่องนี้


6
ทำงานได้อย่างไม่มีที่ติบน OSX
pisaruk

41
ความต้องการสำหรับการป้อนข้อมูลเรียงอาจจะเน้น
tripleee

21
commนอกจากนี้ยังมีตัวเลือกในการตรวจสอบอินพุตถูกจัดเรียง--check-order(ซึ่งดูเหมือนว่าจะทำอยู่แล้ว แต่ตัวเลือกนี้จะทำให้เกิดข้อผิดพลาดแทนที่จะดำเนินการต่อ) แต่การจัดเรียงไฟล์ทำได้ง่ายๆ: com -23 <(sort file1) <(sort file2)และอื่น ๆ

ฉันเปรียบเทียบไฟล์ที่สร้างใน Windows กับไฟล์ที่สร้างใน Linux และดูเหมือนcommว่าไม่ได้ทำงานเลย ฉันใช้เวลาสักครู่กว่าจะเข้าใจว่ามันเกี่ยวกับการจบบรรทัด: แม้แต่บรรทัดที่มีลักษณะเหมือนกันจะถือว่าแตกต่างกันหากพวกเขามีการลงท้ายด้วยเส้นที่แตกต่างกัน คำสั่งdos2unixสามารถใช้เพื่อแปลงจุดสิ้นสุดของบรรทัด CRLF เป็น LF เท่านั้น
ZeroOne

23

เช่นเดียวกับที่แนะนำให้ใช้ konsolebox โปสเตอร์ grep solution

grep -v -f file2 file1

ใช้งานได้ดีมาก (เร็ว) หากคุณเพิ่ม-Fตัวเลือกเพื่อใช้รูปแบบเป็นสตริงคงที่แทนที่จะใช้นิพจน์ทั่วไป ฉันตรวจสอบสิ่งนี้ในรายการไฟล์คู่บรรทัด ~ 1000 รายการที่ฉันต้องเปรียบเทียบ ด้วย-Fมันต้องใช้เวลา 0.031 วินาที (จริง) ในขณะที่ไม่ได้เอา 2.278 วินาที (จริง) เมื่อเปลี่ยนเส้นทางออก grep wc -lไป

การทดสอบเหล่านี้ยังรวมถึง-xสวิตช์ซึ่งเป็นส่วนที่จำเป็นของโซลูชันเพื่อให้มั่นใจในความถูกต้องโดยสมบูรณ์ในกรณีที่ file2 มีบรรทัดที่ตรงกับส่วนของ แต่ไม่ทั้งหมดหนึ่งบรรทัดหรือมากกว่าใน file1

ดังนั้นวิธีการแก้ปัญหาที่ไม่ต้องการอินพุตจะเรียงลำดับรวดเร็วยืดหยุ่น (case sensitive ฯลฯ ) คือ:

grep -F -x -v -f file2 file1

สิ่งนี้ใช้ไม่ได้กับ grep ทุกรุ่นตัวอย่างเช่นมันล้มเหลวใน macOS โดยที่บรรทัดในไฟล์ 1 จะแสดงเป็นไม่มีอยู่ในไฟล์ 2 แม้ว่าจะเป็นถ้าตรงกับอีกบรรทัดหนึ่งที่เป็นซับสตริงของมัน . อีกวิธีหนึ่งคุณสามารถติดตั้ง GNU grep บน macOSเพื่อใช้โซลูชันนี้


ใช่มันใช้งานได้ แต่ถึงแม้จะใช้-Fงานได้ไม่ดีนัก
Molomby

นี้ไม่ได้เป็นอย่างรวดเร็วว่าฉันรอ 5 นาทีสำหรับ 2 ไฟล์ ~ 500k เส้นก่อนจะให้ขึ้น
cahen

ที่จริงแล้ววิธีนี้ยังช้ากว่าวิธี comm เนื่องจากไฟล์นี้สามารถจัดการไฟล์ที่ไม่เรียงลำดับดังนั้นจึงถูกลากลงมาโดยไม่ทำการคัดกรอง comm ใช้ประโยชน์จากการเรียงลำดับ
workplaylifecycle

@workplaylifecycle คุณต้องเพิ่มเวลาสำหรับการเรียงลำดับซึ่งอาจเป็นคอขวดสำหรับขนาดใหญ่file2มาก
rwst

อย่างไรก็ตาม grep พร้อม-xตัวเลือกนั้นใช้หน่วยความจำมากกว่า ด้วยคำfile2ที่ประกอบด้วย 180M 6-10 bytes กระบวนการของฉันใช้Killedกับเครื่อง RAM 32GB ...
rwst

11

ความเร็วของการเรียงลำดับและต่างคืออะไร

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

1
ขอบคุณที่เตือนฉันเกี่ยวกับความจำเป็นในการจัดเรียงไฟล์ก่อนที่จะทำ diff sort + diff เร็วขึ้นมาก
Niels2000

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

11

หากคุณสั้นของ "เครื่องมือแฟนซี" เช่นในบางกระจาย Linux น้อยมีวิธีการแก้ปัญหามีเพียงcat, sortและuniq:

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

ทดสอบ:

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    

นี่คือยัง ค่อนข้างgrepรวดเร็วเมื่อเทียบกับ


1
หมายเหตุ - การใช้งานบางอย่างจะไม่รู้จัก--uniqueตัวเลือก คุณควรใช้ตัวเลือก POSIX ที่ได้มาตรฐานสำหรับสิ่งนี้:| uniq -u
AndrewF

1
ในตัวอย่าง "2" มาจากไหน
Niels2000

1
@ Niels2000 seq 1 1 7สร้างตัวเลขจาก 1 โดยเพิ่มขึ้น 1 ถึง 7 คือ 1 2 3 4 5 6 7 และนั่นคือ 2 ของคุณ!
Eirik Lygre

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

-tทำให้แน่ใจว่ามันเปรียบเทียบสายทั้งถ้าคุณมีพื้นที่ในบางส่วนของเส้น


ชอบcomm, joinต้องมีเส้นที่นำเข้าทั้งสองจะแยกบนสนามคุณกำลังดำเนินการเข้าร่วมในการดำเนินงาน
tripleee

4

คุณสามารถใช้ 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

ใช้combineจากmoreutilsแพคเกจ, ยูทิลิตี้ชุดที่สนับสนุนnot, and, or, xorการดำเนินงาน

combine file1 not file2

นั่นคือให้ฉันบรรทัดที่อยู่ใน file1 แต่ไม่ได้อยู่ใน file2

หรือให้บรรทัดกับฉันใน file1 ลบบรรทัดใน file2

หมายเหตุ: combineเรียงลำดับและค้นหาบรรทัดที่ไม่ซ้ำกันในทั้งสองไฟล์ก่อนที่จะดำเนินการใด ๆ แต่diffไม่ได้ ดังนั้นคุณอาจจะพบความแตกต่างระหว่างการส่งออกของและdiffcombine

ดังนั้นคุณจะพูดว่า

ค้นหาบรรทัดที่แตกต่างใน file1 และ file2 จากนั้นให้บรรทัดกับฉันใน file1 ลบบรรทัดใน file2

จากประสบการณ์ของฉันมันเร็วกว่าตัวเลือกอื่น ๆ


2

การใช้ fgrep หรือเพิ่มตัวเลือก -F เพื่อ grep สามารถช่วยได้ แต่สำหรับการคำนวณที่เร็วขึ้นคุณสามารถใช้ Awk ได้

คุณสามารถลองวิธีใดวิธีหนึ่งต่อไปนี้:

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


2
+1 นี่เป็นคำตอบเดียวที่ไม่ต้องการให้มีการเรียงลำดับอินพุต ในขณะที่เห็นได้ชัดว่า OP มีความสุขกับความต้องการนั้นมันเป็นข้อ จำกัด ที่ยอมรับไม่ได้ในสถานการณ์จริงมากมาย
tripleee

1

วิธีที่ฉันมักจะทำคือใช้--suppress-common-linesธงแม้ว่าจะใช้งานได้ก็ต่อเมื่อคุณทำในรูปแบบเคียงข้างกันเท่านั้น

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


0

ฉันพบว่าสำหรับฉันที่ใช้ปกติ if และ for loop statement ทำงานได้อย่างสมบูรณ์

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
ดูDontReadLinesWithFor นอกจากนี้โค้ดนี้จะทำงานได้ไม่ดีมากหากgrepผลลัพธ์ของคุณขยายไปถึงหลายคำหรือหากfile2เชลล์รายการใด ๆ ของคุณสามารถใช้งานได้แบบกลม
Charles Duffy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.