Question Quelle est la manière la plus rapide de compter le nombre de chaque caractère dans un fichier?


Je veux compter le nombre de N et le caractère "-" dans un fichier, ou chaque lettre si nécessaire, y a-t-il une commande Unix rapide pour faire cela?


120


origine


Compter les bases dans les brins d'ADN? - Indrek
J'aime cette question, autant d'approches et d'outils différents utilisés pour résoudre le même problème. - Journeyman Geek♦
Heh, c'est du golf à code limite - Earlz
si somone est intéressé par la version de windows powershell: [System.IO.File]::ReadAllText("C:\yourfile.txt").ToCharArray() | Group-Object $_ | Sort Count -Descending - Guillaume86
Ok je pense que j'ai trouvé la façon pure de PS: Get-Content "C:\eula.3082.txt" | % { $_.ToCharArray() } | Group-Object | Sort Count -Descending - Guillaume86


Réponses:


Si vous voulez une vraie vitesse:

echo 'int cache[256],x,y;char buf[4096],letters[]="tacgn-"; int main(){while((x=read(0,buf,sizeof buf))>0)for(y=0;y<x;y++)cache[(unsigned char)buf[y]]++;for(x=0;x<sizeof letters-1;x++)printf("%c: %d\n",letters[x],cache[letters[x]]);}' | gcc -w -xc -; ./a.out < file; rm a.out;

Est un pseudo-one-liner incroyablement rapide.

Un simple test montre que sur mon processeur Core i7 870 @ 2,93 GHz il compte à peine plus de 600 Mo / s:

$ du -h bigdna 
1.1G    bigdna

time ./a.out < bigdna 
t: 178977308
a: 178958411
c: 178958823
g: 178947772
n: 178959673
-: 178939837

real    0m1.718s
user    0m1.539s
sys     0m0.171s

Contrairement aux solutions de tri, celle-ci fonctionne en mémoire (4K) constante, ce qui est très utile si votre fichier est bien plus volumineux que votre ram.

Et bien sûr, avec un peu de graisse de coude, nous pouvons nous débarrasser de 0,7 seconde:

echo 'int cache[256],x,buf[4096],*bp,*ep;char letters[]="tacgn-"; int main(){while((ep=buf+(read(0,buf,sizeof buf)/sizeof(int)))>buf)for(bp=buf;bp<ep;bp++){cache[(*bp)&0xff]++;cache[(*bp>>8)&0xff]++;cache[(*bp>>16)&0xff]++;cache[(*bp>>24)&0xff]++;}for(x=0;x<sizeof letters-1;x++)printf("%c: %d\n",letters[x],cache[letters[x]]);}' | gcc -O2 -xc -; ./a.out < file; rm a.out;

Filets à peine supérieur à 1,1 Go / s:

real    0m0.943s
user    0m0.798s
sys     0m0.134s

A titre de comparaison, j'ai testé certaines des autres solutions sur cette page qui semblaient avoir une certaine promesse de vitesse.

le sed/awk solution a fait un effort vaillant, mais est mort après 30 secondes. Avec une telle regex, je m'attends à ce que ce soit un bug dans sed (GNU sed version 4.2.1):

$ time sed 's/./&\n/g' bigdna | awk '!/^$/{a[$0]++}END{for (i in a)print i,a[i];}' 
sed: couldn't re-allocate memory

real    0m31.326s
user    0m21.696s
sys     0m2.111s

La méthode perl semblait aussi prometteuse, mais j'ai abandonné après 7 minutes

time perl -e 'while (<>) {$c{$&}++ while /./g} print "$c{$_} $_\n" for keys %c' < bigdna 
^C

real    7m44.161s
user    4m53.941s
sys     2m35.593s

135



+1 Pour une solution saine quand il y a beaucoup de données, et pas seulement une poignée d'octets. Les fichiers sont dans le cache disque, n'est-ce pas? - Daniel Beck♦
La chose intéressante est qu’elle a une complexité de O (N) en traitement et O (1) en mémoire. Les tubes ont généralement O (N log N) en traitement (ou même O (N ^ 2)) et O (N) en mémoire. - Martin Ueding
Cependant, vous étirez un peu la définition de "ligne de commande". - gerrit
Flexion épique des exigences de la question - J'approuve; p. superuser.com/a/486037/10165 <- quelqu'un a couru des benchmarks, et ceci est l'option la plus rapide - Journeyman Geek♦
+1 J'apprécie le bon usage du C aux bons endroits. - Jeff Ferland


grep -o foo.text -e A -e T -e C -e G -e N -e -|sort|uniq -c

Fera le tour comme un seul paquebot. Une petite explication est cependant nécessaire.

grep -o foo.text -e A -e T -e C -e G -e N -e - greps le fichier foo.text pour les lettres a et g et le caractère - pour chaque caractère que vous souhaitez rechercher. Il imprime également un caractère par ligne.

sort le trie dans l'ordre. Cela ouvre la voie à l'outil suivant

uniq -c compte les occurrences consécutives en double de toute ligne. Dans ce cas, comme nous avons une liste de caractères triés, nous obtenons un décompte soigné du moment où les personnages que nous avons rencontrés dans la première étape

Si foo.txt contenait la chaîne GATTACA-c'est ce que j'obtiens de cet ensemble de commandes

[geek@atremis ~]$ grep -o foo.text -e A -e T -e C -e G -e N -e -|sort|uniq -c
      1 -
      3 A
      1 C
      1 G
      2 T

118



Bloody Unix Magie! :RÉ - Pitto
S'il n'y a que des caractères CTAG dans vos fichiers, l'expression rationnelle elle-même devient inutile, n'est-ce pas? grep -o. | trier | uniq -c fonctionnerait également bien, - sylvainulg
+1 J'utilise grep depuis 25 ans et je ne connaissais pas -o. - LarsH
@JourneymanGeek: Le problème est que cela génère beaucoup de données qui sont ensuite transmises au tri. Il serait moins coûteux de laisser un programme analyser chaque personnage. Voir la réponse de Dave pour une réponse de complexité de mémoire O (1) à la place de O (N). - Martin Ueding
@Pitto Les versions Windows natives de coreutils sont largement disponibles - il suffit de demander à Google ou à somesuch - OrangeDog


Essayez celui-ci, inspiré par la réponse de @ Journeyman.

grep -o -E 'A|T|C|G|N|-' foo.txt | sort | uniq -c

La clé est de savoir l'option -o pour grep. Cela divise la correspondance, de sorte que chaque ligne de sortie corresponde à une seule instance du modèle, plutôt que la ligne entière pour toute ligne correspondant. Compte tenu de ces connaissances, tout ce dont nous avons besoin est un modèle à utiliser et une manière de compter les lignes. En utilisant une expression rationnelle, nous pouvons créer un motif disjonctif qui correspondra à n'importe lequel des caractères que vous mentionnez:

A|T|C|G|N|-

Cela signifie "correspondre à A ou T ou C ou G ou N ou -". Le manuel décrit Diverses syntaxes d'expression régulière utilisables.

Maintenant, nous avons une sortie qui ressemble à ceci:

$ grep -o -E 'A|T|C|G|N|-' foo.txt 
A
T
C
G
N
-
-
A
A
N
N
N

Notre dernière étape consiste à fusionner et à compter toutes les lignes similaires, ce qui peut simplement être accompli avec un sort | uniq -c, comme dans la réponse de @ Journeyman. Le tri nous donne des résultats comme ceci:

$ grep -o -E 'A|T|C|G|N|-' foo.txt | sort
-
-
A
A
A
C
G
N
N
N
N
T

Qui, lorsqu'il est passé à travers uniq -c, ressemble finalement à ce que nous voulons:

$ grep -o -E 'A|T|C|G|N|-' foo.txt | sort | uniq -c
      2 -
      3 A
      1 C
      1 G
      4 N
      1 T

Addendum: Si vous souhaitez totaliser le nombre de caractères A, C, G, N, T et - dans un fichier, vous pouvez transmettre la sortie grep via wc -l au lieu de sort | uniq -c. Il y a beaucoup de choses différentes que vous pouvez compter avec seulement de légères modifications à cette approche.


45



J'ai vraiment besoin de fouiller dans les rabbitholes qui sont coreutils et regex. C'est un peu plus élégant que le mien pour cela; p - Journeyman Geek♦
@JourneymanGeek: L'apprentissage des regex vaut bien la peine, car il est utile pour beaucoup de choses. Il suffit de comprendre ses limites et de ne pas abuser du pouvoir en essayant de faire des choses en dehors des capacités des regex, comme essayer d'analyser XHTML. - crazy2be
grep -o '[ATCGN-]' pourrait être un peu plus lisible ici. - sylvainulg


Un liner comptant toutes les lettres en Python:

$ python -c "import collections, pprint; pprint.pprint(dict(collections.Counter(open('FILENAME_HERE', 'r').read())))"

... produisant une sortie conviviale YAML comme ceci:

{'\n': 202,
 ' ': 2153,
 '!': 4,
 '"': 62,
 '#': 12,
 '%': 9,
 "'": 10,
 '(': 84,
 ')': 84,
 '*': 1,
 ',': 39,
 '-': 5,
 '.': 121,
 '/': 12,
 '0': 5,
 '1': 7,
 '2': 1,
 '3': 1,
 ':': 65,
 ';': 3,
 '<': 1,
 '=': 41,
 '>': 12,
 '@': 6,
 'A': 3,
 'B': 2,
 'C': 1,
 'D': 3,
 'E': 25}

Il est intéressant de voir comment la plupart du temps, Python peut facilement battre même la clarté du code.


13





Similaire aux gourous awk méthode:

perl -e 'while (<>) {$c{$&}++ while /./g} print "$c{$_} $_\n" for keys %c'

11





Après avoir utilisé UNIX pendant quelques années, vous maîtrisez très bien plusieurs petites opérations pour effectuer différentes tâches de filtrage et de filtrage. Tout le monde a son propre style - certains aiment awk et sed, certains, comme cut et tr. Voici comment je le ferais:

Pour traiter un nom de fichier particulier:

 od -a FILENAME_HERE | cut -b 9- | tr " " \\n | egrep -v "^$" | sort | uniq -c

ou comme filtre:

 od -a | cut -b 9- | tr " " \\n | egrep -v "^$" | sort | uniq -c

Cela fonctionne comme ceci:

  1. od -a sépare le fichier en caractères ASCII.
  2. cut -b 9- élimine le préfixe od met
  3. tr " " \\n convertit les espaces entre les caractères en nouvelles lignes, il y a donc un caractère par ligne.
  4. egrep -v "^$" se débarrasse de toutes les lignes vides supplémentaires que cela crée.
  5. sort rassemble les instances de chaque personnage ensemble.
  6. uniq -c compte le nombre de répétitions de chaque ligne.

Je l'ai nourri "Bonjour, monde!" suivi par une nouvelle ligne et a obtenu ceci:

  1 ,
  1 !
  1 d
  1 e
  1 H
  3 l
  1 nl
  2 o
  1 r
  1 sp
  1 w

10





le sed la partie étant basée sur @ Réponse du gourou, voici une autre approche utilisant uniq, similaire à la solution de David Schwartz.

$ cat foo
aix
linux
bsd
foo
$ sed 's/\(.\)/\1\n/g' foo | sort | uniq -c
4 
1 a
1 b
1 d
1 f
2 i
1 l
1 n
2 o
1 s
1 u
2 x

9



Utilisation [[:alpha:]] plutôt que . dans sed pour correspondre uniquement aux caractères et non aux nouvelles lignes. - Claudius
[[:alpha:]] va échouer si vous essayez également de faire correspondre des choses comme -, qui était mentionné dans la question - Izkata
Correct. Il serait peut-être plus intéressant d’ajouter une seconde expression à sed pour tout d’abord filtrer tout le reste, puis faire correspondre explicitement les caractères souhaités: sed -e 's/[^ATCGN-]//g' -e 's/\([ATCGN-]\)/\1\n/g' foo | sort | uniq -c. Cependant, je ne sais pas comment y remédier: \ - Claudius


Vous pouvez combiner grep et wc pour faire ça:

grep -o 'character' file.txt | wc -w

grep recherche le ou les fichiers donnés pour le texte spécifié, et le -o option lui indique de n'imprimer que les correspondances réelles (c.-à-d. les caractères que vous cherchiez), plutôt que la valeur par défaut qui consiste à imprimer chaque ligne sur laquelle le texte de recherche a été trouvé.

wc imprime le nombre d'octets, de mots et de lignes pour chaque fichier ou, dans ce cas, la sortie du grep commander. le -w L'option lui indique de compter les mots, chaque mot étant une occurrence de votre caractère de recherche. Bien sûr, le -l l'option (qui compte les lignes) fonctionnerait également, puisque grep imprime chaque occurrence de votre caractère de recherche sur une ligne distincte.

Pour faire cela pour un certain nombre de caractères à la fois, placez les caractères dans un tableau et faites-en une boucle:

chars=(A T C G N -)
for c in "${chars[@]}"; do echo -n $c ' ' && grep -o $c file.txt | wc -w; done

Exemple: pour un fichier contenant la chaîne TGC-GTCCNATGCGNNTCACANN-, le résultat serait:

A  3
T  4
C  6
G  4
N  5
-  2

Pour plus d'informations, voir man grep et man wc.


L’inconvénient de cette approche, comme le note ci-dessous l’utilisateur Journeyman Geek, est que grep doit être exécuté une fois pour chaque personnage. En fonction de la taille de vos fichiers, cela peut entraîner une baisse sensible des performances. D'un autre côté, lorsque cela est fait de cette manière, il est un peu plus facile de voir rapidement quels caractères sont recherchés et de les ajouter / supprimer, car ils se trouvent sur une ligne distincte du reste du code.


7



ils auraient besoin de le répéter par caractère qu'ils veulent ... J'ajouterais. Je pourrais jurer qu'il existe une solution plus élégante, mais il faut plus de piquer - Journeyman Geek♦
@JourneymanGeek Bon point. Une approche qui vient à l'esprit est de mettre les caractères dans un tableau et de les parcourir en boucle. J'ai mis à jour mon post. - Indrek
trop complexe OMI. Utilisez simplement grep -e a -e t et ainsi de suite. Si vous le mettez dans un tableau et que vous le parcourez en boucle, ne devriez-vous pas exécuter le cycle grep une fois par caractère? - Journeyman Geek♦
@JourneymanGeek Vous avez probablement raison. uniq -c semble également être un meilleur moyen d'obtenir une sortie bien formatée. Je ne suis pas un gourou de * nix, ce qui précède est ce que j'ai réussi à rassembler à partir de mes connaissances limitées et de certaines pages de manuel :) - Indrek
Moi aussi, p, et l'une de mes missions du dernier trimestre impliquait de trier environ 5000 entrées du carnet d'adresses, et uniq l'a rendu beaucoup plus facile. - Journeyman Geek♦