web-dev-qa-db-fra.com

Quel est le moyen le plus rapide de compter le nombre de caractères dans un fichier?

Je veux compter les caractères "-" dans un fichier, ou chaque lettre si nécessaire, existe-t-il une commande Unix rapide pour le faire?

120
Kirt

Si vous voulez de la vitesse réelle:

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 CPU Core i7 870 à 2,93 GHz, elle 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 impliquant le tri, celle-ci fonctionne en mémoire constante (4K), ce qui est très utile si votre fichier est beaucoup plus volumineux que votre RAM.

Et, bien sûr, avec un peu de graisse pour coude, nous pouvons gagner 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 juste au-dessus de 1,1 Go/s en finition en:

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

À titre de comparaison, j’ai testé certaines des autres solutions de cette page qui semblaient prometteuses.

La solution sed/awk a fait un effort vaillant, mais est morte au bout de 30 secondes. Avec une expression aussi simple, je m'attends à ce que ce soit un bogue dans sed (version 4.2.1 de GNU sed):

$ 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 également prometteuse, mais j’ai abandonné après 7 minutes d’exécution.

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
Dave

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

Fera le tour comme un one-line. Une petite explication est nécessaire cependant.

grep -o foo.text -e A -e T -e C -e G -e N -e - insère le fichier foo.text pour les lettres a et g et le caractère - pour chaque caractère à 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 d'une ligne. Dans ce cas, puisque nous avons une liste triée de caractères, nous obtenons un décompte précis du moment où les caractères ont été extirpés à la première étape.

Si foo.txt contenait la chaîne GATTACA- c'est ce que j'obtiendrais 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
Journeyman Geek

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 connaître l'option -o pour grep . Cela divise la correspondance, de sorte que chaque ligne de sortie correspond à une seule instance du modèle, plutôt qu'à la ligne entière de toute ligne correspondante. Compte tenu de ces connaissances, tout ce dont nous avons besoin est un modèle à utiliser et un moyen de compter les lignes. En utilisant une regex, nous pouvons créer un motif disjonctif qui correspondra à n’importe quel des caractères que vous mentionnez:

A|T|C|G|N|-

Cela signifie "faire correspondre A ou T ou C ou G ou N ou -". Le manuel décrit diverses syntaxes d’expression régulière que vous pouvez utiliser .

Nous avons maintenant 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. La sorte nous donne la sortie comme ceci:

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

Ce qui, une fois passé par 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 diriger la sortie de 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
crazy2be

Une doublure comptant toutes les lettres utilisant 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 en termes de clarté du code.

13
Giampaolo Rodolà

Similaire à la méthode awk de Guru:

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

Après avoir utilisé UNIX pendant quelques années, vous maîtrisez parfaitement les opérations de liaison entre plusieurs petites opérations pour effectuer diverses tâches de filtrage et de comptage. Tout le monde a son propre style - certains aiment awk et sed, d'autres 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 ASCII caractères.
  2. cut -b 9- élimine le préfixe od met.
  3. tr " " \\n convertit les espaces entre les caractères en nouvelles lignes afin qu'il y ait un caractère par ligne.
  4. egrep -v "^$" supprime toutes les lignes vides supplémentaires que cela crée.
  5. sort rassemble les occurrences de chaque caractère.
  6. uniq -c compte le nombre de répétitions de chaque ligne.

Je l'ai nourri "Bonjour tout le monde!" suivi d'une nouvelle ligne et obtenu ceci:

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

La partie sed étant basée sur la réponse de @ Guru , voici une autre approche qui utilise uniq, semblable à 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
Claudius

En utilisant les lignes de séquence de 22hgp10a.txt, la différence de temps entre grep et awk sur mon système rend l'utilisation de awk très utile ...

[Edit]: Après avoir vu la solution compilée de Dave, oubliez également awk, qui se termine en ~ 0,1 seconde sur ce fichier pour un comptage respectant la casse.

# A Nice large sample file.
wget http://gutenberg.readingroo.ms/etext02/22hgp10a.txt

# Omit the regular text up to the start `>chr22` indicator.
sed -ie '1,/^>chr22/d' 22hgp10a.txt

Sudo test # Just get Sudo setup to not ask for password...

# ghostdog74 answered a question <linked below> about character frequency which
# gave me all case sensitive [ACGNTacgnt] counts in ~10 seconds.
Sudo chrt -f 99 /usr/bin/time -f "%E elapsed, %c context switches" \
awk -vFS="" '{for(i=1;i<=NF;i++)w[$i]++}END{for(i in w) print i,w[i]}' 22hgp10a.txt

# The grep version given by Journeyman Geek took a whopping 3:41.47 minutes
# and yielded the case sensitive [ACGNT] counts.
Sudo chrt -f 99 /usr/bin/time -f "%E elapsed, %c context switches" \
grep -o foo.text -e A -e T -e C -e G -e N -e -|sort|uniq -c

La version de ghostdog insensible à la casse est terminée en 14 secondes environ.

Le sed est expliqué dans la réponse acceptée à cette question .
Le benchmarking est comme dans la réponse acceptée à cette question .
La réponse acceptée par ghostdog74 était cette question .

7
Thell

Vous pouvez combiner grep et wc pour faire ceci:

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

grep recherche dans le (s) fichier (s) donné (s) le texte spécifié. L'option -o lui indique d'imprimer uniquement les correspondances réelles (c'est-à-dire les caractères recherchés), plutôt que le paramètre par défaut consistant à imprimer chaque ligne dans laquelle la recherche est effectuée. le texte a été trouvé sur.

wc imprime les nombres d'octets, de mots et de lignes pour chaque fichier ou, dans ce cas, la sortie de la commande grep. L'option -w lui dit de compter les mots, chaque mot étant une occurrence de votre caractère de recherche. Bien entendu, l'option -l (qui compte les lignes) fonctionnerait également, car grep imprime chaque occurrence du caractère recherché sur une ligne distincte.

Pour faire cela pour un nombre de caractères à la fois, placez les caractères dans un tableau et passez-le en 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 l'utilisateur Journeyman Geek ci-dessous, 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 notable des performances. En revanche, une fois cette opération effectuée, il est un peu plus facile de voir rapidement quels caractères sont recherchés et de les ajouter/supprimer, car ils sont sur une ligne distincte du reste du code.

7
Indrek

Je pense que toute mise en œuvre décente évite de trier. Mais comme c'est aussi une mauvaise idée de tout lire 4 fois, je pense que l'on pourrait générer un flux qui passe par 4 filtres, un pour chaque caractère, qui est filtré et où les longueurs de flux sont également calculées.

time cat /dev/random | tr -d -C 'AGCTN\-' | head -c16M >dna.txt
real    0m5.797s
user    0m6.816s
sys     0m1.371s

$ time tr -d -C 'AGCTN\-' <dna.txt | tee >(wc -c >tmp0.txt) | tr -d 'A' | 
tee >(wc -c >tmp1.txt) | tr -d 'G' | tee >(wc -c >tmp2.txt) | tr -d 'C' | 
tee >(wc -c >tmp3.txt) | tr -d 'T' | tee >(wc -c >tmp4.txt) | tr -d 'N' | 
tee >(wc -c >tmp5.txt) | tr -d '\-' | wc -c >tmp6.txt && cat tmp[0-6].txt

real    0m0.742s
user    0m0.883s
sys     0m0.866s

16777216
13983005
11184107
8387205
5591177
2795114
0

Les sommes cumulées sont alors dans tmp [0-6] .txt .. donc le travail est toujours en cours

Dans cette approche, il n’ya que 13 canaux convertis en moins de 1 Mo de mémoire.
Bien sûr, ma solution préférée est:

time cat >f.c && gcc -O6 f.c && ./a.out
# then type your favourite c-program
real    0m42.130s
6
Aki Suihkonen

Je ne connaissais ni uniq ni grep -o, mais comme mes commentaires sur @JourneymanGeek et @ crazy2be bénéficiaient d'un tel soutien, je devrais peut-être en faire une réponse à part entière:

Si vous savez qu'il n'y a que de "bons" caractères (ceux que vous voulez compter) dans votre fichier, vous pouvez aller chercher

grep . -o YourFile | sort | uniq -c

Si seuls certains caractères doivent être comptés et d’autres pas (séparateurs)

grep '[ACTGN-]' YourFile | sort | uniq -c

Le premier utilise l'expression générique ., qui correspond à n'importe quel caractère. Le second utilise un "jeu de caractères acceptés", sans ordre spécifique, excepté que - doit venir en dernier (A-C est interprété comme "tout caractère entreA et C). Des guillemets sont nécessaires dans ce cas pour que votre shell n'essaie pas de développer cela pour vérifier les fichiers à un seul caractère, le cas échéant (et produire une erreur "aucune correspondance" si aucun).

Notez que "sort" a aussi un drapeau -unique pour ne rapporter que les choses une fois, mais qu’il n’a pas d’indicateur compagnon pour compter les doublons, alors uniq est en effet obligatoire.

4
sylvainulg
time $( { tr -cd ACGTD- < dna.txt | dd | tr -d A | dd | tr -d C | dd | tr -d G |
dd | tr -d T | dd | tr -d D | dd | tr -d - | dd >/dev/null; } 2>tmp ) &&
grep byte < tmp | sort -r -g | awk '{ if ((s-$0)>=0) { print s-$0} s=$0 }'

Le format de sortie n'est pas le meilleur ...

real    0m0.176s
user    0m0.200s
sys     0m0.160s
2069046
2070218
2061086
2057418
2070062
2052266

Théorie de fonctionnement:

  • $ ({command | command} 2> tmp) redirige le stderr du flux vers un fichier temporaire.
  • dd sort stdin sur stdout et affiche le nombre d'octets passés à stderr
  • tr -d filtre un caractère à la fois
  • grep and sort filtre la sortie de dd en ordre décroissant
  • awk calcule la différence
  • le tri est utilisé uniquement dans l'étape de post-traitement pour traiter l'incertitude de l'ordre de sortie des instances de dd

La vitesse semble être de 60 Mbps +

2
Aki Suihkonen

Un idiot:

tr -cd ATCGN- | iconv -f ascii -t ucs2 | tr '\0' '\n' | sort | uniq -c
  • tr pour supprimer (-d) tous les caractères sauf (-c) ATCGN-
  • iconv pour convertir en ucs2 (UTF16 limité à 2 octets) pour ajouter un octet 0 après chaque octet,
  • une autre tr pour traduire ces caractères NUL en NL. Maintenant, chaque personnage est sur sa propre ligne
  • sort | uniq -c compter chaque ligne uniq

C'est une alternative à l'option non-standard (GNU) -o grep.

2
sch

Combinant quelques autres

chars='abcdefghijklmnopqrstuvwxyz-'
grep -o -i "[$chars]" foo|sort | uniq -c

Ajoutez | sort -nr pour voir les résultats par ordre de fréquence.

1
Keith Wolters

Réponse courte:

Si les circonstances le permettent, comparez les tailles de fichier des jeux de caractères faibles à celles ne contenant aucun caractère pour obtenir un décalage et compter simplement les octets.

Ah, mais les détails enchevêtrés:

Ce sont tous des personnages Ascii. Un octet par. Bien sûr, les fichiers contiennent des métadonnées supplémentaires pour une variété d'éléments utilisés par le système d'exploitation et l'application qui les a créés. Dans la plupart des cas, je m'attendrais à ce qu'ils occupent le même espace indépendamment des métadonnées, mais j'essaierais de maintenir des circonstances identiques lorsque vous testez d'abord l'approche, puis que vous vérifiez que vous disposez d'un décalage constant avant de ne pas vous en préoccuper. L'autre piège est que les sauts de ligne impliquent généralement deux caractères d'espaces blancs ascii et que chaque onglet ou espace correspond à un. Si vous pouvez être sûr qu'ils seront présents et qu'il n'y a aucun moyen de savoir combien d'avance, je cesserais de lire maintenant.

Cela peut sembler beaucoup de contraintes, mais si vous pouvez facilement les établir, cela me semble l’approche la plus facile/la plus performante si vous en avez beaucoup à regarder (ce qui semble probable si c’est de l’ADN). Contrôler la longueur d'une tonne de fichiers et soustraire une constante prendrait plus de temps que d'exécuter grep (ou similaire) sur chacun d'entre eux.

Si:

  • Ce sont de simples chaînes ininterrompues dans des fichiers texte purs
  • Ils sont dans des types de fichiers identiques créés par le même éditeur de texte sans mise en forme dans Vanilla comme Scite (le collage est correct tant que vous vérifiez les espaces/retours) ou un programme de base écrit par quelqu'un

Et deux choses qui pourraient ne pas avoir d'importance mais que je testerais avec d'abord

  • Les noms de fichiers sont de même longueur
  • Les fichiers sont dans le même répertoire

Essayez de trouver le décalage en procédant comme suit:

Comparez un fichier vide à un fichier contenant quelques caractères facilement comptables, à un autre contenant quelques caractères. Si vous soustrayez le fichier vide des deux autres fichiers pour obtenir un nombre d'octets correspondant au nombre de caractères, vous avez terminé. Vérifiez les longueurs de fichier et soustrayez cette quantité vide. Si vous voulez essayer de comprendre des fichiers multilignes, la plupart des éditeurs associent deux caractères spéciaux d'un octet pour les sauts de ligne, l'un d'eux étant généralement ignoré, mais vous devrez au moins grep pour les caractères d'espace-blanc, auquel cas vous pourriez aussi bien tout faire avec grep.

1
Erik Reppen

Exemple de fichier:

$ cat file
aix
unix
linux

Commander:

$ sed 's/./&\n/g' file | awk '!/^$/{a[$0]++}END{for (i in a)print i,a[i];}'
u 2
i 3
x 3
l 1
n 2
a 1
1
Guru

Haskell manière:

import Data.Ord
import Data.List
import Control.Arrow

main :: IO ()
main = interact $
  show . sortBy (comparing fst) . map (length &&& head) . group . sort

cela fonctionne comme ceci:

112123123412345
=> sort
111112222333445
=> group
11111 2222 333 44 5
=> map (length &&& head)
(5 '1') (4 '2') (3 '3') (2 '4') (1,'5')
=> sortBy (comparing fst)
(1 '5') (2 '4') (3 '3') (4 '2') (5 '1')
=> one can add some pretty-printing here
...

compiler et utiliser:

$ ghc -O2 q.hs
[1 of 1] Compiling Main             ( q.hs, q.o )
Linking q ...
$ echo 112123123412345 | ./q
[(1,'\n'),(1,'5'),(2,'4'),(3,'3'),(4,'2'),(5,'1')]%       
$ cat path/to/file | ./q
...

pas bon pour les gros fichiers peut-être.

1
ht.

Quick Perl hack:

Perl -nle 'while(/[ATCGN]/g){$a{$&}+=1};END{for(keys(%a)){print "$_:$a{$_}"}}'
  • -n: Itérer sur les lignes mais ne rien imprimer pour elles
  • -l: Supprimer ou ajouter des sauts de ligne automatiquement
  • while: itérer sur toutes les occurrences de vos symboles demandés dans la ligne courante
  • END: À la fin, imprimer les résultats
  • %a: Hash où les valeurs sont stockées

Les caractères qui ne se produisent pas du tout ne seront pas inclus dans le résultat.

1
MvG