La meilleure façon de simuler "group by" de bash?
supposons que vous ayez un fichier qui contient 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 combien de fois il 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 le faire est:
cat ip_addresses |uniq |while read ip
do
echo -n $ip" "
grep -c $ip ip_addresses
done
mais il est vraiment loin d'être efficace.
comment résoudre ce problème utiliser plus efficacement bash?
(une chose à ajouter: je sais qu'il peut être résolu de perl ou awk, je suis intéressé par une meilleure solution en bash, pas dans ces langues.)
INFORMATIONS SUPPLÉMENTAIRES:
supposons que le fichier source est de 5 Go et que la machine qui exécute l'algorithme a 4 Go. Si le tri n'est pas une solution efficace, ni de la lecture du fichier plusieurs fois.
j'ai aimé la table de hachage comme solution - n'importe qui peuvent apporter des améliorations à cette solution?
INFORMATION SUPPLÉMENTAIRE #2:
certaines personnes ont demandé pourquoi je me donnerais la peine de le faire à bash quand il est beaucoup plus facile dans perl par exemple. La raison est que sur la machine, 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 le faites pas plaire. :- )
14 réponses
sort ip_addresses | uniq -c
cela affichera le compte en premier, mais à part cela, il devrait être exactement ce que vous voulez.
la méthode rapide et sale est la suivante:
cat ip_addresses | sort -n | uniq -c
si vous avez besoin d'utiliser les valeurs de bash, vous pouvez affecter l'ensemble de la commande à une variable de bash et ensuite boucler les résultats.
PS
si la commande sort est omise, vous n'obtiendrez pas les bons résultats car uniq ne regarde que des lignes identiques successives.
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 voulez pas utiliser sort, parce que la taille des données est plus grande que la taille de la mémoire principale de la machine. Ne sous-estimez pas la qualité d'implémentation de la commande unix sort. Le tri a été utilisé pour traiter de très grands volumes de données (pensez à la facturation originale D'AT & T données) sur des machines avec 128k (soit 131.072 octets) de mémoire (PDP-11). Quand sort rencontre plus de données qu'une limite prédéfinie (souvent accordé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 morceaux de données. Enfin, il effectue un tri de fusion sur ces fichiers intermédiaires. Cela permet à sort de travailler sur des données beaucoup plus grandes que la mémoire principale de la machine.
pour résumer plusieurs champs, en se basant sur un groupe de champs existants, utiliser l'exemple ci-dessous:$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[,]+=+ }END {for (i in arr) print i,arr[i]}' file
US|A|3000
US|B|3000
US|C|3000
UK|1|9000
cat ip_addresses | sort | uniq -c | sort -nr | awk '{print " " }'
cette commande vous donnerait la sortie désirée
il semble que vous devez soit utiliser une grande quantité de code pour simuler des hachures dans bash pour obtenir le comportement linéaire ou s'en tenir à la quadratique les versions superlinéaires.
Parmi ces versions, saua 's est la meilleure solution (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...
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
à la fin, tout ce que vous avez à faire est de parcourir tous les fichiers et d'imprimer les noms de fichiers et les numéros qu'ils contiennent. Alternativement, au lieu de garder un compte, vous pourriez ajouter un espace ou une nouvelle ligne à chaque fois au fichier, et à la fin il suffit de regarder la taille du fichier en octets.
Solution (group by like 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
je comprends que vous recherchez quelque chose à Bash, mais au cas où quelqu'un d'autre pourrait être à la recherche de quelque chose en Python, vous pourriez vouloir considérer ceci:
mySet = set()
for line in open("ip_address_file.txt"):
line = line.rstrip()
mySet.add(line)
comme valeurs dans l'ensemble sont uniques par défaut et Python est assez bon à ce truc, vous pourriez gagner quelque chose ici. Je n'ai pas testé le code, il est peut-être sur écoute, mais ça pourrait t'y mener. Et si vous voulez compter les occurrences, utiliser un dict au lieu d'un ensemble est facile à mettre en œuvre.
Edit: Je lis mal, alors j'ai mal répondu. Voici un extrait avec une dict qui compterait les occurences.
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 sont apparues en tant que leurs valeurs.
Pure bash (pas de fourche!)
il y a un moyen, en utilisant un bash fonction . Ce moyen est très rapide car il n'y a pas de fourchette!...
... Alors que le groupe 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
}
Note: les adresses IP sont converties en une valeur entière non signée de 32bits, utilisée comme index pour tableau . Cette utilisation simple bash tableaux , pas tableau associatif (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, le faire est beaucoup plus rapide que d'utiliser des fourches, jusqu'à environ 1'000 adresses, mais prendre environ 1 seconde entière quand je vais essayer de tri'n count 10'000 adresses.
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 travailler pour vous.
la plupart des autres solutions comptent les doublons. Si vous avez vraiment besoin de grouper des paires de valeurs clés, essayez ceci:
Voici mon exemple de données:
find . | xargs md5sum
fe4ab8e15432161f452e345ff30c68b0 a.txt
30c68b02161e15435ff52e34f4fe4ab8 b.txt
30c68b02161e15435ff52e34f4fe4ab8 c.txt
fe4ab8e15432161f452e345ff30c68b0 d.txt
fe4ab8e15432161f452e345ff30c68b0 e.txt
cette option permet d'afficher les paires de valeurs clés groupées par le total de contrôle md5.
cat table.txt | awk '{print }' | sort | uniq | xargs -i grep {} table.txt
30c68b02161e15435ff52e34f4fe4ab8 b.txt
30c68b02161e15435ff52e34f4fe4ab8 c.txt
fe4ab8e15432161f452e345ff30c68b0 a.txt
fe4ab8e15432161f452e345ff30c68b0 d.txt
fe4ab8e15432161f452e345ff30c68b0 e.txt
Tri peut être omis si l'ordre n'est pas significatif
uniq -c <source_file>
ou
echo "$list" | uniq -c
si la liste des sources est une variable