Jak policzyć liczbę określonego znaku w każdej linii?

87

Zastanawiałem się, jak policzyć liczbę określonego znaku w każdej linii przez niektóre narzędzia do przetwarzania tekstu?

Na przykład, aby policzyć "w każdym wierszu następującego tekstu

"hello!" 
Thank you!

Pierwszy wiersz ma dwa, a drugi wiersz ma 0.

Innym przykładem jest liczenie (w każdej linii.

Tim
źródło
1
Dodam tylko, że uzyskałeś znacznie wyższą wydajność, pisząc do tego własny program z 10 liniami C. Zamiast używać wyrażeń regularnych z sed. Powinieneś rozważyć zrobienie tego w zależności od rozmiaru plików wejściowych.
user606723,

Odpowiedzi:

104

Możesz to zrobić za pomocą sedi awk:

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

Gdzie datjest twój przykładowy tekst, sed usuwa (dla każdego wiersza) wszystkie "znaki niebędące znakami i awkdrukuje dla każdego wiersza o jego rozmiarze (tzn. lengthJest równoważne length($0), gdzie $0oznacza bieżącą linię).

W przypadku innej postaci wystarczy zmienić wyraz sed. Na przykład dla (:

's/[^(]//g'

Aktualizacja: sed to rodzaj przesady w zadaniu - trwystarczy. Równoważne rozwiązanie z tr:

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

Oznacza to, że trusuwa wszystkie znaki, które nie są ( -coznaczają uzupełnienie) w zestawie znaków "\n.

maxschlepzig
źródło
3
+1 powinno być bardziej wydajne niż wersja tr& wc.
Stéphane Gimenez
1
Tak, ale czy może obsłużyć Unicode?
amfetamachina
@amphetamachine, tak - przynajmniej szybkie testy z ß(UTF hex: c3 9f) (zamiast ") działa zgodnie z oczekiwaniami, to znaczy tr, seda awknie uzupełnienia / wymiany / liczenie bez problemu - na systemie Ubuntu 10.04.
maxschlepzig
1
Większość wersji tr, w tym GNU tr i klasyczny Unix tr, działa na znakach jednobajtowych i nie jest zgodnych z Unicode. Cytat z Wikipedii tr (Unix) .. Wypróbuj ten fragment: echo "aā⧾c" | tr "ā⧾" b... na Ubuntu 10.04 ... ßto jeden bajt Rozszerzony znak łaciński i jest obsługiwany przez tr... Prawdziwy problem nie polega na tym, trże nie obsługuje Unicode (ponieważ WSZYSTKIE znaki są Unicode), to tak naprawdę trobsługuje tylko jeden bajt na raz ...
Peter.O
@ fred, nie, ß nie jest jednobajtowym znakiem - jego pozycja Unicode to U + 00DF, która w UTF-8 jest kodowana jako „c3 9f”, tj. dwa bajty.
maxschlepzig
49

Po prostu użyłbym awk

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

Tutaj ustawiamy separator pól (z flagą -F) na znak, "a następnie drukujemy liczbę pól NF- 1. Liczba wystąpień znaku docelowego będzie o jeden mniejsza niż liczba oddzielnych pól.

W przypadku zabawnych postaci, które są interpretowane przez powłokę, musisz tylko upewnić się, że uciec przed nimi, w przeciwnym razie linia poleceń spróbuje je zinterpretować. Więc dla obu "i )musisz uciec z separatora pól (z \).

Martin York
źródło
1
Może edytuj swoją odpowiedź, aby zamiast ucieczki użyć cudzysłowów. Będzie działać z dowolną postacią (oprócz '). Ponadto ma dziwne zachowanie z pustymi liniami.
Stéphane Gimenez
Pytanie dotyczy konkretnie, "więc czuję się zobowiązany, aby kod działał z nim. To zależy od tego, z jakiej powłoki korzystasz przy pogodzie, postać musi być uciekła, ale zarówno bash / tcsh będą musiały uciec ”
Martin York
Oczywiście, ale nie ma z tym problemu -F'"'.
Stéphane Gimenez
+1 Co za dobry pomysł na użycie FS .... To rozwiąże pusty wiersz pokazujący -1, a na przykład „$ 1” z linii poleceń bash. ...awk -F"$1" '{print NF==0?NF:NF-1}' filename
Peter.O,
Pracuj także z wieloma znakami jako separatorem ... przydatne!
Cewka
14

Za pomocą trard wc:

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

Stosowanie:

$ 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
źródło
3
Uwaga. trnie obsługuje znaków, które używają więcej niż jednego bajtu .. patrz Wikipedia tr (Unix) .. tj. trnie jest zgodny z Unicode.
Peter.O,
Uruchomisz
musisz usunąć białe znaki $IFS, w przeciwnym razie readprzycinimy je od początku i na końcu.
Stéphane Chazelas
@ Peter.O, niektóre trimplementacje obsługują znaki wielobajtowe, ale i tak wc -cliczą bajty, a nie znaki (potrzeba wc -mznaków).
Stéphane Chazelas
11

Jeszcze inna realizacja, która nie opiera się na zewnętrznych programów, w bash, zsh, yasha niektóre implementacje / wersje ksh:

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

Użyj line="${line//[!(]}"do liczenia (.

enzotib
źródło
Gdy ostatni wiersz nie ma końca \ n, pętla while kończy działanie, ponieważ chociaż odczytuje ostatni wiersz, zwraca również niezerowy kod wyjścia wskazujący EOF ... aby go ominąć, następujący fragment kodu działa (.. Martwi mnie to od jakiegoś czasu, a właśnie odkryłem to obejście) ... eof=false; IFS=; until $eof; do read -r || eof=true; echo "$REPLY"; done
Peter.O
@Gilles: dodałeś trailing, /który nie jest potrzebny w bash. Czy to wymóg Ksh?
enzotib
1
Końcowe /jest potrzebne w starszych wersjach ksh, a IIRC również w starszych wersjach bash.
Gilles
10

Odpowiedzi przy użyciu awkniepowodzenia kończą się niepowodzeniem, jeśli liczba dopasowań jest zbyt duża (co dzieje się w mojej sytuacji). W przypadku odpowiedzi z loki-astari zgłaszany jest następujący błąd:

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

W przypadku odpowiedzi z enzotibu (i odpowiednika manatwork ) występuje błąd segmentacji:

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

sedRozwiązanie przez maxschlepzig działa poprawnie, ale jest powolne (czasy poniżej).

Niektóre rozwiązania nie zostały tu jeszcze zaproponowane. Po pierwsze, używając grep:

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

I używając perl:

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

Oto kilka harmonogramów kilku rozwiązań (uporządkowane od najwolniejszego do najszybszego); Tutaj ograniczyłem się do jednowarstwowych. „foo.txt” to plik z jedną linią i jednym długim łańcuchem, który zawiera 84922 dopasowań.

## 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
źródło
+ dobry pomysł! Rozszerzyłem twój stół, w nowej odpowiedzi, nie krępuj się edytować (ostateczne zdjęcie nie jest tak jasne, ale uważam, że @maxschlepzig to stal najszybszym rozwiązaniem)
JJoao
Rozwiązanie maxschlepzig jest super szybkie!
okwap
9

Inne awkrozwiązanie:

awk '{print gsub(/"/, "")}'
Stéphane Chazelas
źródło
8

Kolejna możliwa implementacja z awk i gsub:

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

Ta funkcja gsubjest odpowiednikiem sed 's///g'.

Użyj gsub("[^(]", "")do liczenia (.

enzotib
źródło
Możesz zapisać jedną postać, tj. Podczas usuwania przekierowania stdin ...;)
maxschlepzig
@maxschlepzig: tak, oczywiście;)
enzotib
1
awk '{print gsub(/"/,"")}' input-filebyłoby wystarczające, ponieważ „Dla każdego podłańcucha pasującego do wyrażenia regularnego r w ciągu t zastąp łańcuch s i zwróć liczbę podstawień”. (man awk)
manatwork
6

Postanowiłem napisać program w C, bo się nudziłem.

Prawdopodobnie powinieneś dodać sprawdzanie poprawności danych wejściowych, ale poza tym wszystko jest ustawione.

#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);
}
użytkownik606723
źródło
Dzięki! Dzięki, że się nudzę, żebym mógł się czegoś nauczyć. Och, czekaj, potrzebujesz zwrotu?
Tim
* wzrusza ramionami * , jeśli chcesz być w pełni poprawny, musisz także dodać jeszcze #include, ale domyślne ostrzeżenia w moim kompilatorze nie wydają się obchodzić.
user606723,
Możesz pominąć, free(line)ponieważ wyjście z programu domyślnie zwalnia całą przydzieloną pamięć - wtedy jest miejsce na return 0;...;). Nawet w przykładach pozostawienie kodu powrotu niezdefiniowanym nie jest dobrym stylem. Btw, getlineto rozszerzenie GNU - na wypadek, gdyby ktoś się zastanawiał.
maxschlepzig
@maxschlepzig: Czy pamięć jest wskazywana przez linię przydzieloną przez getline ()? Czy jest alokowany dynamicznie na stosie przez malloc czy statycznie na stosie? Powiedziałeś, że uwolnienie nie jest konieczne, więc nie jest przydzielane dynamicznie?
Tim
1
@Tim, tak, np. Jeśli zmienisz kod tak, że jest to samodzielna funkcja - powiedzmy - f, która jest wywoływana kilka razy z innego kodu, wtedy musisz zadzwonić freepo ostatnim wywołaniu getlinena końcu tej funkcji f.
maxschlepzig
6

W przypadku ciągu najprostsze byłoby z tri wc(nie trzeba przesadzać z awklub sed) - ale zwróć uwagę na powyższe komentarze na temat tr, liczy bajty, a nie znaki -

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

gdzie $xjest zmienną zawierającą ciąg (nie plik) do oceny.

Ocumo
źródło
4

Oto inne rozwiązanie C, które potrzebuje tylko STD C i mniej pamięci:

#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
źródło
To nie zostanie zgłoszone w ostatnim wierszu, jeśli nie ma końcowego „\ n”
Peter.O
1
@ fred, tak, co jest celowe, ponieważ linia bez końcowego \nnie jest prawdziwą linią. To jest to samo zachowanie, co w mojej innej odpowiedzi sed / awk (tr / awk).
maxschlepzig
3

Możemy użyć grepz, regexaby uczynić to prostszym i wydajniejszym.

Aby policzyć konkretny znak.

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

Aby policzyć znaki specjalne, w tym spacje.

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

Tutaj jesteśmy wybierając dowolny znak z [\S\s]iz -oopcją wykonujemy grepwydrukować każdą meczu (co jest, każdy znak) w osobnym wierszu. A następnie użyj, wc -laby policzyć każdą linię.

Kannan Mohan
źródło
OP nie chce wydrukować liczby wszystkich znaków w pliku! Chce policzyć / wydrukować numer określonej postaci. na przykład, ile "jest w każdej linii; i dla innych znaków. zobacz jego pytanie, a także zaakceptowaną odpowiedź.
αғsнιη
3

Być może bardziej bezpośrednią, czystą odpowiedzią byłoby użycie podziału. Podział pobiera ciąg i przekształca go w tablicę, zwracaną wartością jest liczba wygenerowanych elementów tablicy + 1.

Poniższy kod wypisze liczbę razy „w każdym wierszu”.

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

więcej informacji na temat podziału http://www.staff.science.uu.nl/~oostr102/docs/nawk/nawk_92.html

ślinić się
źródło
2

Oto prosty skrypt Pythona do znalezienia liczby "w każdym wierszu pliku:

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

Tutaj zastosowaliśmy countmetodę typu wbudowanego str.

heemayl
źródło
2

Dla czystego rozwiązania bash (jednak jest ono specyficzne dla bash): Jeśli $xzmienna zawiera Twój ciąg:

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

${x//Rzeczą usuwa wszystkie znaki z wyjątkiem ", ${#x2}oblicza długość tego odpoczynku.

(Oryginalna sugestia, z exprktórej korzysta problem, patrz komentarze:)

expr length "${x//[^\"]/}"
Marian
źródło
Zauważ, że jest specyficzny dla GNU expri liczy bajty, a nie znaki. Z innymi expr:expr "x${x...}" : "x.*" - 1
Stéphane Chazelas
No dobrze, dzięki! Zmodyfikowałem go, używając innego pomysłu, który właśnie miałem, który ma tę zaletę, że w ogóle nie używa zewnętrznego programu.
Marian
2

Zastąp aznak, który chcesz policzyć. Wyjście jest licznikiem dla każdej linii.

perl -nE 'say y!a!!'
JJoao
źródło
2

Porównanie czasowe prezentowanych rozwiązań (brak odpowiedzi)

Skuteczność odpowiedzi nie jest ważna. Niemniej jednak, postępując zgodnie z podejściem @josephwb, próbowałem ustalić czas na wszystkie przedstawione odpowiedzi.

Używam jako danych wejściowych portugalskiego tłumaczenia Victora Hugo „Les Miserables” (świetna książka!) I liczę wystąpienia „a”. Moje wydanie ma 5 tomów, wiele stron ...

$ wc miseraveis.txt 
29331  304166 1852674 miseraveis.txt 

Odpowiedzi C zostały skompilowane z gcc, (bez optymalizacji).

Każda odpowiedź została uruchomiona 3 razy i wybierz najlepszą.

Nie ufaj zbytnio tym liczbom (moja maszyna wykonuje inne zadania itp.). Dzielę się z Tobą tymi czasami, ponieważ otrzymałem nieoczekiwane wyniki i jestem pewien, że znajdziesz więcej ...

  • 14 z 16 rozwiązań czasowych zajęło mniej niż 1 sekundę; 9 mniej niż 0,1 s, wiele z nich używa rur
  • 2 rozwiązania wykorzystujące bash linia po linii przetworzyły 30 tys. Linii, tworząc nowe procesy, obliczając prawidłowe rozwiązanie w 10 s / 20 s.
  • grep -oP adrzewo jest wtedy szybsze grep -o a (10; 11 vs 12)
  • Różnica między C i innymi nie jest tak duża, jak się spodziewałem. (7; 8 vs 2; 3)
  • (wnioski mile widziane)

(wyniki w losowej kolejności)

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

gdzie grep wykonuje ciężkie podnoszenie: zgłasza każdy znak znaleziony przy każdym numerze linii. Reszta to po prostu zsumowanie liczby wierszy i sformatowanie wyniku.

Usuń -ni pobierz liczbę dla całego pliku.

Liczenie pliku tekstowego 1,5 Meg w czasie krótszym niż 0,015 sekundy wydaje się szybkie.
I działa ze znakami (nie bajtami).


źródło
1

Rozwiązanie na bash. Nie został wywołany program zewnętrzny (szybszy w przypadku krótkich ciągów znaków).

Jeśli wartość jest w zmiennej:

$ a='"Hello!"'

Spowoduje to wydrukowanie, ile "zawiera:

$ b="${a//[^\"]}"; echo "${#b}"
2
sorontar
źródło