web-dev-qa-db-fra.com

Meilleure façon de simuler "groupe par" à partir de bash?

Supposons que vous ayez un fichier contenant des adresses IP, une adresse par 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 faire est:

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

Cependant, c'est vraiment loin d'être efficace.

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

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

INFORMATION ADDITIONNELLE:

Supposons que le fichier source soit de 5 Go et que la machine exécutant l'algorithme en ait 4. Donc, le tri n'est pas une solution efficace, ni la lecture du fichier plus d'une fois.

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

INFORMATIONS COMPLEMENTAIRES N ° 2:

Certaines personnes ont demandé pourquoi je prendrais la peine de le faire en bash alors que c’est beaucoup plus facile, par exemple Perl. La raison en est que sur la machine que je devais faire, 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. :-)

210
Zizzencs
sort ip_addresses | uniq -c

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

374
Joachim Sauer

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 de tri est omise, vous n'obtiendrez pas les bons résultats, car uniq ne considère que les lignes identiques successives.

45
Francois Wolmarans

pour récapituler plusieurs champs, en fonction d'un groupe de champs existants, utilisez l'exemple ci-dessous: (remplacez 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
19
Anonymous

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é de la mise en œuvre de la commande de tri Unix. Sort a été utilisé pour gérer de très gros volumes de données (par exemple les données de facturation d’AT & T d’origine) sur des machines disposant de 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 proche 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 morceaux de données. Enfin, il effectue un tri par fusion sur ces fichiers intermédiaires. Cela permet de trier des données plusieurs fois plus volumineuses que la mémoire principale de la machine.

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

cette commande vous donnerait la sortie désirée

9
zjor

Il semble que vous deviez soit utiliser une grande quantité de code pour simuler des hachages en bash pour obtenir un comportement linéaire, soit vous en tenir à la quadratique versions superlinéaires.

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 ...

4
Vinko Vrsalovic

Solution (groupe 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
4
kairouan2020

Je me sens que le tableau associatif awk est également utile dans ce cas

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

Un groupe par courrier ici

3
Jadu Saikia

Vous pouvez probablement utiliser le système de fichiers lui-même en tant que 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’imprimer leur nom et leur numéro. Alternativement, au lieu de garder un nombre, vous pouvez ajouter un espace ou une nouvelle ligne au fichier, et finalement ne regarder que la taille du fichier en octets.

3
PolyThinker

Pure bash (pas de fourche!)

Il existe un moyen, en utilisant une bash fonction. Ce chemin est très rapide car il n'y a pas de fourche! ...

... Alors que beaucoup de adresses IP rester petit!

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 array. Cette utilisation simple tableaux bash, pas 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 forks, jusqu'à environ 1 000 adresses, mais prenez environ 1 seconde lorsque je vais essayer de sort'n compte 10'000 adresses .

1
F. Hauri

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.

0
nicerobot

La plupart des autres solutions comptent des doublons. Si vous avez vraiment besoin de grouper des paires clé-valeur, essayez ceci:

Voici mes exemples de données:

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

Cela imprimera les paires clé/valeur 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
0
Aron Curzon

Je comprends que vous cherchiez quelque chose dans Bash, mais si quelqu'un d'autre cherchait quelque chose en Python, vous pouvez envisager ceci:

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

Comme les valeurs de cet ensemble sont uniques par défaut et que Python est très bon dans ce domaine, vous pourriez gagner quelque chose ici. Je n’ai pas testé le code; Et si vous voulez compter les occurrences, utiliser un dict au lieu d’un ensemble est facile à implémenter.

Edit: Je suis un lecteur moche, alors j'ai mal répondu. Voici un extrait avec un dict qui compterait les événements.

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'adresses IP uniques sous forme de clés et leur nombre de fois.

0
wzzrd