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?
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
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
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.
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.
Similaire à la méthode awk
de Guru:
Perl -e 'while (<>) {$c{$&}++ while /./g} print "$c{$_} $_\n" for keys %c'
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:
od -a
sépare le fichier en ASCII caractères.cut -b 9-
élimine le préfixe od
met.tr " " \\n
convertit les espaces entre les caractères en nouvelles lignes afin qu'il y ait un caractère par ligne.egrep -v "^$"
supprime toutes les lignes vides supplémentaires que cela crée.sort
rassemble les occurrences de chaque caractère.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
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
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 .
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.
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
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 -u
nique 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.
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:
La vitesse semble être de 60 Mbps +
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,tr
pour traduire ces caractères NUL en NL. Maintenant, chaque personnage est sur sa propre lignesort | uniq -c
compter chaque ligne uniq C'est une alternative à l'option non-standard (GNU) -o
grep.
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.
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:
Et deux choses qui pourraient ne pas avoir d'importance mais que je testerais avec d'abord
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.
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
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.
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 automatiquementwhile
: itérer sur toutes les occurrences de vos symboles demandés dans la ligne couranteEND
: À la fin, imprimer les résultats%a
: Hash où les valeurs sont stockéesLes caractères qui ne se produisent pas du tout ne seront pas inclus dans le résultat.