La meilleure façon de simuler «group by» à partir de bash?

231

Supposons que vous ayez un fichier contenant des adresses IP, une adresse dans chaque ligne:

10.0.10.1
10.0.10.1
10.0.10.3
10.0.10.2
10.0.10.1

Vous avez besoin d'un script shell qui compte pour chaque adresse IP le nombre de fois qu'elle apparaît dans le fichier. Pour l'entrée précédente, vous avez besoin de la sortie suivante:

10.0.10.1 3
10.0.10.2 1
10.0.10.3 1

Une façon de procéder est:

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

Mais c'est vraiment loin d'être efficace.

Comment résoudriez-vous ce problème plus efficacement en utilisant bash?

(Une chose à ajouter: je sais que cela peut être résolu depuis perl ou awk, je suis intéressé par une meilleure solution en bash, pas dans ces langues.)

INFORMATION ADDITIONNELLE:

Supposons que le fichier source mesure 5 Go et que la machine exécutant l'algorithme dispose de 4 Go. Le tri n'est donc pas une solution efficace, pas plus que la lecture du fichier plus d'une fois.

J'ai aimé la solution de type table de hachage - n'importe qui peut apporter des améliorations à cette solution?

INFORMATION SUPPLÉMENTAIRE # 2:

Certaines personnes ont demandé pourquoi je prendrais la peine de le faire en bash alors que c'est beaucoup plus facile, par exemple en perl. La raison en est que sur la machine que je devais faire, ce Perl n'était pas disponible pour moi. C'était une machine Linux construite sur mesure sans la plupart des outils auxquels je suis habitué. Et je pense que c'était un problème intéressant.

Alors s'il vous plaît, ne blâmez pas la question, ignorez-la si vous ne l'aimez pas. :-)

Zizzencs
la source
Je pense que bash n'est pas le bon outil pour le travail. Perl sera probablement une meilleure solution.
Francois Wolmarans

Réponses:

412
sort ip_addresses | uniq -c

Cela imprimera le décompte en premier, mais à part cela, il devrait être exactement ce que vous voulez.

Joachim Sauer
la source
71
que vous pouvez ensuite diriger vers "sort -nr" pour les trier par ordre décroissant, du plus haut au plus bas. iesort ip_addresses | uniq -c | sort -nr
Brad Parks
15
Et sort ip_addresses | uniq -c | sort -nr | awk '{ print $2, $1 }'pour obtenir l'adresse IP dans la première colonne et compter dans la seconde.
Raghu Dodda
un ajustement supplémentaire pour la partie de tri:sort -nr -k1,1
Andrzej Martyna
50

La méthode rapide et sale est la suivante:

cat ip_addresses | sort -n | uniq -c

Si vous devez utiliser les valeurs dans bash, vous pouvez affecter la commande entière à une variable bash, puis parcourir les résultats.

PS

Si la commande sort est omise, vous n'obtiendrez pas les résultats corrects car uniq ne regarde que les lignes identiques successives.

François Wolmarans
la source
C'est très similaire sur le plan de l'efficacité, vous avez toujours un comportement quadratique
Vinko Vrsalovic
Signification quadratique O (n ^ 2) ?? Cela dépendrait sûrement de l'algorithme de tri, il est peu probable d'utiliser un tel bogo-tri.
paxdiablo
Eh bien, dans le meilleur des cas, ce serait O (n log (n)), ce qui est pire que deux passes (c'est ce que vous obtenez avec une implémentation basée sur un hachage trivial). J'aurais dû dire «superlinéaire» au lieu de quadratique.
Vinko Vrsalovic
Et c'est toujours dans la même limite que ce que le PO a demandé pour améliorer son efficacité ...
Vinko Vrsalovic
11
uuoc, utilisation inutile de chat
22

pour résumer plusieurs champs, sur la base d'un groupe de champs existants, utilisez l'exemple ci-dessous: (remplacez les $ 1, $ 2, $ 3, $ 4 selon vos besoins)

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
Anonyme
la source
2
+1 car il montre quoi faire lorsque non seulement le décompte est nécessaire
user829755
1
+1 parce que sortet uniqsont plus faciles à faire des comptages, mais ne vous aident pas lorsque vous devez calculer / additionner des valeurs de champs. La syntaxe de tableau awk est très puissante et clé pour le regroupement ici. Merci!
odony
1
encore une chose, faites attention à ce que la printfonction d'awk semble réduire les entiers de 64 bits à 32 bits, donc pour les valeurs int supérieures à 2 ^ 31, vous voudrez peut-être utiliser printfle %.0fformat au lieu de printcela
odony
1
Les personnes recherchant "group by" avec une concaténation de chaînes au lieu d'un ajout de nombre remplaceraient arr[$1,$2]+=$3+$4par exemple 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) `avec succès.
Stéphane Gourichon
20

La solution canonique est celle mentionnée par un autre répondant:

sort | uniq -c

Il est plus court et plus concis que ce qui peut être écrit en Perl ou en awk.

Vous écrivez que vous ne souhaitez pas utiliser le tri, car la taille des données est supérieure à la taille de la mémoire principale de la machine. Ne sous-estimez pas la qualité d'implémentation de la commande de tri Unix. Le tri a été utilisé pour gérer de très gros volumes de données (pensez aux données de facturation originales d'AT & T) sur des machines avec 128 Ko (soit 131 072 octets) de mémoire (PDP-11). Lorsque le tri rencontre plus de données qu'une limite prédéfinie (souvent réglée près de la taille de la mémoire principale de la machine), il trie les données qu'il a lues dans la mémoire principale et les écrit dans un fichier temporaire. Il répète ensuite l'action avec les prochains blocs de données. Enfin, il effectue un tri par fusion sur ces fichiers intermédiaires. Cela permet au tri de fonctionner sur des données beaucoup plus volumineuses que la mémoire principale de la machine.

Diomidis Spinellis
la source
Eh bien, c'est encore pire qu'un compte de hachage, non? Savez-vous quel algorithme de tri utilise le tri si les données tiennent en mémoire? Cela varie-t-il dans le cas des données numériques (option -n)?
Vinko Vrsalovic
Cela dépend de la façon dont sort (1) est implémenté. Le tri GNU (utilisé sur les distributions Linux) et le tri BSD vont de grandes longueurs pour utiliser l'algorithme le plus approprié.
Diomidis Spinellis
9
cat ip_addresses | sort | uniq -c | sort -nr | awk '{print $2 " " $1}'

cette commande vous donnerait la sortie souhaitée

zjor
la source
4

Il semble que vous devez utiliser une grande quantité de code pour simuler les hachages dans bash pour obtenir un comportement linéaire ou vous en tenir aux versions super linéaires quadratiques .

Parmi ces versions, la solution de saua est la meilleure (et la plus simple):

sort -n ip_addresses.txt | uniq -c

J'ai trouvé http://unix.derkeiler.com/Newsgroups/comp.unix.shell/2005-11/0118.html . Mais c'est moche comme l'enfer ...

Vinko Vrsalovic
la source
Je suis d'accord. C'est la meilleure solution jusqu'à présent et des solutions similaires sont possibles en perl et awk. Quelqu'un peut-il fournir une implémentation plus propre dans bash?
Zizzencs
Pas que je sache de. Vous pouvez obtenir de meilleures implémentations dans les langues prenant en charge les hachages, où vous le faites pour mon $ ip (@ips) {$ hash {$ ip} = $ hash {$ ip} + 1; }, puis imprimez simplement les clés et les valeurs.
Vinko Vrsalovic
4

Solution (regrouper par comme mysql)

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

Résultat

3249  googleplus
4211 linkedin
5212 xing
7928 facebook
kairouan2020
la source
3

Vous pouvez probablement utiliser le système de fichiers lui-même comme table de hachage. Pseudo-code comme suit:

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

En fin de compte, tout ce que vous avez à faire est de parcourir tous les fichiers et d'y imprimer les noms et les numéros de fichier. Alternativement, au lieu de garder un décompte, vous pouvez ajouter un espace ou une nouvelle ligne à chaque fois au fichier, et à la fin, il suffit de regarder la taille du fichier en octets.

PolyThinker
la source
3

Je pense que le tableau associatif awk est également pratique dans ce cas

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

Un groupe par courrier ici

SriniV
la source
Oui, excellente solution awk, mais awk n'était tout simplement pas disponible sur la machine sur laquelle je faisais cela.
Zizzencs
1

La plupart des autres solutions comptent les doublons. Si vous avez vraiment besoin de regrouper des paires de valeurs clés, essayez ceci:

Voici mes données d'exemple:

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

Cela affichera les paires de valeurs clés regroupées par la somme de contrôle 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
la source
1

Pur (pas de fourchette!)

Il y a un moyen, en utilisant un fonction . Cette façon est très rapide car il n'y a pas de fourche! ...

... Alors que des tas d' adresses IP restent petites !

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
}

Remarque: les adresses IP sont converties en une valeur entière non signée de 32 bits, utilisée comme index pour le tableau . Cela utilise des tableaux bash simples , pas un tableau associatif (ce qui est plus cher)!

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

Sur mon hôte, cela est beaucoup plus rapide que d'utiliser des fourches, jusqu'à environ 1'000 adresses, mais cela prend environ 1 seconde entière lorsque j'essaierai de trier et de compter 10'000 adresses.

F. Hauri
la source
0

Je l'aurais fait comme ça:

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

mais uniq pourrait fonctionner pour vous.

nicerobot
la source
Comme je l'ai dit dans le post original, perl n'est pas une option. Je sais que c'est facile en perl, pas de problème avec ça :-)
Zizzencs
0

Je comprends que vous cherchez quelque chose dans Bash, mais au cas où quelqu'un d'autre chercherait quelque chose en Python, vous pourriez envisager ceci:

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

Comme les valeurs de l'ensemble sont uniques par défaut et que Python est plutôt bon dans ce domaine, vous pourriez gagner quelque chose ici. Je n'ai pas testé le code, il pourrait donc être buggé, mais cela pourrait vous y amener. Et si vous voulez compter les occurrences, l'utilisation d'un dict au lieu d'un ensemble est facile à mettre en œuvre.

Edit: je suis un lecteur moche, donc j'ai répondu mal. Voici un extrait avec un dict qui compterait les occurrences.

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

Le dictionnaire mydict contient maintenant une liste d'IP uniques en tant que clés et le nombre de fois où elles se sont produites comme valeurs.

wzzrd
la source
cela ne compte rien. vous avez besoin d'un dict qui garde le score.
Doh. Mauvaise lecture de la question, désolé. À l'origine, j'avais un petit quelque chose à propos de l'utilisation d'un dict pour stocker le nombre de fois où chaque adresse IP s'est produite, mais je l'ai supprimée, car, eh bien, je n'ai pas très bien lu la question. * essaie de se réveiller correctement
wzzrd
2
Il y a un itertools.groupby()qui, combiné avec, sorted()fait exactement ce que demande OP.
jfs
C'est une excellente solution en python, qui n'était pas disponible pour cela :-)
Zizzencs
-8

Le tri peut être omis si la commande n'est pas significative

uniq -c <source_file>

ou

echo "$list" | uniq -c

si la liste source est une variable

Sudden Def
la source
1
Pour clarifier davantage, à partir de la page de manuel uniq: Remarque: 'uniq' ne détecte pas les lignes répétées à moins qu'elles ne soient adjacentes. Vous pouvez d'abord trier l'entrée ou utiliser 'sort -u' sans 'uniq'.
converter42