Cara terbaik untuk mensimulasikan "grup oleh" dari bash?

231

Misalkan Anda memiliki file yang berisi alamat IP, satu alamat di setiap baris:

10.0.10.1
10.0.10.1
10.0.10.3
10.0.10.2
10.0.10.1

Anda memerlukan skrip shell yang menghitung untuk setiap alamat IP berapa kali muncul dalam file. Untuk input sebelumnya, Anda memerlukan output berikut:

10.0.10.1 3
10.0.10.2 1
10.0.10.3 1

Salah satu cara untuk melakukan ini adalah:

cat ip_addresses |uniq |while read ip
do
    echo -n $ip" "
    grep -c $ip ip_addresses
done

Namun itu jauh dari efisien.

Bagaimana Anda memecahkan masalah ini secara lebih efisien menggunakan bash?

(Satu hal untuk ditambahkan: Saya tahu ini bisa diselesaikan dari perl atau awk, saya tertarik pada solusi yang lebih baik dalam bash, bukan dalam bahasa itu.)

INFORMASI TAMBAHAN:

Misalkan file sumber 5GB dan mesin yang menjalankan algoritma 4GB. Jadi sort bukanlah solusi yang efisien, juga tidak membaca file lebih dari sekali.

Saya menyukai solusi seperti hashtable - siapa pun dapat memberikan peningkatan pada solusi itu?

INFO TAMBAHAN # 2:

Beberapa orang bertanya mengapa saya repot-repot melakukannya di bash ketika itu jauh lebih mudah di misalnya perl. Alasannya adalah bahwa pada mesin saya harus melakukan perl ini tidak tersedia untuk saya. Itu adalah mesin linux yang dibuat khusus tanpa sebagian besar alat yang biasa saya gunakan. Dan saya pikir itu masalah yang menarik.

Jadi tolong, jangan salahkan pertanyaannya, abaikan saja jika Anda tidak menyukainya. :-)

Zizzencs
sumber
Saya pikir bash adalah alat yang salah untuk pekerjaan itu. Perl mungkin akan menjadi solusi yang lebih baik.
Francois Wolmarans

Jawaban:

412
sort ip_addresses | uniq -c

Ini akan mencetak hitungan pertama, tetapi selain itu harus persis apa yang Anda inginkan.

Joachim Sauer
sumber
71
yang kemudian Anda dapat pipa ke "sort -nr" untuk diurutkan dalam urutan menurun, dari tertinggi ke terendah. yaitusort ip_addresses | uniq -c | sort -nr
Brad Parks
15
Dan sort ip_addresses | uniq -c | sort -nr | awk '{ print $2, $1 }'untuk mendapatkan alamat ip di kolom pertama dan hitung di kolom kedua.
Raghu Dodda
satu lagi tweak untuk bagian semacam:sort -nr -k1,1
Andrzej Martyna
50

Metode cepat dan kotor adalah sebagai berikut:

cat ip_addresses | sort -n | uniq -c

Jika Anda perlu menggunakan nilai-nilai dalam bash Anda dapat menetapkan seluruh perintah ke variabel bash dan kemudian mengulangi hasilnya.

PS

Jika perintah sortir dihilangkan, Anda tidak akan mendapatkan hasil yang benar karena uniq hanya melihat garis identik yang berurutan.

Francois Wolmarans
sumber
Ini sangat mirip efisiensi-bijaksana, Anda masih memiliki perilaku kuadratik
Vinko Vrsalovic
Arti kuadratik O (n ^ 2) ?? Itu akan tergantung pada algoritma pengurutan tentunya, itu tidak mungkin untuk menggunakan semacam bogo seperti itu.
paxdiablo
Nah, dalam kasus terbaik itu akan menjadi O (n log (n)), yang lebih buruk dari dua melewati (yang adalah apa yang Anda dapatkan dengan implementasi berbasis hash sepele). Saya seharusnya mengatakan 'superlinear' alih-alih kuadrat.
Vinko Vrsalovic
Dan itu masih dalam batas yang sama dengan apa yang diminta OP untuk meningkatkan efisiensi ...
Vinko Vrsalovic
11
uuoc, penggunaan kucing yang tidak berguna
22

untuk menjumlahkan beberapa bidang, berdasarkan sekelompok bidang yang ada, gunakan contoh di bawah ini: (ganti $ 1, $ 2, $ 3, $ 4 sesuai dengan kebutuhan Anda)

cat file

US|A|1000|2000
US|B|1000|2000
US|C|1000|2000
UK|1|1000|2000
UK|1|1000|2000
UK|1|1000|2000

awk 'BEGIN { FS=OFS=SUBSEP="|"}{arr[$1,$2]+=$3+$4 }END {for (i in arr) print i,arr[i]}' file

US|A|3000
US|B|3000
US|C|3000
UK|1|9000
Anonim
sumber
2
Memberi +1 karena ini menunjukkan apa yang harus dilakukan ketika penghitungan tidak hanya diperlukan
user829755
1
Memberi +1 karena sortdan uniqpaling mudah untuk melakukan penghitungan, tetapi tidak membantu ketika Anda perlu menghitung / menjumlahkan nilai bidang. Sintaks array awk sangat kuat dan kunci untuk pengelompokan di sini. Terima kasih!
odony
1
satu hal lagi, hati-hati bahwa awk ini printfungsi tampaknya downscale 64 bit bilangan bulat 32 bit, sehingga nilai-nilai int melebihi 2 ^ 31 Anda mungkin ingin menggunakan printfdengan %.0fformat yang bukan printada
odony
1
Orang-orang yang mencari "grup oleh" dengan penggabungan string alih-alih penambahan angka akan menggantikan arr[$1,$2]+=$3+$4dengan misalnya arr[$1,$2]=(arr[$1,$2] $3 "," $4). I needed this to provide a grouped-by-package list of files (two columns only) and used: arr [$ 1] = (arr [$ 1] $ 2) `dengan sukses.
Stéphane Gourichon
20

Solusi kanonik adalah yang disebutkan oleh responden lain:

sort | uniq -c

Ini lebih pendek dan lebih ringkas daripada apa yang dapat ditulis dalam Perl atau awk.

Anda menulis bahwa Anda tidak ingin menggunakan pengurutan, karena ukuran data lebih besar dari ukuran memori utama mesin. Jangan meremehkan kualitas implementasi dari perintah sort Unix. Sortir digunakan untuk menangani volume data yang sangat besar (pikirkan data penagihan AT&T asli) pada mesin dengan 128k (yaitu 131.072 byte) memori (PDP-11). Ketika sortir menemukan lebih banyak data daripada batas yang telah ditentukan (sering disetel mendekati ukuran memori utama mesin) itu mengurutkan data yang telah dibaca dalam memori utama dan menulisnya ke dalam file sementara. Kemudian mengulangi tindakan dengan potongan data berikutnya. Akhirnya, ia melakukan semacam penggabungan pada file-file perantara. Ini memungkinkan sortir bekerja pada data beberapa kali lebih besar dari memori utama mesin.

Diomidis Spinellis
sumber
Yah, ini masih lebih buruk dari hitungan hash, bukan? Apakah Anda tahu algoritma pengurutan apa yang digunakan jika data cocok dalam memori? Apakah ini bervariasi dalam case data numerik (opsi -n)?
Vinko Vrsalovic
Itu tergantung pada bagaimana sort (1) diimplementasikan. Baik jenis GNU (digunakan pada distribusi Linux) dan jenis BSD berusaha keras untuk menggunakan algoritma yang paling tepat.
Diomidis Spinellis
9
cat ip_addresses | sort | uniq -c | sort -nr | awk '{print $2 " " $1}'

perintah ini akan memberi Anda output yang diinginkan

zjor
sumber
4

Tampaknya Anda harus menggunakan sejumlah besar kode untuk mensimulasikan hash dalam bash untuk mendapatkan perilaku linier atau tetap berpegang pada kuadratik versi superlinear .

Di antara versi tersebut, solusi saua adalah yang terbaik (dan paling sederhana):

sort -n ip_addresses.txt | uniq -c

Saya menemukan http://unix.derkeiler.com/Newsgroups/comp.unix.shell/2005-11/0118.html . Tapi itu jelek sekali ...

Vinko Vrsalovic
sumber
Saya setuju. Ini adalah solusi terbaik sejauh ini dan solusi serupa dimungkinkan dalam perl dan awk. Adakah yang bisa memberikan implementasi yang lebih bersih di bash?
Zizzencs
Tidak yang saya tahu. Anda bisa mendapatkan implementasi yang lebih baik dalam bahasa yang mendukung hash, di mana Anda melakukannya untuk $ ip (@ips) {$ hash {$ ip} = $ hash {$ ip} + 1; } lalu cetak saja kunci dan nilainya.
Vinko Vrsalovic
4

Solusi (dikelompokkan berdasarkan like mysql)

grep -ioh "facebook\|xing\|linkedin\|googleplus" access-log.txt | sort | uniq -c | sort -n

Hasil

3249  googleplus
4211 linkedin
5212 xing
7928 facebook
kairouan2020
sumber
3

Anda mungkin dapat menggunakan sistem file itu sendiri sebagai tabel hash. Kode semu sebagai berikut:

for every entry in the ip address file; do
  let addr denote the ip address;

  if file "addr" does not exist; then
    create file "addr";
    write a number "0" in the file;
  else 
    read the number from "addr";
    increase the number by 1 and write it back;
  fi
done

Pada akhirnya, yang perlu Anda lakukan adalah menelusuri semua file dan mencetak nama dan nomor file di dalamnya. Sebagai alternatif, alih-alih menyimpan hitungan, Anda dapat menambahkan spasi atau baris baru setiap kali ke file, dan pada akhirnya hanya melihat ukuran file dalam byte.

PolyThinker
sumber
3

Saya merasa array asosiatif awk juga berguna dalam kasus ini

$ awk '{count[$1]++}END{for(j in count) print j,count[j]}' ips.txt

Grup lewat pos di sini

SriniV
sumber
Yepp, solusi awk yang hebat, tetapi awk tidak tersedia pada mesin yang saya gunakan.
Zizzencs
1

Sebagian besar solusi lain menghitung duplikat. Jika Anda benar-benar perlu mengelompokkan pasangan nilai kunci, coba ini:

Berikut ini contoh data saya:

find . | xargs md5sum
fe4ab8e15432161f452e345ff30c68b0 a.txt
30c68b02161e15435ff52e34f4fe4ab8 b.txt
30c68b02161e15435ff52e34f4fe4ab8 c.txt
fe4ab8e15432161f452e345ff30c68b0 d.txt
fe4ab8e15432161f452e345ff30c68b0 e.txt

Ini akan mencetak pasangan nilai kunci yang dikelompokkan oleh checksum MD5.

cat table.txt | awk '{print $1}' | sort | uniq  | xargs -i grep {} table.txt
30c68b02161e15435ff52e34f4fe4ab8 b.txt
30c68b02161e15435ff52e34f4fe4ab8 c.txt
fe4ab8e15432161f452e345ff30c68b0 a.txt
fe4ab8e15432161f452e345ff30c68b0 d.txt
fe4ab8e15432161f452e345ff30c68b0 e.txt
Aron Curzon
sumber
1

Murni (tanpa garpu!)

Ada caranya, menggunakan a fungsi . Cara ini sangat cepat karena tidak ada garpu! ...

... Sementara sekelompok alamat ip tetap kecil !

countIp () { 
    local -a _ips=(); local _a
    while IFS=. read -a _a ;do
        ((_ips[_a<<24|${_a[1]}<<16|${_a[2]}<<8|${_a[3]}]++))
    done
    for _a in ${!_ips[@]} ;do
        printf "%.16s %4d\n" \
          $(($_a>>24)).$(($_a>>16&255)).$(($_a>>8&255)).$(($_a&255)) ${_ips[_a]}
    done
}

Catatan: Alamat IP dikonversi menjadi nilai integer 32 bit yang tidak ditandai, digunakan sebagai indeks untuk array . Ini menggunakan array bash sederhana , bukan array asosiatif (yang lebih mahal)!

time countIp < ip_addresses 
10.0.10.1    3
10.0.10.2    1
10.0.10.3    1
real    0m0.001s
user    0m0.004s
sys     0m0.000s

time sort ip_addresses | uniq -c
      3 10.0.10.1
      1 10.0.10.2
      1 10.0.10.3
real    0m0.010s
user    0m0.000s
sys     0m0.000s

Di host saya, melakukannya jauh lebih cepat daripada menggunakan garpu, hingga kira-kira 1'000 alamat, tetapi ambil kira-kira 1 seluruh detik ketika saya akan mencoba mengurutkan'n menghitung 10'000 alamat.

F. Hauri
sumber
0

Saya akan melakukannya seperti ini:

perl -e 'while (<>) {chop; $h{$_}++;} for $k (keys %h) {print "$k $h{$k}\n";}' ip_addresses

tetapi uniq mungkin bekerja untuk Anda.

nicerobot
sumber
Seperti yang saya katakan di perl posting asli bukan merupakan pilihan. Saya tahu itu mudah di perl, tidak ada masalah dengan itu :-)
Zizzencs
0

Saya mengerti Anda sedang mencari sesuatu di Bash, tetapi kalau-kalau ada orang lain yang mencari sesuatu dengan Python, Anda mungkin ingin mempertimbangkan ini:

mySet = set()
for line in open("ip_address_file.txt"):
     line = line.rstrip()
     mySet.add(line)

Karena nilai dalam himpunan unik secara default dan Python cukup bagus dalam hal ini, Anda mungkin memenangkan sesuatu di sini. Saya belum menguji kodenya, jadi mungkin disadap, tetapi ini mungkin akan membawa Anda ke sana. Dan jika Anda ingin menghitung kejadian, menggunakan dict bukan set mudah untuk diimplementasikan.

Sunting: Saya seorang pembaca yang buruk, jadi saya menjawab salah. Berikut cuplikan dengan dikt yang akan menghitung kejadian.

mydict = {}
for line in open("ip_address_file.txt"):
    line = line.rstrip()
    if line in mydict:
        mydict[line] += 1
    else:
        mydict[line] = 1

Kamus mydict sekarang menyimpan daftar kunci unik IP sebagai dan berapa kali mereka muncul sebagai nilainya.

wzzrd
sumber
ini tidak masuk hitungan. Anda memerlukan dikt yang membuat skor.
Doh. Pembacaan pertanyaan yang buruk, maaf. Saya awalnya punya sedikit sesuatu tentang menggunakan dict untuk menyimpan jumlah setiap alamat IP terjadi, tetapi menghapusnya, karena, yah, saya tidak membaca pertanyaan dengan sangat baik. * mencoba untuk bangun dengan benar
wzzrd
2
Ada itertools.groupby()yang dikombinasikan dengan sorted()melakukan persis apa yang diminta OP.
jfs
Ini adalah solusi hebat dengan python, yang tidak tersedia untuk ini :-)
Zizzencs
-8

Sortir dapat dihilangkan jika pesanan tidak signifikan

uniq -c <source_file>

atau

echo "$list" | uniq -c

jika daftar sumber adalah variabel

Def Mendadak
sumber
1
Untuk lebih memperjelas, dari halaman manual uniq: Catatan: 'uniq' tidak mendeteksi garis yang berulang kecuali jika berdekatan. Anda mungkin ingin mengurutkan input terlebih dahulu, atau menggunakan 'sort -u' tanpa 'uniq'.
converter42