Comment compter le nombre d'un caractère spécifique dans chaque ligne?


88

Je me demandais comment compter le nombre d'un caractère spécifique dans chaque ligne avec certains utilitaires de traitement de texte?

Par exemple, compter "dans chaque ligne du texte suivant

"hello!" 
Thank you!

La première ligne a deux et la deuxième ligne a 0.

Un autre exemple consiste à compter (dans chaque ligne.


1
J'ajouterai simplement que vous avez beaucoup plus performé en écrivant votre propre programme en 10 lignes en C plutôt que d'utiliser des expressions régulières avec sed. Vous devriez envisager de le faire en fonction de la taille de vos fichiers d'entrée.
user606723

Réponses:


105

Vous pouvez le faire avec sedet awk:

$ sed 's/[^"]//g' dat | awk '{ print length }'
2
0

Où se dattrouve votre exemple de texte, sed supprime (pour chaque ligne) tous les "caractères autres que des caractères et awkaffiche pour chaque ligne sa taille (c’est length-à- dire qu’elle correspond à length($0), où $0indique la ligne en cours).

Pour un autre personnage, il vous suffit de changer l'expression sed. Par exemple pour (:

's/[^(]//g'

Mise à jour: sed est un peu excessif pour la tâche - trsuffit. Une solution équivalente avec trest:

$ tr -d -c '"\n' < dat | awk '{ print length; }'

Ce qui trsupprime tous les caractères qui ne sont pas ( -csignifie complément) dans le jeu de caractères "\n.


3
+1 devrait être plus efficace que la version tr& wc.
Stéphane Gimenez

1
Oui, mais peut-il gérer Unicode?
amphetamachine

@amphetamachine, oui - au moins un test rapide avec ß(hex utf: c3 9F) ( au lieu de ") fonctionne comme prévu, à savoir tr, sedet awkne compléter / remplacer / comptage sans problème - sur un système Ubuntu 10.04.
maxschlepzig

1
La plupart des versions tr, y compris GNU tr et classic Unix tr, fonctionnent sur des caractères à un octet et ne sont pas compatibles avec Unicode .. Cité sur Wikipedia tr (Unix) .. Essayez cet extrait: echo "aā⧾c" | tr "ā⧾" b... sur Ubuntu 10.04 ... ßest un seul octet Le caractère latin étendu est traité par tr... Le vrai problème ici n’est pas que trUnicode ne soit pas géré (car TOUS les caractères sont Unicode), c’est vraiment que trseul un octet à la fois est
traité

@fred, non, ß n'est pas un caractère sur un octet - sa position Unicode est U + 00DF, qui est codée comme «c3 9f» en UTF-8, c'est-à-dire deux octets.
maxschlepzig

50

Je voudrais juste utiliser awk

awk -F\" '{print NF-1}' <fileName>

Ici, nous définissons le séparateur de champ (avec l'indicateur -F) comme étant le caractère, "puis tout ce que nous faisons est d'imprimer le nombre de champs NF- 1. Le nombre d'occurrences du caractère cible sera égal à un de moins que le nombre de champs séparés.

Pour les personnages amusants interprétés par le shell, vous devez simplement vous assurer de les échapper, sinon la ligne de commande essaiera de les interpréter. Donc, pour les deux "et )vous devez échapper au séparateur de champ (avec \).


1
Modifiez peut-être votre réponse pour utiliser des guillemets simples à la place pour vous échapper. Cela fonctionnera avec n'importe quel personnage (sauf '). En outre, il a un comportement étrange avec des lignes vides.
Stéphane Gimenez

La question utilise spécifiquement "donc je me sens obligé de faire fonctionner le code avec elle. Cela dépend de la coquille que vous utilisez si le personnage doit être échappé, mais bash / tcsh devront s'échapper tous les deux "
Martin York

Bien sûr, mais il n'y a pas de problème avec -F'"'.
Stéphane Gimenez

+1 Quelle bonne idée d'utiliser FS .... Ceci résoudra la ligne vide affichant -1 et, par exemple, le "$ 1" à partir de la ligne de commande bash. ...awk -F"$1" '{print NF==0?NF:NF-1}' filename
Peter.O

Travaillez également avec plusieurs caractères comme séparateur ... utile!
Bob

15

En utilisant trard wc:

function countchar()
{
    while IFS= read -r i; do printf "%s" "$i" | tr -dc "$1" | wc -m; done
}

Usage:

$ countchar '"' <file.txt  #returns one count per line of file.txt
1
3
0

$ countchar ')'           #will count parenthesis from stdin
$ countchar '0123456789'  #will count numbers from stdin

3
Remarque. trne gère pas les caractères qui utilisent plus d'un octet .. voir Wikipedia tr (Unix) .. ie. trn'est pas conforme à Unicode.
Peter.O


vous devez supprimer les caractères d'espacement $IFS, sinon vous readdevrez les rogner du début à la fin.
Stéphane Chazelas


@ Peter.O, certaines trimplémentations prennent en charge les caractères multi-octets, mais wc -ccomptent des octets, pas des caractères quand même (nécessité wc -mde caractères).
Stéphane Chazelas

11

Une autre mise en œuvre qui ne repose pas sur des programmes externes, dans bash, zsh, yashet certaines implémentations / versions de ksh:

while IFS= read -r line; do 
  line="${line//[!\"]/}"
  echo "${#line}"
done <input-file

Utilisez line="${line//[!(]}"pour compter (.


Lorsque la dernière ligne ne comporte pas \ n de fin, la boucle while se termine, car bien qu'elle lise la dernière ligne, elle renvoie également un code de sortie différent de zéro pour indiquer EOF ... pour le contourner, l'extrait de code suivant fonctionne. (..Il me dérange depuis un moment, et je viens de découvrir ce workaroung) ... eof=false; IFS=; until $eof; do read -r || eof=true; echo "$REPLY"; done
Peter.O

@ Gilles: vous avez ajouté une fin /qui n'est pas nécessaire dans bash. C'est une exigence de ksh?
enzotib

1
La fin /est nécessaire dans les anciennes versions de ksh, et IIRC dans les anciennes versions de bash.
Gilles

10

Les réponses à l'aide awkéchouent si le nombre de correspondances est trop grand (ce qui est mon cas). Pour la réponse de loki-astari , l'erreur suivante est signalée:

awk -F" '{print NF-1}' foo.txt 
awk: program limit exceeded: maximum number of fields size=32767
    FILENAME="foo.txt" FNR=1 NR=1

Pour la réponse de enzotib (et l’équivalent de manatwork ), une erreur de segmentation se produit:

awk '{ gsub("[^\"]", ""); print length }' foo.txt
Segmentation fault

La sedsolution de maxschlepzig fonctionne correctement, mais est lente (timings ci-dessous).

Certaines solutions pas encore suggérées ici. Tout d'abord, en utilisant grep:

grep -o \" foo.txt | wc -w

Et en utilisant perl:

perl -ne '$x+=s/\"//g; END {print "$x\n"}' foo.txt

Voici quelques timings pour quelques solutions (ordonnées du plus lent au plus rapide); J'ai limité les choses à une ligne ici. 'foo.txt' est un fichier avec une ligne et une longue chaîne contenant 849 correspondances.

## sed solution by [maxschlepzig]
$ time sed 's/[^"]//g' foo.txt | awk '{ print length }'
84922
real    0m1.207s
user    0m1.192s
sys     0m0.008s

## using grep
$ time grep -o \" foo.txt | wc -w
84922
real    0m0.109s
user    0m0.100s
sys     0m0.012s

## using perl
$ time perl -ne '$x+=s/\"//g; END {print "$x\n"}' foo.txt
84922
real    0m0.034s
user    0m0.028s
sys     0m0.004s

## the winner: updated tr solution by [maxschlepzig]
$ time tr -d -c '\"\n' < foo.txt |  awk '{ print length }'
84922
real    0m0.016s
user    0m0.012s
sys     0m0.004s

+ bonne idée! J'ai élargi votre tableau, dans une nouvelle réponse, n'hésitez pas à éditer (la photo finale n'est pas aussi claire, mais je crois que @maxschlepzig est l'acier la solution la plus rapide)
JJoao

La solution de maxschlepzig est super rapide!
okwap


8

Une autre implémentation possible avec awk et gsub:

awk '{ gsub("[^\"]", ""); print length }' input-file

La fonction gsubest l'équivalent de sed 's///g'.

Utilisez gsub("[^(]", "")pour compter (.


Vous pouvez sauvegarder un caractère, c'est-à-dire lorsque vous supprimez la redirection stdin ...;)
maxschlepzig

@maxschlepzig: oui, bien sûr;)
enzotib

1
awk '{print gsub(/"/,"")}' input-fileserait suffisant, comme "Pour chaque sous-chaîne correspondant à l'expression régulière r dans la chaîne t, remplacez la chaîne s et renvoyez le nombre de substitutions." (man awk)
manatwork

6

J'ai décidé d'écrire un programme C parce que je m'ennuyais.

Vous devriez probablement ajouter une validation d'entrée, mais à part cela, tout est défini.

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
        char c = argv[1][0];
        char * line = NULL;
        size_t len = 0;
        while (getline(&line, &len, stdin) != -1)
        {
                int count = 0;
                char * s = line;
                while (*s) if(*s++ == c) count++;
                printf("%d\n",count);
        }
        if(line) free(line);
}

Merci! Merci de m'ennuyer pour que je puisse apprendre quelque chose. Oh, attends, tu as besoin d'un retour?
Tim

* hausse les épaules * , si vous voulez être complètement correct, vous devez également ajouter un peu plus de #includes, mais les avertissements par défaut sur mon compilateur ne semblent pas s'en soucier.
user606723

Vous pouvez free(line)omettre car la sortie du programme libère implicitement toute la mémoire allouée - alors il y a de la place pour un return 0;...;). Même dans les exemples, il n'est pas bon de laisser le code de retour non défini. Btw, getlineest une extension GNU - au cas où quelqu'un se le demanderait.
maxschlepzig

@maxschlepzig: La mémoire indiquée par ligne est-elle allouée par getline ()? Est-il alloué dynamiquement sur tas par malloc ou de manière statique sur pile? Vous avez dit que libérer ce n'est pas nécessaire, alors n'est-il pas alloué de manière dynamique?
Tim

1
@ Tim, oui, par exemple, si vous refactorisez le code de telle sorte qu'il s'agisse d'une fonction autonome - disons - f, appelée plusieurs fois à partir d'un autre code, vous devez appeler freeaprès le dernier appel de getlineà la fin de cette fonction f.
maxschlepzig

6

Pour une chaîne, le plus simple serait avec tret wc(pas besoin de trop utiliser avec awkou sed) - mais notez les commentaires ci-dessus à propos tr, compte les octets, pas les caractères -

echo $x | tr -d -c '"' | wc -m

$xest la variable qui contient la chaîne (pas un fichier) à évaluer.


4

Voici une autre solution C qui nécessite uniquement STD C et moins de mémoire:

#include <stdio.h>

int main(int argc, char **argv)
{
  if (argc < 2 || !*argv[1]) {
    puts("Argument missing.");
    return 1;
  }
  char c = *argv[1], x = 0;
  size_t count = 0;
  while ((x = getc(stdin)) != EOF)
    if (x == '\n') {
      printf("%zd\n", count);
      count = 0;
    } else if (x == c)
      ++count;
  return 0;
}

Cela ne sera pas signalé sur la dernière ligne s'il n'y a pas de '\ n'
final

1
@fred, oui, ce qui est intentionnel, car une ligne sans fin \nn'est pas une vraie ligne. C'est le même comportement qu'avec mon autre réponse sed / awk (tr / awk).
maxschlepzig

3

Nous pouvons utiliser grepavec regexpour le rendre plus simple et puissant.

Compter un caractère spécifique.

$ grep -o '"' file.txt|wc -l

Compter les caractères spéciaux, y compris les espaces.

$ grep -Po '[\W_]' file.txt|wc -l

Ici, nous sélectionnons n’importe quel caractère avec [\S\s]et avec la -opossibilité grepd’imprimer chaque correspondance (c’est-à-dire chaque caractère) sur une ligne distincte. Et puis utilisez wc -lpour compter chaque ligne.


OP ne veut pas imprimer le nombre de caractères dans un fichier! Il veut compter / imprimer le numéro d'un personnage spécifique. par exemple combien "sont dans chaque ligne; et pour tous les autres personnages. voir sa question et a également accepté la réponse.
αғsнιη

3

Peut-être qu'une réponse plus simple, purement awk, serait d'utiliser la scission. Split prend une chaîne et la transforme en tableau, la valeur de retour est le nombre d'éléments de tableau générés + 1.

Le code suivant affichera le nombre de fois "apparaît sur chaque ligne.

awk ' {print (split($0,a,"\"")-1) }' file_to_parse

plus d'infos sur le split http://www.staff.science.uu.nl/~oostr102/docs/nawk/nawk_92.html


2

Voici un script Python simple pour trouver le nombre de "dans chaque ligne d'un fichier:

#!/usr/bin/env python2
with open('file.txt') as f:
    for line in f:
        print line.count('"')

Ici, nous avons utilisé la countméthode du type intégré str.


2

Pour une solution pure bash (cependant, elle est spécifique à bash): If $xest la variable contenant votre chaîne:

x2="${x//[^\"]/}"
echo ${#x2}

La ${x//chose supprime tous les caractères sauf ", ${#x2}calcule la longueur de ce repos.

(Suggestion originale en utilisant exprqui a des problèmes, voir les commentaires:)

expr length "${x//[^\"]/}"

Notez qu'il est spécifique à GNU expret compte octets, pas de caractères. Avec d'autres expr:expr "x${x...}" : "x.*" - 1
Stéphane Chazelas

Oh oui, merci! Je l'ai modifié en utilisant une autre idée que je viens d'avoir, qui a l'avantage de ne pas utiliser de programme externe du tout.
Marian

2

Remplacer apar le caractère à compter. La sortie est le compteur pour chaque ligne.

perl -nE 'say y!a!!'

2

Comparaison dans le temps des solutions présentées (pas une réponse)

L'efficacité des réponses n'est pas importante. Néanmoins, après l’approche @josephwb, j’ai essayé de chronométrer toutes les réponses présentées.

J'utilise comme entrée la traduction portugaise de Victor Hugo "Les Misérables" (grand livre!) Et compte les occurrences de "a". Mon édition a 5 volumes, plusieurs pages ...

$ wc miseraveis.txt 
29331  304166 1852674 miseraveis.txt 

Les réponses en C ont été compilées avec gcc, sans optimisation.

Chaque réponse a été exécutée 3 fois et choisissez le meilleur.

Ne faites pas trop confiance à ces chiffres (ma machine effectue d'autres tâches, etc.). Je partage ces moments avec vous, car j’ai eu des résultats inattendus et je suis sûr que vous en trouverez d’autres ...

  • 14 des 16 solutions chronométrées ont pris moins de 1 s; 9 moins de 0,1, beaucoup d'entre eux utilisant des tuyaux
  • 2 solutions, utilisant bash ligne par ligne, ont traité les 30k lignes en créant de nouveaux processus et calculent la solution correcte en 10s / 20s.
  • grep -oP aest l'arbre fois plus vite que grep -o a (10; 11 vs 12)
  • La différence entre C et les autres n'est pas si grande que ce à quoi je m'attendais. (7; 8 vs 2; 3)
  • (conclusions bienvenues)

(résultats dans un ordre aléatoire)

=========================1 maxschlepzig
$ time sed 's/[^a]//g' mis.txt | awk '{print length}' > a2
real    0m0.704s ; user 0m0.716s
=========================2 maxschlepzig
$ time tr -d -c 'a\n' < mis.txt | awk '{ print length; }' > a12
real    0m0.022s ; user 0m0.028s
=========================3 jjoao
$ time perl -nE 'say y!a!!' mis.txt  > a1
real    0m0.032s ; user 0m0.028s
=========================4 Stéphane Gimenez
$ function countchar(){while read -r i; do echo "$i"|tr -dc "$1"|wc -c; done }

$ time countchar "a"  < mis.txt > a3
real    0m27.990s ; user    0m3.132s
=========================5 Loki Astari
$ time awk -Fa '{print NF-1}' mis.txt > a4
real    0m0.064s ; user 0m0.060s
Error : several -1
=========================6 enzotib
$ time awk '{ gsub("[^a]", ""); print length }' mis.txt > a5
real    0m0.781s ; user 0m0.780s
=========================7 user606723
#include <stdio.h> #include <string.h> // int main(int argc, char *argv[]) ...  if(line) free(line); }

$ time a.out a < mis.txt > a6
real    0m0.024s ; user 0m0.020s
=========================8 maxschlepzig
#include <stdio.h> // int main(int argc, char **argv){if (argc < 2 || !*argv[1]) { ...  return 0; }

$ time a.out a < mis.txt > a7
real    0m0.028s ; user 0m0.024s
=========================9 Stéphane Chazelas
$ time awk '{print gsub(/a/, "")}'< mis.txt > a8
real    0m0.053s ; user 0m0.048s
=========================10 josephwb count total
$ time grep -o a < mis.txt | wc -w > a9
real    0m0.131s ; user 0m0.148s
=========================11 Kannan Mohan count total
$ time grep -o 'a' mis.txt | wc -l > a15
real    0m0.128s ; user 0m0.124s
=========================12 Kannan Mohan count total
$ time grep -oP 'a' mis.txt | wc -l > a16
real    0m0.047s ; user 0m0.044s
=========================13 josephwb Count total
$ time perl -ne '$x+=s/a//g; END {print "$x\n"}'< mis.txt > a10
real    0m0.051s ; user 0m0.048s
=========================14 heemayl
#!/usr/bin/env python2 // with open('mis.txt') as f: for line in f: print line.count('"')

$ time pyt > a11
real    0m0.052s ; user 0m0.052s
=========================15 enzotib
$ time  while IFS= read -r line; do   line="${line//[!a]/}"; echo "${#line}"; done < mis.txt  > a13
real    0m9.254s ; user 0m8.724s
=========================16 bleurp
$ time awk ' {print (split($0,a,"a")-1) }' mis.txt > a14
real    0m0.148s ; user 0m0.144s
Error several -1

1
grep -n -o \" file | sort -n | uniq -c | cut -d : -f 1

où grep fait le gros du travail: rapporte chaque caractère trouvé à chaque numéro de ligne. Le reste consiste simplement à additionner le nombre par ligne et à formater la sortie.

Supprimez le -net obtenez le nombre pour le fichier entier.

Compter un fichier texte 1,5Meg en moins de 0,015 seconde semble rapide.
Et fonctionne avec des caractères (pas des octets).


1

Une solution pour bash. Aucun programme externe appelé (plus rapide pour les chaînes courtes).

Si la valeur est dans une variable:

$ a='"Hello!"'

Cela imprimera combien "il contient:

$ b="${a//[^\"]}"; echo "${#b}"
2
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.