“Group by” ı bash'tan simüle etmenin en iyi yolu?


231

Her satırda bir adres olan IP adresleri içeren bir dosyanız olduğunu varsayalım:

10.0.10.1
10.0.10.1
10.0.10.3
10.0.10.2
10.0.10.1

Her IP adresi için dosyada kaç kez göründüğünü sayan bir kabuk betiğine ihtiyacınız vardır. Önceki giriş için aşağıdaki çıkışa ihtiyacınız vardır:

10.0.10.1 3
10.0.10.2 1
10.0.10.3 1

Bunu yapmanın bir yolu:

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

Ancak gerçekten verimli olmaktan çok uzak.

Bu sorunu bash kullanarak nasıl daha verimli çözersiniz?

(Eklenecek bir şey var: Perl veya awk'den çözülebileceğini biliyorum, bash'da daha iyi bir çözümle ilgileniyorum, bu dillerde değil.)

İLAVE BİLGİ:

Kaynak dosyanın 5 GB ve algoritmayı çalıştıran makinede 4 GB olduğunu varsayalım. Yani sıralama etkili bir çözüm değildir, dosyayı bir kereden fazla okumaz.

Hashhtable benzeri çözümü beğendim - herkes bu çözümde iyileştirmeler yapabilir mi?

EK BİLGİ # 2:

Bazı insanlar, örneğin perl'de çok daha kolay olduğunda neden bash yaparken rahatsız edeyim diye sordu. Nedeni makinede bu perl benim için mevcut değildi yapmak zorundaydı. Alıştığım aletlerin çoğu olmadan özel olarak yapılmış bir linux makinesiydi. Ve bence bu ilginç bir problemdi.

Bu yüzden lütfen soruyu suçlamayın, hoşunuza gitmiyorsa görmezden gelin. :-)


Bence bash iş için yanlış araç. Perl muhtemelen daha iyi bir çözüm olacaktır.
Francois Wolmarans

Yanıtlar:


412
sort ip_addresses | uniq -c

Bu, önce sayıyı yazdırır, ancak bunun dışında tam olarak istediğiniz şey olmalıdır.


71
daha sonra en yüksekten en düşük sayıya doğru azalan düzende sıralamak için "sort -nr" komutunu kullanabilirsiniz. iesort ip_addresses | uniq -c | sort -nr
Brad Parks

15
Ve sort ip_addresses | uniq -c | sort -nr | awk '{ print $2, $1 }'ilk sütundaki ip adresini almak ve ikincisinde saymak için.
Raghu Dodda

sıralama bölümü için bir tweak daha:sort -nr -k1,1
Andrzej Martyna

50

Hızlı ve kirli yöntem aşağıdaki gibidir:

cat ip_addresses | sort -n | uniq -c

Bash içindeki değerleri kullanmanız gerekiyorsa, tüm komutu bir bash değişkenine atayabilir ve sonra sonuçlar arasında geçiş yapabilirsiniz.

PS

Sort komutu atlanırsa, uniq yalnızca birbirini izleyen özdeş satırlara baktığından doğru sonuçları elde edemezsiniz.


Verimlilik açısından çok benzer, hala ikinci dereceden davranışınız var
Vinko Vrsalovic

İkinci dereceden anlam O (n ^ 2) ?? Bu kesinlikle sıralama algoritmasına bağlı olacaktır, böyle bir bogo-sıralama kullanmak pek mümkün değildir.
paxdiablo

En iyi durumda, iki geçişten daha kötü olan O (n log (n)) olurdu (önemsiz bir karma tabanlı uygulama ile elde ettiğiniz şey budur). İkinci dereceden yerine 'süper doğrusal' demeliydim.
Vinko Vrsalovic

Ve OP'nin verimliliği arttırmak için sorduğu şey hala aynı
sınırda

11
uuoc, kedi yararsız kullanımı

22

bir grup mevcut alanı temel alarak birden çok alanı toplamak için aşağıdaki örneği kullanın: (gereksinimlerinize göre 1 $, 2 $, 3 $, 4 $ tutarını değiştirin)

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

2
+1, çünkü yalnızca sayım gerektiğinde ne yapılacağını gösterir
user829755

1
1 çünkü sortve uniqsayımlarını yapmak için en kolay, ancak / toplamı alanları değerlerin hesaplanması için gerektiğinde yardım yok. awk dizi sözdizimi çok güçlü ve burada gruplandırmanın anahtarı. Teşekkürler!
odony

1
awk en emin bir şey daha, dikkat printişlevi 32 bite 64 bit tamsayılar downscale görünüyor kullanmak isteyebilirsiniz ^ 31 2 aşan int değerler için, bu yüzden printfbirlikte %.0fformatında yerine printorada
odony

1
Sayı ekleme yerine dize birleştirmesiyle "gruplama ölçütü" araması yapan kullanıcılar arr[$1,$2]+=$3+$4, başarılı bir şekilde örneğin 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) `ile değiştirilir.
Stéphane Gourichon

20

Kanonik çözüm, başka bir katılımcı tarafından belirtilen çözümdür:

sort | uniq -c

Perl veya awk ile yazılabileceklerden daha kısa ve özlüdür.

Sıralamayı kullanmak istemediğinizi yazarsınız, çünkü verilerin boyutu makinenin ana bellek boyutundan daha büyüktür. Unix sıralama komutunun uygulama kalitesini hafife almayın. Sort, 128k (131.072 bayt) belleğe (PDP-11) sahip makinelerde çok büyük miktarda veriyi (orijinal AT&T faturalandırma verilerini düşünün) işlemek için kullanıldı. Sıralama, önceden ayarlanmış bir sınırdan (genellikle makinenin ana belleğinin boyutuna yakın olarak ayarlanır) daha fazla veriyle karşılaştığında, ana bellekte okuduğu verileri sıralar ve geçici bir dosyaya yazar. Daha sonra eylemi bir sonraki veri parçalarıyla tekrarlar. Son olarak, bu ara dosyalar üzerinde bir birleştirme sıralaması gerçekleştirir. Bu, sıralamanın makinenin ana belleğinden çok daha büyük veriler üzerinde çalışmasına izin verir.


Hala bir karma sayımından daha kötü, değil mi? Veriler belleğe sığarsa, sıralama yönteminin hangi sıralama algoritmasını kullandığını biliyor musunuz? Sayısal veri durumunda değişiklik gösterir mi (-n seçeneği)?
Vinko Vrsalovic

Sıralamanın (1) nasıl uygulandığına bağlıdır. Hem GNU sıralaması (Linux dağıtımlarında kullanılır) hem de BSD sıralaması en uygun algoritmayı kullanmak için büyük uzunluklara gider.
Diomidis Spinellis

9
cat ip_addresses | sort | uniq -c | sort -nr | awk '{print $2 " " $1}'

bu komut size istenen çıktıyı verir


4

Görünüşe göre ya lineer davranış elde etmek için bash'ta karmaları simüle etmek için büyük miktarda kod kullanmanız ya da ikinci dereceden süper lineer versiyonlara bağlı kalmanız gerekiyor.

Bu sürümler arasında saua'nın çözümü en iyisidir (ve en basitidir):

sort -n ip_addresses.txt | uniq -c

Http://unix.derkeiler.com/Newsgroups/comp.unix.shell/2005-11/0118.html buldum . Ama cehennem kadar çirkin ...


Katılıyorum. Bu şimdiye kadarki en iyi çözümdür ve benzer çözümler perl ve awk'de mümkündür. Herkes bash'da daha temiz bir uygulama sağlayabilir mi?
Zizzencs

Bildiğim kadarıyla hayır. $ İp (@ips) {$ hash {$ ip} = $ hash {$ ip} + 1; } ve ardından yalnızca anahtarları ve değerleri yazdırın.
Vinko Vrsalovic

4

Çözüm (mysql gibi gruplandır)

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

Sonuç

3249  googleplus
4211 linkedin
5212 xing
7928 facebook

3

Muhtemelen bir karma tablo olarak dosya sisteminin kendisini kullanabilirsiniz. Sahte kod aşağıdaki gibi:

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

Sonunda, tek yapmanız gereken tüm dosyaları taramak ve içindeki dosya adlarını ve numaralarını yazdırmak. Alternatif olarak, bir sayı tutmak yerine, dosyaya her seferinde boşluk veya yeni satır ekleyebilirsiniz ve sonunda dosya boyutuna bayt cinsinden bakabilirsiniz.


3

Ben awk ilişkisel dizi de bu durumda kullanışlı hissediyorum

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

Buraya posta yoluyla bir grup


Yepp, büyük awk çözümü, ama awk bunu yaptığım makinede mevcut değildi.
Zizzencs

1

Diğer çözümlerin çoğu kopyaları sayar. Anahtar / değer çiftlerini gerçekten gruplamanız gerekiyorsa şunu deneyin:

İşte benim örnek veriler:

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

Bu, md5 sağlama toplamına göre gruplandırılmış anahtar / değer çiftlerini yazdırır.

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

1

Saf (çatal yok!)

Bir yol var, bir işlev . Çatal olmadığı için bu yol çok hızlı! ...

... demet iken ip adresleri kalmak küçük !

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
}

Not: IP adresleri, dizi için dizin olarak kullanılan 32 bit işaretsiz tam sayı değerine dönüştürülür . Bu kullanımı basit bash dizileri , ilişkisel dizi (hangi daha pahalı)!

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

Ev sahibimde , bunu yapmak yaklaşık 1.000 adrese kadar çatal kullanmaktan çok daha hızlıdır, ancak 10.000 adresi sıralamaya çalıştığımda yaklaşık 1 saniye sürüyor .


0

Ben böyle yapardım:

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

ama uniq sizin için işe yarayabilir.


Orijinal yazıda söylediğim gibi perl bir seçenek değil. Perl'de kolay olduğunu biliyorum, bununla sorun yok :-)
Zizzencs

0

Bash'de bir şey aradığınızı anlıyorum, ancak başka birinin Python'da bir şey araması durumunda, bunu düşünmek isteyebilirsiniz:

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

Setteki değerler varsayılan olarak benzersiz olduğundan ve Python bu konuda oldukça iyi olduğundan, burada bir şeyler kazanabilirsiniz. Kodu test etmedim, bu yüzden hata olabilir, ama bu sizi oraya götürebilir. Ve olayları saymak istiyorsanız, bir küme yerine bir diktenin kullanılması kolaydır.

Edit: Ben berbat bir okuyucum, bu yüzden yanlış cevap verdim. Burada, olayları sayacak bir dikteye sahip bir pasaj.

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

Dictionary mydict artık anahtar olarak benzersiz IP'lerin bir listesini ve değerleri olarak gerçekleşme sayısını tutar.


bu hiçbir şey saymaz. puan tutan bir dikte gerekir.

Hamuru. Soruyu kötü okumak, üzgünüm. Başlangıçta her IP adresinin kaç kez meydana geldiğini saklamak için bir diksiyon kullanma hakkında küçük bir şeyim vardı, ancak kaldırdım, çünkü, soruyu çok iyi okumadım. * düzgün uyanmaya çalışır
wzzrd

2
OP'nin tam olarak sorduğu şeyle itertools.groupby()birleşen bir kombinasyon var sorted().
jfs

Bu mevcut değildi python için harika bir çözüm :-)
Zizzencs

-8

Sipariş önemli değilse sıralama atlanabilir

uniq -c <source_file>

veya

echo "$list" | uniq -c

kaynak listesi değişkense


1
Daha açıklamak için, uniq man sayfasından: Not: 'uniq', bitişik olmadıkça tekrarlanan satırları algılamaz. İlk olarak girişi sıralamak veya 'uniq' olmadan 'sort -u' kullanmak isteyebilirsiniz.
converter42
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.