วิธีลบบรรทัดที่ซ้ำกันในไฟล์ข้อความ?


126

ไฟล์ข้อความขนาดใหญ่ (มากถึง 2 GiB) ของฉันมีข้อมูลที่ซ้ำกันประมาณ 100 รายการทุกบรรทัดในนั้น (ไร้ประโยชน์ในกรณีของฉันเนื่องจากไฟล์เป็นตารางข้อมูลที่เหมือน CSV)

สิ่งที่ฉันต้องการคือการลบการทำซ้ำทั้งหมดในขณะที่ (โดยเฉพาะอย่างยิ่ง แต่สิ่งนี้สามารถเสียสละเพื่อเพิ่มประสิทธิภาพที่สำคัญ) การรักษาลำดับเดิม ในผลลัพธ์แต่ละบรรทัดจะต้องไม่ซ้ำกัน หากมี 100 เส้นเท่ากัน (โดยปกติรายการที่ซ้ำกันจะกระจายไปทั่วไฟล์และจะไม่เป็นเพื่อนบ้าน) จะต้องมีเพียงหนึ่งในประเภทที่เหลือ

ฉันได้เขียนโปรแกรมใน Scala (พิจารณาเป็น Java ถ้าคุณไม่ทราบเกี่ยวกับ Scala) เพื่อดำเนินการนี้ แต่บางทีมีเครื่องมือดั้งเดิมที่เขียนด้วยภาษา C เร็วกว่าสามารถทำสิ่งนี้ได้เร็วขึ้น?

อัปเดต: awk '!seen[$0]++' filenameดูเหมือนว่าโซลูชันจะทำงานได้ดีสำหรับฉันตราบใดที่ไฟล์อยู่ใกล้กับ 2 GiB หรือเล็กกว่า แต่ตอนนี้เมื่อฉันต้องล้างไฟล์ 8 GiB มันไม่ทำงานอีกต่อไป ดูเหมือนว่าจะไม่มีที่สิ้นสุดบน Mac ที่มี 4 GiB RAM และ 64-bit Windows 7 PC ที่มี 4 GiB RAM และ 6 GiB swap เพียงหน่วยความจำหมด และฉันไม่รู้สึกกระตือรือร้นกับการทดลองใช้บน Linux ด้วย 4 GiB RAM เนื่องจากประสบการณ์นี้


นี้จะทำลายการสั่งซื้อของคุณ แต่คุณได้ลองเรียงลำดับ -u ฉันไม่ทราบว่าจะสามารถทำงานกับไฟล์จำนวนมากได้อย่างไร
0x7c0

5
C มักจะไม่เร็วกว่า Java อย่างมีนัยสำคัญและหากคุณใช้งาน (ตามลำดับ) ในตอนนี้มีโอกาสพอสมควรที่จะเสร็จสิ้นก่อนที่คุณจะได้รับคำตอบที่นี่ใช้มันและมันจะทำงานเสร็จ ออกคำสั่งsort -uอาจจะเร็วขึ้น
Kevin

คำตอบ:


215

awkวิธีการแก้ปัญหาที่เห็นใน #bash (Freenode):

awk '!seen[$0]++' filename

1
เพิ่งลองใช้ไฟล์ 2G และใช้เวลาสามนาทีบนโน้ตบุ๊คของฉัน ไม่เลว. ฉันยังลองใช้ชื่อไฟล์ uniq | awk '! เห็น [$ 0] ++' แต่มันก็ไม่เร็วกว่านี้
mgjk

นี่น่าแปลกใจที่เร็วกว่าawkเวอร์ชั่นverbose ที่มากขึ้นโดยใช้การค้นหา 2 อาร์เรย์ (แสดงเป็นคำอธิบายเพิ่มเติมในคำตอบของ Gilles): 0m36.132sเทียบกับ0m49.958s 0m49.958s .. สำหรับ 50 ล้านบรรทัดฉันคิดว่าคอขวดจะเป็น I / O แต่การค้นหาอาร์เรย์พิเศษคือ ... 1000000 องค์ประกอบในอาร์เรย์ดูเหมือนว่าจะทำให้บุ๋มค่อนข้างสำคัญ ...
Peter.O

แต่มันเปรียบเทียบกับ sort -u อย่างไร?
HashWizard

1
@HashWizard: คำสั่งนี้ไม่ได้เรียงลำดับ แต่กำจัดทุกการเกิดขึ้นของบรรทัดเดียวกันทุกครั้ง
enzotib

1
@ MaxWilliams ใช่มันทำงานเป็นแบบกระจาย
setholopolus

47

มีวิธีง่าย ๆ (ซึ่งไม่ได้บอกชัดเจน) โดยใช้ยูทิลิตี้มาตรฐานซึ่งไม่ต้องการหน่วยความจำขนาดใหญ่ยกเว้นการรันsortซึ่งในการใช้งานส่วนใหญ่มีการปรับแต่งเฉพาะสำหรับไฟล์ขนาดใหญ่ (อัลกอริทึมการเรียงลำดับภายนอกที่ดี) ข้อได้เปรียบของวิธีนี้คือการวนซ้ำทุกบรรทัดในยูทิลิตีวัตถุประสงค์พิเศษไม่เคยอยู่ในภาษาที่ตีความ

<input nl -b a -s : |           # number the lines
sort -t : -k 2 -u |             # sort and uniquify ignoring the line numbers
sort -t : -k 1n |               # sort according to the line numbers
cut -d : -f 2- >output          # remove the line numbers

หากทุกบรรทัดเริ่มต้นด้วยอักขระที่ไม่ใช่ช่องว่างคุณสามารถแจกจ่ายด้วยตัวเลือกบางอย่าง:

<input nl | sort -k 2 -u | sort -k 1n | cut -f 2- >output

สำหรับการทำสำเนาจำนวนมากวิธีการที่ต้องเก็บสำเนาเดียวของแต่ละบรรทัดในหน่วยความจำจะทำงานได้ดีขึ้น ด้วยค่าใช้จ่ายในการตีความมีสคริปต์ awk ที่กระชับมากสำหรับเรื่องนั้น ( โพสต์แล้วโดย enzotib ):

<input awk '!seen[$0]++'

รัดกุมน้อยกว่า: !seen[$0] {print} {seen[$0] += 1}เช่นพิมพ์บรรทัดปัจจุบันหากยังไม่เห็นจากนั้นจึงเพิ่มตัวseenนับสำหรับบรรทัดนี้ (ตัวแปรที่ไม่กำหนดค่าเริ่มต้นหรือองค์ประกอบอาร์เรย์มีค่าตัวเลข 0)

สำหรับสายยาวคุณสามารถบันทึกหน่วยความจำได้โดยเก็บเฉพาะค่าเช็คซัมที่ไม่สามารถทำการปลอมแปลงได้ (เช่นตัวย่อการเข้ารหัส) ของแต่ละบรรทัด ตัวอย่างเช่นการใช้ SHA-1 คุณต้องการเพียง 20 ไบต์บวกค่าใช้จ่ายคงที่ต่อบรรทัด แต่การคำนวณย่อยค่อนข้างช้า วิธีนี้จะชนะก็ต่อเมื่อคุณมี CPU ที่รวดเร็ว (โดยเฉพาะอย่างยิ่งหนึ่งที่มีตัวเร่งความเร็วฮาร์ดแวร์เพื่อคำนวณการย่อย) และหน่วยความจำไม่มากเมื่อเทียบกับขนาดของไฟล์และบรรทัดที่มีความยาวเพียงพอ ไม่มียูทิลิตี้พื้นฐานช่วยให้คุณคำนวณการตรวจสอบสำหรับแต่ละบรรทัด คุณต้องแบกรับค่าใช้จ่ายการตีความของ Perl / Python / Ruby / ... หรือเขียนโปรแกรมรวบรวมเฉพาะ

<input perl -MDigest::MD5 -ne '$seen{Digest::MD5::md5($_)}++ or print' >output

@Gilles ตามคำอธิบายของawk '!seen[$0]++'คุณหมายความว่าถ้า awk เห็น 2 บรรทัดที่ซ้ำกันมันจะเก็บบรรทัดแรกเสมอและละเว้นบรรทัดถัดไปทั้งหมดหรือไม่ (หรือมันจะเก็บไว้ล่าสุด)
user779159

1
@ user779159 มันเก็บแรก: แต่ละบรรทัดอินพุตถูกพิมพ์ทันที (เกิดครั้งแรก) หรือไม่เลย (เกิดซ้ำ)
Gilles

แต่จะเปรียบเทียบกับ sort -u อย่างไร ...
HashWizard

@HashWizard การsort -uเปลี่ยนแปลงคำสั่งธรรมดา คำตอบของฉันแสดงวิธีแก้ปัญหาที่รักษาลำดับ (ลำดับของการเกิดครั้งแรกให้แม่นยำ)
Gilles

@Gilles คุณจะบอกว่ามันเร็วกว่า sort -u สำหรับไฟล์ขนาดใหญ่ (10G) ที่ซ้ำกัน 50%?
HashWizard

25
sort -u big-csv-file.csv > duplicates-removed.csv

โปรดทราบว่าไฟล์เอาต์พุตจะถูกเรียงลำดับ


1
ไม่เร็วเท่ากับawkคำสั่งในคำตอบอื่น ๆ แต่ง่ายในเชิงแนวคิด!
โยฮันมี. ค.

@Johann ฉันกำลังทำสิ่งนี้บ่อยครั้งในไฟล์ที่มีสตริงขึ้นบรรทัดใหม่จำนวนสั้น ๆ เป็นแสน ๆ (แม้แต่ล้าน) ฉันได้ผลลัพธ์ค่อนข้างเร็วสำหรับการทดลองที่ฉันทำ มันอาจมีความสำคัญมากกว่านี้หากใช้ในสคริปต์ที่ทำงานซ้ำแล้วซ้ำอีกการประหยัดเวลาเป็นสิ่งสำคัญ
Vladislavs Dovgalecs

1
ใช้sort -uเพื่อลบรายการที่ซ้ำกันในระหว่างการเรียงลำดับแทนที่จะดีกว่า (และบันทึกแบนด์วิดธ์ของหน่วยความจำ) ไพพ์ไปยังโปรแกรมอื่น) นี่จะดีกว่าawkรุ่นหากคุณต้องการเรียงลำดับผลลัพธ์ของคุณด้วย (OP ในคำถามนี้ต้องการคำสั่งดั้งเดิมที่เก็บรักษาไว้ดังนั้นนี่เป็นคำตอบที่ดีสำหรับกรณีการใช้งานที่แตกต่างออกไปเล็กน้อย)
Peter Cordes

ใช้เวลาประมาณหนึ่งนาทีสำหรับฉันสำหรับไฟล์ 5.5 ล้านบรรทัด (รวม 1.8 GB) สุกใส
Max Williams Max

18

สมมติว่าคุณสามารถเก็บได้มากเท่ากับไฟล์ที่ถูกทำซ้ำในหน่วยความจำ (ถ้าข้อมูลของคุณถูกทำซ้ำโดยปัจจัยที่ 100 นั่นควรจะอยู่ที่ประมาณ 20MiB + ค่าใช้จ่าย) คุณสามารถทำได้อย่างง่ายดายด้วย Perl

$ perl -ne 'print unless $dup{$_}++;' input_file > output_file

สิ่งนี้จะรักษาลำดับไว้เช่นกัน

คุณสามารถแยกจำนวนการเกิดขึ้นของแต่ละบรรทัดออกจาก%dupแฮชได้หากคุณต้องการเป็นโบนัสฟรีเพิ่ม

หากคุณต้องการawkสิ่งนี้ก็ควรทำเช่นเดียวกัน (ตรรกะเดียวกันกับรุ่น Perl, การสั่งซื้อเดียวกัน, ข้อมูลเดียวกันที่รวบรวมในdupตัวแปร):

$ awk '{if (++dup[$0] == 1) print $0;}' input_file > output_file

นี่เป็น @Mat ที่ดีเกินไปฉันกำลังจะตบไฟล์ lol ;-)
Nikhil Mulley

ตอนนี้กำลังรอ @ManAtWork สำหรับความอ่อนแอและความเวทนาเวทมนตร์ของเขาด้วย :-)
Nikhil Mulley

สุดยอดอีกครั้งสำหรับเคล็ดลับเล็ก ๆ น้อย ๆ :-)
Nikhil Mulley

1
เป็นไปได้หรือไม่ที่จะเปลี่ยนสคริปต์ Perl เพื่อลบบรรทัดที่ติดกันซ้ำเท่านั้น
dumbledad

2
@dumbledad: uniqทำทั้งหมดด้วยตัวเอง
Mat

3

เนื่องจากไม่มีคำตอบอื่นใดที่ให้การสนับสนุนแบบ inplace นี่คือข้อหนึ่ง:

gawk -i inplace '!a[$0]++' file

สิ่งนี้จะรักษาคำสั่งซื้อหรือไม่ โดยวิธีการนี้ไม่ได้ผลสำหรับฉัน เวอร์ชันของฉันคือ:GNU Awk 4.0.2
Leonid

1
@ Leonid ใช่มันเป็นเช่นนั้น มันพิมพ์เหตุการณ์แรกของบรรทัดที่ไม่ซ้ำกัน การสนับสนุนแบบ inplace ได้รับการเปิดตัวครั้งแรกในเวอร์ชัน 4.1 ซึ่งเปิดตัวในปี 2013
Jan Chren - rindeal

3

คุณสามารถใช้uniq http://www.computerhope.com/unix/uuniq.htm

uniq รายงานหรือกรองบรรทัดที่ซ้ำกันในไฟล์


เมื่อให้คำตอบควรให้คำอธิบายว่าทำไมคำตอบของคุณจึงเป็นคำตอบ ดังนั้นคำตอบนี้แตกต่างจากคำตอบก่อนหน้านี้หลายคำตอบอย่างไร
Stephen Rauch

1
จากหน้า man uniq: หมายเหตุ: 'uniq' does not detect repeated lines unless they are adjacent. ดังนั้นคุณต้องเรียงลำดับมันก่อนและคลายลำดับของบรรทัดที่ไม่ซ้ำกัน
Vindolin

2

Python One liners:

python -c "import sys; lines = sys.stdin.readlines(); print ''.join(sorted(set(lines)))" < InputFile

สิ่งนี้ทำให้ไฟล์ทั้งหมดถูก slurped ในหน่วยความจำและอาจไม่เหมาะสมกับปัญหาของ OP ไม่รับประกันว่าจะรักษาความสงบเรียบร้อย
iruvar

ขอบคุณสำหรับคำแนะนำฉันเพิ่งเรียนรู้ python .. ลองทำสิ่งนี้เพื่อจุดประสงค์ในการเรียนรู้ .. :)
Rahul Patil

นี่คือเวอร์ชั่น Python 2.7 ที่ไม่ใช่ซับใน แต่ (รวบรัด) ส่งคืนบรรทัดที่ไม่ซ้ำกันเพื่อรักษาลำดับโดยไม่ต้องโหลดไฟล์ทั้งหมดลงในหน่วยความจำหรือสร้างสตริงขนาดมหึมาตัวเดียวเพื่อป้อนให้พิมพ์
iruvar

ขอบคุณ @ 1_CR ฉันได้เรียนรู้อะไรบางอย่างในวันนี้ :)OrderedDict
Rahul Patil

0

ไม่มีคำตอบที่นี่สำหรับฉันบน Mac ของฉันดังนั้นฉันจึงเขียนสคริปต์ง่ายๆที่เหมาะกับฉัน ฉันไม่สนใจช่องว่างชั้นนำ / ต่อท้ายและไม่สนใจปริมาณการใช้หน่วยความจำ

import sys

inputfile = sys.argv[1]
outputfile = sys.argv[2]

with open(inputfile) as f:
    content = f.readlines()

content = [x.strip() for x in content]

my_list = list(set(content))

with open(outputfile, 'w') as output:
    for item in my_list:
        output.write("%s\n" % item)

บันทึกด้านบนเพื่อ unique.py และเรียกใช้ดังนี้:

python unique.py inputfile.txt outputfile.txt

-1

ด้วย bash 4 สามารถใช้โซลูชันทุบตีบริสุทธิ์ซึ่งใช้ประโยชน์จากอาเรย์แบบเชื่อมโยงได้ นี่คือตัวอย่าง

unset llist; declare -A llist;
while read -r line; do
if [[ ${llist[$line]} ]]; then
  continue
else 
  printf '%s\n' "$line"
  llist[$line]="x"
fi
done < file.txt

2
อย่าใช้readลูปเพื่อประมวลผลไฟล์ข้อความขนาดใหญ่ bash ต้องอ่านทีละไบต์เพื่อหลีกเลี่ยงการขึ้นบรรทัดใหม่ Bash นั้นยังไม่เร็วมากนักเมื่อทำการประมวลผลข้อความโดยทั่วไปเมื่อเทียบกับ awk หากคุณใช้สิ่งนี้read -raจะหลีกเลี่ยงการรับประทานแบ็กสแลชในอินพุตของคุณ นอกจากนี้อย่าลืมunset llist หลังจากลูปถ้าคุณใส่สิ่งนี้ในฟังก์ชั่นเปลือกหรือใช้มันในเชิงโต้ตอบ
Peter Cordes

2
@PeterCordes หรือคุณอาจอ้างถึงสิ่งนี้ :-)
iruvar
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.