Come contare il numero di un carattere specifico in ogni riga?


88

Mi chiedevo come contare il numero di un carattere specifico in ciascuna riga da parte di alcune utility di elaborazione del testo?

Ad esempio, per contare "in ogni riga del seguente testo

"hello!" 
Thank you!

La prima riga ha due e la seconda riga ha 0.

Un altro esempio è contare (in ogni riga.


1
Aggiungo solo che hai ricevuto prestazioni molto maggiori scrivendo il tuo programma da 10 linee C per questo piuttosto che usare espressioni regolari con sed. Si dovrebbe considerare di fare in base alla dimensione dei file di input.
user606723

Risposte:


105

Puoi farlo con sede awk:

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

Dov'è datil testo di esempio, sed elimina (per ogni riga) tutti i non "caratteri e awkstampa per ogni riga la sua dimensione (cioè lengthè equivalente a length($0), dove $0indica la riga corrente).

Per un altro personaggio devi solo cambiare l'espressione sed. Ad esempio per (:

's/[^(]//g'

Aggiornamento: sed è un po 'eccessivo per l'attività - trè sufficiente. Una soluzione equivalente con trè:

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

Significa che trcancella tutti i caratteri che non sono ( -csignifica complemento) nel set di caratteri "\n.


3
+1 dovrebbe essere più efficiente della versione tr& wc.
Stéphane Gimenez,

1
Sì, ma può gestire Unicode?
anfetamachina,

@amphetamachine, yes - almeno un test rapido con ß(utf hex: c3 9f) (invece di ") funziona come previsto, vale a dire tr, sede awkcompleta / sostituisce / conta senza problemi - su un sistema Ubuntu 10.04.
maxschlepzig,

1
La maggior parte delle versioni di tr, incluso GNU tr e Unix classico, funzionano su caratteri a byte singolo e non sono conformi a Unicode. Citato da Wikipedia tr (Unix) .. Prova questo frammento: echo "aā⧾c" | tr "ā⧾" b... su Ubuntu 10.04 ... ßè un singolo byte Carattere latino esteso ed è gestito da tr... Il vero problema qui non è che trnon gestisce Unicode (perché TUTTI i caratteri sono Unicode), è davvero che trgestisce solo un byte alla volta ..
Peter.O

@fred, no, ß non è un carattere a byte singolo - la sua posizione Unicode è U + 00DF, che è codificata come 'c3 9f' in UTF-8, cioè due byte.
maxschlepzig

50

Vorrei solo usare Awk

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

Qui impostiamo il separatore di campo (con l'indicatore -F) come carattere, "quindi tutto ciò che facciamo è stampare il numero di campi NF- 1. Il numero di occorrenze del carattere target sarà uno in meno del numero di campi separati.

Per i personaggi divertenti interpretati dalla shell devi solo assicurarti di sfuggirli altrimenti la riga di comando proverà ad interpretarli. Quindi per entrambi "e )devi fuggire dal separatore di campo (con \).


1
Forse modifica la tua risposta per usare le virgolette singole invece di scappare. Funzionerà con qualsiasi personaggio (tranne '). Inoltre, ha un comportamento strano con linee vuote.
Stéphane Gimenez,

La domanda utilizza in "modo specifico quindi mi sento obbligato a far funzionare il codice con esso. Dipende da quale shell stai usando il tempo per cui il personaggio deve essere evaso, ma bash / tcsh dovranno entrambi fuggire "
Martin York,

Certo, ma non ci sono problemi -F'"'.
Stéphane Gimenez,

+1 Che buona idea usare FS .... Questo risolverà la riga vuota che mostra -1 e, ad esempio, "$ 1" dalla riga di comando bash. ...awk -F"$1" '{print NF==0?NF:NF-1}' filename
Peter

Funziona anche con più caratteri come separatore ... utile!
Bobina

15

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

3
Nota. trnon gestisce i caratteri che usano più di un byte .. vedi Wikipedia tr (Unix) .. ie. trnon è conforme Unicode.
Peter


devi rimuovere i caratteri degli spazi bianchi $IFS, altrimenti readli taglieranno dall'inizio e dalla fine.
Stéphane Chazelas


@ Peter.O, alcune trimplementazioni supportano caratteri multibyte, ma wc -cconteggiano byte, non caratteri (necessità wc -mdi caratteri).
Stéphane Chazelas

11

Ancora un altro implementazione che non si basa su programmi esterni, in bash, zsh, yashe alcune implementazioni / versioni di ksh:

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

Utilizzare line="${line//[!(]}"per il conteggio (.


Quando l'ultima riga non ha un trailing \ n, il ciclo while termina, perché sebbene legga l'ultima riga, restituisce anche un codice di uscita diverso da zero per indicare EOF ... per aggirarlo, il seguente frammento funziona (... Mi ha infastidito per un po 'e ho appena scoperto questa soluzione alternativa) ... eof=false; IFS=; until $eof; do read -r || eof=true; echo "$REPLY"; done
Peter.O

@Gilles: hai aggiunto un trailing /che non è necessario in bash. È un requisito ksh?
enzotib,

1
Il trailing /è necessario nelle versioni precedenti di ksh e IIRC anche nelle versioni precedenti di bash.
Gilles,

10

Le risposte usando awkfalliscono se il numero di partite è troppo grande (che sembra essere la mia situazione). Per la risposta di loki-astari , viene riportato il seguente errore:

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

Per la risposta di enzotib (e l'equivalente di manatwork ), si verifica un errore di segmentazione:

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

La sedsoluzione di maxschlepzig funziona correttamente, ma è lenta (tempi sotto).

Alcune soluzioni non ancora suggerite qui. Innanzitutto, usando grep:

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

E usando perl:

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

Ecco alcuni tempi per alcune delle soluzioni (ordinate dal più lento al più veloce); Ho limitato le cose a una linea qui. 'foo.txt' è un file con una riga e una stringa lunga che contiene 84922 corrispondenze.

## 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

+ buona idea! Ho ampliato il tuo tavolo, in una nuova risposta, sentiti libero di modificare (l'immagine finale non è così chiara, ma credo che @maxschlepzig sia l'acciaio la soluzione più veloce)
JJoao

La soluzione di maxschlepzig è super veloce!
Okwap


8

Un'altra possibile implementazione con awk e gsub:

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

La funzione gsubè l'equivalente di sed 's///g'.

Utilizzare gsub("[^(]", "")per il conteggio (.


È possibile salvare un carattere, ad esempio quando si rimuove il reindirizzamento stdin ...;)
maxschlepzig

@maxschlepzig: sì, certo;)
enzotib,

1
awk '{print gsub(/"/,"")}' input-filesarebbe sufficiente, poiché "Per ogni sottostringa corrispondente all'espressione regolare r nella stringa t, sostituire la stringa s e restituire il numero di sostituzioni." (man awk)
arte

6

Ho deciso di scrivere un programma C perché ero annoiato.

Probabilmente dovresti aggiungere la convalida dell'input, ma a parte questo è tutto impostato.

#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);
}

Grazie! Grazie per esserti annoiato per poter imparare qualcosa. Oh aspetta, hai bisogno di un ritorno?
Tim

* scrolla le spalle * , se vuoi essere completamente corretto, devi anche aggiungere qualche altro #include, ma gli avvisi di default sul mio compilatore non sembrano preoccuparti.
user606723

Puoi tralasciare free(line)perché l'uscita dal programma libera implicitamente tutta la memoria allocata - quindi c'è posto per un return 0;...;). Anche negli esempi non è un buon stile lasciare indefinito il codice di ritorno. A proposito, getlineè un'estensione GNU - nel caso qualcuno si stia chiedendo.
maxschlepzig,

@maxschlepzig: la memoria è puntata per linea allocata da getline ()? È allocato dinamicamente su heap da malloc o staticamente su stack? Hai detto che liberarlo non è necessario, quindi non è allocato dinamicamente?
Tim

1
@Tim, sì, ad es. Se si esegue il refactoring del codice in modo che sia una funzione autonoma, ad esempio f, che viene chiamata più volte da un altro codice, è necessario chiamare freedopo l'ultima chiamata di getlineal termine di questa funzione f.
maxschlepzig,

6

Per una stringa, il più semplice sarebbe con tre wc(non è necessario eseguire l'overkill con awko sed) - ma annota i commenti sopra tr, conta byte, non caratteri -

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

dove si $xtrova la variabile che contiene la stringa (non un file) da valutare.


4

Ecco un'altra soluzione C che richiede solo STD C e meno 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;
}

Questo non riporterà sull'ultima riga se non ha un finale '\ n'
Peter.O

1
@fred, sì, che è apposta, perché una linea senza trascinamento \nnon è una linea reale. Questo è lo stesso comportamento della mia altra risposta sed / awk (tr / awk).
maxschlepzig

3

Possiamo usarlo grepcon regexper renderlo più semplice e potente.

Per contare un personaggio specifico.

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

Per contare i caratteri speciali inclusi i caratteri degli spazi bianchi.

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

Qui stiamo selezionando qualsiasi personaggio con [\S\s]e con l' -oopzione facciamo grepper stampare ogni corrispondenza (che è, ogni personaggio) in una riga separata. E poi usa wc -lper contare ogni riga.


OP non vuole stampare il numero di tutti i caratteri in un file! Vuole contare / stampare il numero di un personaggio specifico. per esempio quanti ce ne "sono in ciascuna riga; e per qualsiasi altro carattere. vedere la sua domanda e anche la risposta accettata.
αғsнιη,

3

Forse una risposta più semplice, semplicemente imbarazzante sarebbe quella di usare la divisione. La divisione prende una stringa e la trasforma in un array, il valore restituito è il numero di elementi dell'array generati + 1.

Il seguente codice stamperà il numero di volte "appare su ogni riga.

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

maggiori informazioni su split http://www.staff.science.uu.nl/~oostr102/docs/nawk/nawk_92.html


2

Ecco un semplice script Python per trovare il conteggio "in ogni riga di un file:

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

Qui abbiamo usato il countmetodo del tipo incorporato str.


2

Per una soluzione bash pura (tuttavia, è specifica per bash): Se $xè la variabile che contiene la stringa:

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

La ${x//cosa rimuove tutti i caratteri tranne ", ${#x2}calcola la lunghezza di questo riposo.

(Suggerimento originale usando exprquale ha problemi, vedi commenti:)

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

Nota che è specifico per GNU expre conta i byte, non i caratteri. Con altri expr:expr "x${x...}" : "x.*" - 1
Stéphane Chazelas il

Oh giusto, grazie! L'ho modificato usando un'altra idea che avevo appena avuto, che ha il vantaggio di non usare affatto un programma esterno.
Marian

2

Sostituisci acon il carattere da contare. L'output è il contatore per ogni riga.

perl -nE 'say y!a!!'

2

Confronto temporale delle soluzioni presentate (non una risposta)

L'efficienza delle risposte non è importante. Tuttavia, seguendo l'approccio di @josephwb, ho cercato di valutare tutte le risposte presentate.

Uso come input la traduzione portoghese di Victor Hugo "Les Miserables" (ottimo libro!) E conto le occorrenze di "a". La mia edizione ha 5 volumi, molte pagine ...

$ wc miseraveis.txt 
29331  304166 1852674 miseraveis.txt 

Le risposte C sono state compilate con gcc, (nessuna ottimizzazione).

Ogni risposta è stata eseguita 3 volte e scegli la migliore.

Non fidarti troppo di questi numeri (la mia macchina sta svolgendo altre attività, ecc., Ecc.). Condivido questi momenti con te, perché ho ottenuto risultati inaspettati e sono sicuro che ne troverai altri ...

  • 14 di 16 soluzioni a tempo impiegavano meno di 1 secondo; 9 in meno di 0,1 secondi, molti dei quali utilizzano tubi
  • 2 soluzioni, usando bash riga per riga, hanno elaborato le linee 30k creando nuovi processi, calcolando la soluzione corretta in 10s / 20s.
  • grep -oP ai tempi degli alberi sono più veloci di allora grep -o a (10; 11 contro 12)
  • La differenza tra C e gli altri non è così grande come mi aspettavo. (7; 8 vs 2; 3)
  • (conclusioni benvenute)

(risulta in un ordine casuale)

=========================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

dove grep fa tutto il sollevamento pesante: riporta ogni personaggio trovato in ogni numero di riga. Il resto è solo quello di sommare il conteggio per riga e formattare l'output.

Rimuovere il -ne ottenere il conteggio per l'intero file.

Il conteggio di un file di testo 1.5Meg in meno di 0,015 secondi sembra veloce.
E funziona con caratteri (non byte).


1

Una soluzione per bash. Nessun programma esterno chiamato (più veloce per stringhe brevi).

Se il valore è in una variabile:

$ a='"Hello!"'

Questo stamperà quanti "ne contiene:

$ b="${a//[^\"]}"; echo "${#b}"
2
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.