¿Cómo contar el número de un personaje específico en cada línea?

87

Me preguntaba cómo contar el número de un carácter específico en cada línea por algunas utilidades de procesamiento de texto.

Por ejemplo, para contar "en cada línea del siguiente texto

"hello!" 
Thank you!

La primera línea tiene dos y la segunda línea tiene 0.

Otro ejemplo es contar (en cada línea.

Tim
fuente
1
Solo agregaré que recibió un rendimiento mucho mayor al escribir su propio programa de 10 líneas C para esto en lugar de usar expresiones regulares con sed. Debería considerar hacerlo dependiendo del tamaño de sus archivos de entrada.
user606723

Respuestas:

104

Puedes hacerlo con sedy awk:

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

¿Dónde datestá su texto de ejemplo? Sed elimina (para cada línea) todos los no "caracteres e awkimprime para cada línea su tamaño ( lengthes decir, es equivalente a length($0), donde $0denota la línea actual).

Para otro personaje solo tienes que cambiar la expresión sed. Por ejemplo para (:

's/[^(]//g'

Actualización: sed es una especie de exageración para la tarea, tres suficiente. Una solución equivalente con tres:

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

Lo que significa que trelimina todos los caracteres que no están ( -csignifica complemento) en el conjunto de caracteres "\n.

maxschlepzig
fuente
3
+1 debería ser más eficiente que la versión tr& wc.
Stéphane Gimenez
1
Sí, pero ¿puede manejar Unicode?
anfetamaquina
@amphetamachine, sí, al menos una prueba rápida con ß(utf hex: c3 9f) (en lugar de ") funciona como se esperaba, es decir tr, sedy awkcomplementa / reemplaza / cuenta sin problemas, en un sistema Ubuntu 10.04.
maxschlepzig
1
La mayoría de las versiones tr, incluidas GNU tr y Unix tr clásico, funcionan con caracteres de un solo byte y no son compatibles con Unicode. Citado de Wikipedia tr (Unix) . Pruebe este fragmento: echo "aā⧾c" | tr "ā⧾" b... en Ubuntu 10.04 ... ßes un solo byte El carácter latino extendido y es manejado por tr... El verdadero problema aquí no es que trno maneja Unicode (porque TODOS los caracteres son Unicode), es realmente que trsolo maneja un byte a la vez ...
Peter.O
@fred, no, ß no es un carácter de un solo byte: su posición Unicode es U + 00DF, que se codifica como 'c3 9f' en UTF-8, es decir, dos bytes.
maxschlepzig
49

Yo solo usaría awk

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

Aquí configuramos el separador de campo (con el indicador -F) para que sea el carácter, "entonces todo lo que hacemos es imprimir el número de campos NF- 1. El número de ocurrencias del carácter de destino será uno menos que el número de campos separados.

Para los personajes divertidos que son interpretados por el shell, solo necesita asegurarse de escapar de ellos; de lo contrario, la línea de comandos intentará interpretarlos. Por lo tanto "y )tiene que escapar el separador de campo (con \).

Martin York
fuente
1
Tal vez edite su respuesta para usar comillas simples en lugar de escapar. Funcionará con cualquier personaje (excepto '). Además, tiene un comportamiento extraño con líneas vacías.
Stéphane Gimenez
La pregunta se usa específicamente, "así que me siento obligado a hacer que el código funcione con él. Depende del tipo de capa que esté utilizando, el personaje necesita escapar, pero bash / tcsh necesitará escapar "
Martin York
Por supuesto, pero no hay problema con -F'"'.
Stéphane Gimenez
+1 Qué buena idea usar FS ... Esto resolverá la línea en blanco que muestra -1 y, por ejemplo, el "$ 1" de la línea de comando bash. ...awk -F"$1" '{print NF==0?NF:NF-1}' filename
Peter.O
También trabaje con múltiples caracteres como separador ... ¡útil!
Bobina
14

Usando trard wc:

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

Uso:

$ 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
Stéphane Gimenez
fuente
3
Nota. trno maneja caracteres que usan más de un byte ... ver Wikipedia tr (Unix) ... es decir. trno es compatible con Unicode.
Peter.O
debe eliminar los caracteres de espacios en blanco $IFS, de lo contrario readlos recortará desde el principio y el final.
Stéphane Chazelas
no se puede usar echopara datos arbitrarios
Stéphane Chazelas
@ Peter.O, algunas trimplementaciones admiten caracteres multibyte, pero wc -ccuentan bytes, no caracteres de todos modos (se necesitan wc -mcaracteres).
Stéphane Chazelas
11

Sin embargo, otra aplicación que no se basa en programas externos, en bash, zsh, yashy algunas implementaciones / versiones de ksh:

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

Úselo line="${line//[!(]}"para contar (.

enzotib
fuente
Cuando la última línea no tiene un \ n final, el ciclo while sale, porque aunque lee la última línea, también devuelve un código de salida distinto de cero para indicar EOF ... para evitarlo, el siguiente fragmento funciona (... Me ha estado molestando por un tiempo, y acabo de descubrir esta solución) ... eof=false; IFS=; until $eof; do read -r || eof=true; echo "$REPLY"; done
Peter.O
@Gilles: agregó un final /que no es necesario en bash. Es un requisito ksh?
enzotib
1
El seguimiento /es necesario en versiones anteriores de ksh, y IIRC también en versiones anteriores de bash.
Gilles
10

Las respuestas que usan awkfallan si el número de coincidencias es demasiado grande (que es mi situación). Para la respuesta de loki-astari , se informa el siguiente error:

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

Para la respuesta de enzotib (y el equivalente de manatwork ), se produce una falla de segmentación:

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

La sedsolución de maxschlepzig funciona correctamente, pero es lenta (tiempos a continuación).

Algunas soluciones aún no sugeridas aquí. Primero, usando grep:

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

Y usando perl:

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

Aquí hay algunos tiempos para algunas de las soluciones (ordenadas de la más lenta a la más rápida); Limité las cosas a frases sencillas aquí. 'foo.txt' es un archivo con una línea y una cadena larga que contiene 84922 coincidencias.

## 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
josephwb
fuente
+ buena idea! Expandí su tabla, en una nueva respuesta, siéntase libre de editar (la imagen final no es tan clara, pero creo que @maxschlepzig es el acero la solución más rápida)
JJoao
¡La solución de maxschlepzig es súper rápida!
okwap
9

Otra awksolución:

awk '{print gsub(/"/, "")}'
Stéphane Chazelas
fuente
8

Otra posible implementación con awk y gsub:

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

La función gsubes el equivalente de sed 's///g'.

Úselo gsub("[^(]", "")para contar (.

enzotib
fuente
Puede guardar un carácter, es decir, al eliminar la redirección stdin ...;)
maxschlepzig
@maxschlepzig: sí, por supuesto;)
enzotib
1
awk '{print gsub(/"/,"")}' input-filesería suficiente, como "Para cada subcadena que coincida con la expresión regular r en la cadena t, sustituya la cadena s y devuelva el número de sustituciones". (man awk)
manatwork
6

Decidí escribir un programa en C porque estaba aburrido.

Probablemente debería agregar validación de entrada, pero aparte de eso, todo está configurado.

#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);
}
usuario606723
fuente
¡Gracias! Gracias por aburrirme para poder aprender algo. Oh espera, ¿necesitas una devolución?
Tim
* se encoge de hombros * , si quieres estar completamente correcto, también debes agregar algunos # más incluidos, pero las advertencias predeterminadas en mi compilador no parecen importarle.
user606723
Puede omitir el free(line)porque salir del programa implícitamente libera toda la memoria asignada, entonces hay lugar para un return 0;...;). Incluso en los ejemplos, no es un buen estilo dejar el código de retorno sin definir. Por cierto, getlinees una extensión GNU, en caso de que alguien se pregunte.
maxschlepzig
@maxschlepzig: ¿La memoria apunta a la línea asignada por getline ()? ¿Se asigna dinámicamente en el montón por malloc o estáticamente en la pila? Dijiste que liberarlo no es necesario, entonces, ¿no se asigna dinámicamente?
Tim
1
@Tim, sí, por ejemplo, si refactoriza el código de manera que sea una función independiente, digamos f, que se llama varias veces desde otro código, debe llamar freedespués de la última llamada getlineal final de esta función f.
maxschlepzig
6

Para una cadena, lo más simple sería con try wc(no es necesario exagerar con awko sed), pero tenga en cuenta los comentarios anteriores sobre tr, cuenta bytes, no caracteres -

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

donde $xes la variable que contiene la cadena (no un archivo) para evaluar.

Ocumo
fuente
4

Aquí hay otra solución C que solo necesita STD C y menos memoria:

#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;
}
maxschlepzig
fuente
Esto no informará en la última línea si no tiene un '\ n'
final
1
@fred, sí, lo cual es a propósito, porque una línea sin un final \nno es una línea real. Este es el mismo comportamiento que con mi otra respuesta sed / awk (tr / awk).
maxschlepzig
3

Podemos utilizar grepcon regexpara que sea más sencillo y potente.

Para contar un personaje específico.

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

Para contar caracteres especiales, incluidos los espacios en blanco.

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

Aquí estamos seleccionando cualquier carácter con [\S\s]y con la -oopción que hagamos greppara imprimir cada coincidencia (es decir, cada carácter) en una línea separada. Y luego use wc -lpara contar cada línea.

Kannan Mohan
fuente
¡OP no quiere imprimir el número de todos los caracteres en un archivo! Quiere contar / imprimir el número de un personaje específico. por ejemplo, cuántos "hay en cada línea; y para cualquier otro personaje. vea su pregunta y también aceptó la respuesta.
αғsнιη
3

Tal vez una respuesta más directa y puramente mala sería usar split. Split toma una cadena y la convierte en una matriz, el valor de retorno es el número de elementos de la matriz generados + 1.

El siguiente código imprimirá el número de veces "aparece en cada línea.

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

más información sobre split http://www.staff.science.uu.nl/~oostr102/docs/nawk/nawk_92.html

bleurp
fuente
2

Aquí hay un script simple de Python para encontrar el recuento "en cada línea de un archivo:

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

Aquí hemos utilizado el countmétodo de tipo incorporado str.

heemayl
fuente
2

Para una solución bash pura (sin embargo, es específica de bash): If $xes la variable que contiene su cadena:

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

La ${x//cosa elimina todos los caracteres excepto ", ${#x2}calcula la duración de este descanso.

(Sugerencia original usando la exprque tiene problemas, ver comentarios:)

expr length "${x//[^\"]/}"
Mariana
fuente
Tenga en cuenta que es específico de GNU expry cuenta bytes, no caracteres. Con otros expr:expr "x${x...}" : "x.*" - 1
Stéphane Chazelas
Oh cierto, gracias! Lo modifiqué usando otra idea que acabo de tener, que tiene la ventaja de no usar ningún programa externo.
Marian
2

Reemplazar apor el carácter a contar. La salida es el contador para cada línea.

perl -nE 'say y!a!!'
JJoao
fuente
2

Comparación de tiempos de las soluciones presentadas (no es una respuesta)

La eficiencia de las respuestas no es importante. Sin embargo, siguiendo el enfoque de @josephwb, intenté cronometrar todas las respuestas presentadas.

Utilizo como entrada la traducción portuguesa de Victor Hugo "Les Miserables" (¡gran libro!) Y cuento las ocurrencias de "a". Mi edición tiene 5 volúmenes, muchas páginas ...

$ wc miseraveis.txt 
29331  304166 1852674 miseraveis.txt 

Las respuestas C se compilaron con gcc (sin optimizaciones).

Cada respuesta se ejecutó 3 veces y elige la mejor.

No confíe demasiado en estos números (mi máquina está haciendo otras tareas, etc., etc.). Comparto estos momentos con ustedes, porque obtuve algunos resultados inesperados y estoy seguro de que encontrarán más ...

  • 14 de las 16 soluciones cronometradas tomaron menos de 1 s; 9 menos de 0.1s, muchos de ellos usando tuberías
  • 2 soluciones, usando bash línea por línea, procesaron las 30k líneas creando nuevos procesos, calculan la solución correcta en 10s / 20s.
  • grep -oP aes el árbol veces más rápido que grep -o a (10; 11 vs 12)
  • La diferencia entre C y otros no es tan grande como esperaba. (7; 8 contra 2; 3)
  • (conclusiones bienvenidas)

(resultados en un orden aleatorio)

=========================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
JJoao
fuente
1
grep -n -o \" file | sort -n | uniq -c | cut -d : -f 1

donde grep hace todo el trabajo pesado: informa cada carácter encontrado en cada número de línea. El resto es solo para sumar el recuento por línea y formatear la salida.

Elimine -ny obtenga el recuento de todo el archivo.

Contar un archivo de texto de 1.5Meg en menos de 0.015 segundos parece rápido.
Y funciona con caracteres (no bytes).


fuente
1

Una solución para bash. No se llama a un programa externo (más rápido para cadenas cortas).

Si el valor está en una variable:

$ a='"Hello!"'

Esto imprimirá cuántos "contiene:

$ b="${a//[^\"]}"; echo "${#b}"
2
sorontar
fuente