Como contar o número de um caractere específico em cada linha?

87

Eu queria saber como contar o número de um caractere específico em cada linha por alguns utilitários de processamento de texto?

Por exemplo, para contar "em cada linha do texto a seguir

"hello!" 
Thank you!

A primeira linha tem dois e a segunda linha tem 0.

Outro exemplo é contar (em cada linha.

Tim
fonte
1
Basta acrescentar que você recebeu muito mais desempenho escrevendo seu próprio programa C de 10 linhas para isso, em vez de usar expressões regulares com o sed. Você deve fazer isso dependendo do tamanho dos seus arquivos de entrada.
user606723

Respostas:

104

Você pode fazer isso com sede awk:

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

Onde datestá o texto de exemplo, sed exclui (para cada linha) todos os não "caracteres e awkimprime para cada linha seu tamanho (ou seja, lengthé equivalente a length($0), onde $0denota a linha atual).

Para outro personagem, você apenas precisa alterar a expressão sed. Por exemplo, para (:

's/[^(]//g'

Atualização: sed é um exagero para a tarefa - tré suficiente. Uma solução equivalente com tré:

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

Significa que trexclui todos os caracteres que não estão ( -csignifica complemento) no conjunto de caracteres "\n.

maxschlepzig
fonte
3
O +1 deve ser mais eficiente que a versão tr& wc.
Stéphane Gimenez
1
Sim, mas ele pode lidar com Unicode?
Amphetamachine
@ amphetamachine, sim - pelo menos um teste rápido com ß(utf hex: c3 9f) (em vez de ") funciona conforme o esperado, ou seja tr, sede awkcomplementa / substitui / conta sem problemas - em um sistema Ubuntu 10.04.
maxschlepzig
1
A maioria das versões tr, incluindo GNU tr e clássico Unix tr, opera com caracteres de byte único e não é compatível com Unicode. Citado em Wikipedia tr (Unix) . Experimente este trecho: echo "aā⧾c" | tr "ā⧾" b... no Ubuntu 10.04 ... ßé um byte único Caractere latino estendido e é tratado por tr... O verdadeiro problema aqui não é que trnão lida com Unicode (porque TODOS os caracteres são Unicode), é realmente que trlida apenas com um byte de cada vez ..
Peter.O
@fred, no, ß não é um caractere de byte único - sua posição Unicode é U + 00DF, que é codificada como 'c3 9f' em UTF-8, ou seja, dois bytes.
maxschlepzig
49

Eu usaria apenas awk

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

Aqui, definimos o separador de campos (com o sinalizador -F) como o caractere; "tudo o que fazemos é imprimir o número de campos NF- 1. O número de ocorrências do caractere de destino será um a menos que o número de campos separados.

Para personagens engraçados que são interpretados pelo shell, você só precisa escapar deles, caso contrário a linha de comando tentará interpretá-los. Portanto, para ambos "e )você precisa escapar do separador de campos (com \).

Martin York
fonte
1
Talvez edite sua resposta para usar aspas simples para escapar. Funcionará com qualquer caractere (exceto '). Além disso, ele tem um comportamento estranho com linhas vazias.
Stéphane Gimenez
A pergunta usa especificamente, "então me sinto obrigado a fazer o código funcionar com ele. Depende do que desembolsar você estiver usando o tempo o personagem precisa ser escapado, mas bash / tcsh vai tanto precisa escapar "
Martin Iorque
Claro, mas não há nenhum problema com -F'"'.
Stéphane Gimenez
+1 Que boa idéia usar o FS .... Isso resolverá a linha em branco mostrando -1 e, por exemplo, o "$ 1" na linha de comando do bash. ...awk -F"$1" '{print NF==0?NF:NF-1}' filename
Peter.O
Também trabalhe com vários 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
fonte
3
Nota. trnão lida com caracteres que usam mais de um byte. veja Wikipedia tr (Unix) . trnão é compatível com Unicode.
precisa saber é o seguinte
você precisa remover os caracteres de espaço em branco $IFS, caso contrário, readirá apará-los do início e do fim.
Stéphane Chazelas
você não pode usar echopara dados arbitrários
Stéphane Chazelas 3/15
@ Peter.O, algumas trimplementações suportam caracteres multibyte, mas wc -ccontam bytes, e não caracteres de qualquer maneira (necessidade wc -mde caracteres).
Stéphane Chazelas
11

No entanto, outra aplicação que não depende de programas externos, em bash, zsh, yashe algumas implementações / versões de ksh:

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

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

enzotib
fonte
Quando a última linha não possui um \ n final, o loop while é encerrado porque, apesar de ler a última linha, ele também retorna um código de saída diferente de zero para indicar o EOF ... para contorná-lo, o seguinte snippet funciona (..É foi me incomodando há algum tempo, e eu só descobri este workaroung) ... eof=false; IFS=; until $eof; do read -r || eof=true; echo "$REPLY"; done
Peter.O
@ Gilles: você adicionou uma trilha /que não é necessária no bash. É um requisito ksh?
enzotib
1
O rastreio /é necessário nas versões mais antigas do ksh e no IIRC nas versões mais antigas do bash.
Gilles
10

As respostas que usam awkfalham se o número de correspondências for muito grande (que é a minha situação). Para a resposta de loki-astari , o seguinte erro é relatado:

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 a resposta do enzotib (e o equivalente do manatwork ), ocorre uma falha de segmentação:

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

A sedsolução de maxschlepzig funciona corretamente, mas é lenta (intervalos abaixo).

Algumas soluções ainda não sugeridas aqui. Primeiro, usando grep:

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

E usando perl:

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

Aqui estão alguns horários para algumas das soluções (ordenadas do mais lento para o mais rápido); Limitei as coisas a one-liners aqui. 'foo.txt' é um arquivo com uma linha e uma sequência longa que contém 84922 correspondências.

## 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
fonte
+ boa ideia! I expandiu sua mesa, em uma nova resposta, fique à vontade para editar (a imagem final não é tão clara, mas acredito @maxschlepzig é de aço a solução mais rápida)
JJoao
A solução do maxschlepzig é super rápida!
Okwap #
9

Outra awksolução:

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

Outra implementação possível com o awk e o gsub:

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

A função gsubé equivalente a sed's 's///g'.

Use gsub("[^(]", "")para contar (.

enzotib
fonte
Você pode salvar um personagem, ou seja, ao remover o redirecionamento stdin ...;)
maxschlepzig
@maxschlepzig: sim, é claro;)
enzotib
1
awk '{print gsub(/"/,"")}' input-fileseria suficiente, como "Para cada substring que corresponda à expressão regular r na string t, substitua a string s e retorne o número de substituições". (man awk)
manatwork
6

Decidi escrever um programa em C porque estava entediado.

Você provavelmente deve adicionar validação de entrada, mas fora isso está tudo pronto.

#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);
}
user606723
fonte
Obrigado! Obrigado por estar entediado para que eu possa aprender alguma coisa. Oh espere, você precisa de um retorno?
Tim
* encolhe os ombros * , se você quiser estar totalmente correto, também precisará adicionar mais alguns #includes, mas os avisos padrão no meu compilador não parecem se importar.
user606723
Você pode deixar de fora free(line)porque o fato de sair do programa libera implicitamente toda a memória alocada - então há lugar para um return 0;...;). Mesmo em exemplos, não é bom estilo deixar o código de retorno indefinido. Aliás, getlineé uma extensão GNU - caso alguém esteja se perguntando.
maxschlepzig
@maxschlepzig: a memória é apontada por linha alocada por getline ()? Ele é alocado dinamicamente no heap por malloc ou estaticamente na pilha? Você disse que liberar não é necessário, então não é alocado dinamicamente?
Tim
1
@ Tim, sim, por exemplo, se você refatorar o código de forma que seja uma função autônoma - digamos - f, que é chamada várias vezes de outro código, será necessário chamar freeapós a última chamada getlineno final desta função f.
maxschlepzig
6

Para uma string, o mais simples seria com tre wc(não é necessário exagerar com awkou sed) - mas observe os comentários acima sobre tr, conta bytes, não caracteres -

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

onde $xé a variável que contém a sequência (não um arquivo) a ser avaliada.

Ocumo
fonte
4

Aqui está outra solução C que precisa apenas de STD C e menos memória:

#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
fonte
Isso não será relatado na última linha se não houver um '\ n' à
direita
1
@ fred, sim, que é de propósito, porque uma linha sem um final \nnão é uma linha real. Esse é o mesmo comportamento da minha outra resposta sed / awk (tr / awk).
maxschlepzig
3

Podemos usar grepcom regexpara torná-lo mais simples e poderoso.

Contar caracteres específicos.

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

Para contar caracteres especiais, incluindo caracteres de espaço em branco.

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

Aqui, estamos selecionando qualquer caractere com [\S\s]e com a -oopção que fazemos greppara imprimir cada correspondência (ou seja, cada caractere) em uma linha separada. E então use wc -lpara contar cada linha.

Kannan Mohan
fonte
O OP não deseja imprimir o número de todos os caracteres em um arquivo! Ele quer contar / imprimir o número de um caractere específico. por exemplo, quantos "estão em cada linha; e para quaisquer outros caracteres. veja sua pergunta e também aceite a resposta.
αғsнιη
3

Talvez uma resposta mais direta e puramente estranha seria usar split. Split pega uma string e a transforma em uma matriz, o valor de retorno é o número de itens da matriz gerados + 1.

O código a seguir imprimirá o número de vezes "aparece em cada linha.

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

mais informações sobre http://www.staff.science.uu.nl/~oostr102/docs/nawk/nawk_92.html

bleurp
fonte
2

Aqui está um script Python simples para encontrar a contagem de "em cada linha de um arquivo:

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

Aqui nós usamos o countmétodo do strtipo interno.

heemail
fonte
2

Para uma solução de bash pura (no entanto, é específica do bash): If $xé a variável que contém sua string:

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

A ${x//coisa remove todos os caracteres ", exceto , ${#x2}calcula a duração desse descanso.

(Sugestão original usando exprproblemas, consulte os comentários:)

expr length "${x//[^\"]/}"
Marian
fonte
Note que é específico ao GNU expre conta bytes, não caracteres. Com outros expr:expr "x${x...}" : "x.*" - 1
Stéphane Chazelas
Oh certo, obrigado! Eu o modifiquei usando outra ideia que acabei de ter, que tem a vantagem de não usar um programa externo.
Marian
2

Substitua apelo caractere a ser contado. Saída é o contador para cada linha.

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

Comparação temporal das soluções apresentadas (não uma resposta)

A eficiência das respostas não é importante. No entanto, seguindo a abordagem @josephwb, tentei cronometrar todas as respostas apresentadas.

Utilizo como entrada a tradução em português de Victor Hugo "Les Miserables" (ótimo livro!) E conto as ocorrências de "a". Minha edição tem 5 volumes, muitas páginas ...

$ wc miseraveis.txt 
29331  304166 1852674 miseraveis.txt 

As respostas em C foram compiladas com o gcc (sem otimizações).

Cada resposta foi executada 3 vezes e escolha a melhor.

Não confie demais nesses números (minha máquina está realizando outras tarefas, etc.). Partilho esses momentos com você, porque obtive resultados inesperados e tenho certeza de que encontrará mais ...

  • 14 de 16 soluções temporizadas levaram menos de 1s; 9 menos que 0,1s, muitos deles usando canos
  • 2 soluções, usando bash linha por linha, processaram as linhas de 30k criando novos processos, calcule a solução correta em 10s / 20s.
  • grep -oP aé o tempo da árvore mais rápido que grep -o a (10; 11 vs 12)
  • A diferença entre C e outros não é tão grande quanto eu esperava. (7; 8 vs 2; 3)
  • (conclusões bem-vindas)

(resulta em uma ordem aleatória)

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

onde grep faz todo o trabalho pesado: relata cada caractere encontrado em cada número de linha. O resto é apenas para somar a contagem por linha e formatar a saída.

Remova o -ne obtenha a contagem para o arquivo inteiro.

Contar um arquivo de texto de 1,5Meg em menos de 0,015 segundos parece rápido.
E funciona com caracteres (não bytes).


fonte
1

Uma solução para o bash. Nenhum programa externo é chamado (mais rápido para cadeias curtas).

Se o valor estiver em uma variável:

$ a='"Hello!"'

Isso imprimirá quantas "contém:

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