Bagaimana cara menghapus garis duplikat di dalam file teks?

126

File teks saya yang besar (hingga 2 GiB) berisi sekitar 100 duplikat tepat dari setiap baris di dalamnya (tidak berguna dalam kasus saya, karena file tersebut adalah tabel data seperti CSV).

Yang saya butuhkan adalah menghapus semua pengulangan sementara (lebih disukai, tetapi ini dapat dikorbankan untuk meningkatkan kinerja yang signifikan) mempertahankan urutan urutan asli. Hasilnya, setiap baris harus unik. Jika ada 100 baris yang sama (biasanya duplikat tersebar di seluruh file dan tidak akan menjadi tetangga) hanya akan ada satu dari jenis yang tersisa.

Saya telah menulis sebuah program di Scala (anggap saja Java jika Anda tidak tahu tentang Scala) untuk mengimplementasikannya. Tapi mungkin ada alat-alat asli C-ditulis lebih cepat dapat melakukan ini lebih cepat?

UPDATE: awk '!seen[$0]++' filenamesolusinya tampaknya bekerja dengan baik bagi saya selama file-file itu dekat 2 GiB atau lebih kecil tapi sekarang karena saya harus membersihkan file 8 GiB itu tidak berfungsi lagi. Tampaknya mengambil infinity pada Mac dengan 4 GiB RAM dan 64-bit Windows 7 PC dengan 4 GiB RAM dan 6 GiB swap hanya kehabisan memori. Dan saya tidak merasa antusias untuk mencobanya di Linux dengan RAM 4 GiB mengingat pengalaman ini.

Ivan
sumber
ini akan menghancurkan pemesanan Anda, tetapi, apakah Anda sudah mencoba menyortir -u, saya tidak tahu bagaimana atau apakah itu dapat berjalan pada file besar
0x7c0
5
C seringkali tidak jauh lebih cepat daripada Java, dan jika Anda menjalankannya (secara berurutan) sekarang, ada kemungkinan ia akan selesai sebelum Anda mendapatkan jawaban di sini, mengimplementasikannya, dan selesai berjalan; rusak, sort -umungkin akan lebih cepat.
Kevin

Jawaban:

215

Sebuah awksolusi yang terlihat pada #bash (Freenode):

awk '!seen[$0]++' filename
enzotib
sumber
1
Baru saja mencoba ini pada file 2G dan butuh tiga menit di notebook saya. Tidak buruk. Saya juga mencoba nama file uniq | awk '! seen [$ 0] ++', tapi itu tidak lebih cepat.
mgjk
Ini secara mengejutkan lebih cepat daripada versi yang lebih verbose awkmenggunakan pencarian 2 array (ditampilkan sebagai penjelasan yang diperluas dalam jawaban Gilles): 0m36.132s vs 0m49.958s .. untuk 50 juta baris .. Saya pikir kemacetan akan menjadi I / O, tetapi pencarian array ekstra adalah ... 1 juta elemen dalam array tampaknya membuat penyok yang cukup signifikan ...
Peter.O
Tapi bagaimana itu dibandingkan dengan mengurutkan -u ....?
HashWizard
1
@HashWizard: perintah ini tidak menyortir, tetapi menghilangkan setiap kemunculan berikutnya dari baris yang sama
enzotib
1
@ MaxWilliams ya, itu berfungsi adalah mereka didistribusikan secara acak.
setholopolus
47

Ada metode sederhana (yang tidak bisa dikatakan jelas) menggunakan utilitas standar yang tidak memerlukan memori besar kecuali untuk menjalankan sort, yang dalam kebanyakan implementasi memiliki optimasi khusus untuk file besar (algoritma pengurutan eksternal yang baik). Keuntungan dari metode ini adalah bahwa ia hanya melewati semua baris di dalam utilitas tujuan khusus, tidak pernah di dalam bahasa yang ditafsirkan.

<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

Jika semua baris dimulai dengan karakter non-spasi, Anda dapat membuang beberapa opsi:

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

Untuk duplikasi dalam jumlah besar, metode yang hanya membutuhkan penyimpanan satu salinan dari setiap baris dalam memori akan berkinerja lebih baik. Dengan beberapa overhead interpretasi, ada naskah awk yang sangat ringkas untuk itu (sudah diposting oleh enzotib ):

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

Kurang ringkas:, !seen[$0] {print} {seen[$0] += 1}yaitu mencetak baris saat ini jika belum terlihat, kemudian menambah seenpenghitung untuk baris ini (variabel yang tidak diinisialisasi atau elemen array memiliki nilai numerik 0).

Untuk antrean panjang, Anda dapat menghemat memori dengan hanya menyimpan checksum yang tidak spoofable (misalnya intisari kriptografi) dari masing-masing baris. Misalnya, menggunakan SHA-1, Anda hanya perlu 20 byte plus overhead konstan per baris. Tetapi menghitung cerna agak lambat; metode ini hanya akan menang jika Anda memiliki CPU yang cepat (terutama yang memiliki akselerator perangkat keras untuk menghitung pencernaan) dan tidak banyak memori relatif terhadap ukuran file dan garis yang cukup panjang. Tidak ada utilitas dasar yang memungkinkan Anda menghitung checksum untuk setiap baris; Anda harus menanggung overhead interpretasi Perl / Python / Ruby / ... atau menulis program yang dikompilasi khusus.

<input perl -MDigest::MD5 -ne '$seen{Digest::MD5::md5($_)}++ or print' >output
Gilles
sumber
@Gilles Berdasarkan pada penjelasan Anda tentang awk '!seen[$0]++', apakah ini berarti bahwa jika awk melihat 2 baris duplikat, itu akan menjaga yang pertama dan mengabaikan semua yang berikutnya? (Atau ini akan mempertahankan yang terakhir?)
user779159
1
@ user779159 Ini membuat yang pertama: setiap jalur input baik dicetak segera (kejadian pertama) atau tidak sama sekali (kejadian berulang).
Gilles
Tapi bagaimana itu dibandingkan dengan mengurutkan -u ...?
HashWizard
@HashWizard A dataran sort -umengubah urutan. Jawaban saya menunjukkan solusi yang menjaga urutan (urutan kejadian pertama, tepatnya).
Gilles
@Gilles akankah Anda mengatakan bahwa ini lebih cepat daripada mengurutkan -u untuk file besar (10G) dengan duplikat 50%?
HashWizard
25
sort -u big-csv-file.csv > duplicates-removed.csv

Perhatikan bahwa file output akan diurutkan.

Vladislavs Dovgalecs
sumber
1
Tidak secepat awkperintah dalam jawaban lain, tetapi secara konsep sederhana!
Johann
@ Johann Saya melakukan ini cukup sering pada file dengan ratusan ribu (bahkan jutaan) string yang diakhiri dengan baris baru. Saya mendapatkan hasil yang cukup cepat untuk percobaan yang saya lakukan. Ini bisa lebih penting jika digunakan dalam skrip yang dijalankan berulang kali, penghematan waktu bisa sangat besar.
Vladislavs Dovgalecs
1
Gunakan sort -uuntuk menghapus duplikat selama penyortiran, bukan setelah. (Dan menghemat bandwidth memori) memipangnya ke program lain). Ini hanya lebih baik daripada awkversi jika Anda ingin output Anda diurutkan juga. (OP tentang pertanyaan ini ingin agar pemesanan aslinya dipertahankan , jadi ini adalah jawaban yang bagus untuk kasus penggunaan yang sedikit berbeda.)
Peter Cordes
Butuh sekitar satu menit, bagi saya, untuk 5,5 juta file baris (total 1,8 GB). Cemerlang.
Max Williams
18

Dengan asumsi Anda mampu menyimpan sebanyak file de-duplikat dalam memori (jika data Anda memang digandakan oleh faktor 100, yang seharusnya sekitar 20MiB + overhead), Anda dapat melakukan ini dengan sangat mudah dengan Perl.

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

Ini menjaga pesanan juga.

Anda dapat mengekstraksi jumlah kemunculan setiap baris dari %duphash jika diinginkan, sebagai bonus gratis tambahan.

Jika Anda mau awk, ini juga harus dilakukan (logika yang sama dengan versi perl, urutan yang sama, data yang sama yang dikumpulkan dalam dupvariabel):

$ awk '{if (++dup[$0] == 1) print $0;}' input_file > output_file
Tikar
sumber
@Mat ini terlalu bagus, saya baru saja akan menyeruput file itu, lol ;-).
Nikhil Mulley
Sekarang menunggu @ManAtWork untuk weavery sihir sed and awk-nya juga :-)
Nikhil Mulley
luar biasa lagi untuk tip awk :-)
Nikhil Mulley
1
Apakah mungkin untuk mengubah skrip perl untuk hanya menghapus duplikat baris yang berdekatan?
dumbledad
2
@dumbledad: uniqmelakukan itu sendiri
Mat
3

Karena tidak ada jawaban lain yang disediakan dukungan inplace, berikut adalah salah satu:

gawk -i inplace '!a[$0]++' file
Jan Chren - rindeal
sumber
Apakah ini mempertahankan pesanan? Ngomong-ngomong, ini tidak berhasil untukku. Versi saya adalah:GNU Awk 4.0.2
Leonid
1
@Leonid ya, benar. Ini mencetak kemunculan pertama dari setiap garis unik. Dukungan inplace pertama kali diperkenalkan dalam versi 4.1, yang dirilis pada 2013.
Jan Chren - rindeal
3

Anda dapat menggunakan uniq http://www.computerhope.com/unix/uuniq.htm

uniq melaporkan atau memfilter baris berulang dalam file.

Mahmoud Zalt
sumber
Saat memberikan jawaban, lebih baik memberi penjelasan mengapa MENGAPA jawaban Anda adalah jawabannya . Jadi, bagaimana perbedaan jawaban ini dari beberapa jawaban sebelumnya?
Stephen Rauch
1
Dari halaman manual uniq: Catatan: 'uniq' does not detect repeated lines unless they are adjacent. Jadi, Anda harus mengurutkannya terlebih dahulu dan kehilangan urutan baris yang bukan duplikat.
Vindolin
2

Python One liners:

python -c "import sys; lines = sys.stdin.readlines(); print ''.join(sorted(set(lines)))" < InputFile
Rahul Patil
sumber
ini menyebabkan seluruh file akan disedot ke dalam memori dan mungkin tidak cocok untuk masalah OP. Juga tidak dijamin untuk mempertahankan pesanan
iruvar
Terima kasih atas sarannya, saya baru saja belajar python .. baru mencoba ini untuk tujuan belajar .. :)
Rahul Patil
Berikut adalah versi Python 2.7 yang bukan satu-liner tetapi (secara ringkas) mengembalikan garis-garis unik yang menjaga ketertiban tanpa memuat seluruh file ke dalam memori atau membuat string raksasa tunggal untuk memberi makan untuk dicetak
iruvar
Terima kasih @ 1_CR Saya punya sesuatu yang dipelajari hari ini :)OrderedDict
Rahul Patil
0

Tidak ada jawaban di sini yang berfungsi untuk saya di Mac, jadi saya menulis skrip python sederhana yang berfungsi untuk saya. Saya mengabaikan spasi putih terkemuka / tertinggal dan juga tidak peduli tentang konsumsi memori.

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)

Simpan di atas ke unique.py dan jalankan seperti ini:

python unique.py inputfile.txt outputfile.txt
Jared
sumber
-1

Dengan bash 4, solusi pure-bash yang memanfaatkan array asosiatif dapat digunakan. Berikut ini sebuah contoh

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
iruvar
sumber
2
Jangan gunakan readloop untuk memproses file teks besar. bash harus membaca satu byte per saat untuk menghindari overshooting baris baru. Bash juga tidak terlalu cepat dalam pemrosesan teks secara umum dibandingkan dengan awk. Jika Anda menggunakan ini, read -raakan menghindari makan backslash di input Anda. Juga, jangan lupa unset llist setelah loop, jika Anda menempatkan ini dalam fungsi shell atau menggunakannya secara interaktif.
Peter Cordes
2
@PeterCordes, atau Anda bisa saja mereferensikan ini :-)
iruvar