Grep od końca pliku do początku

38

Mam plik z około 30 000 000 wierszy (Radius Accounting) i muszę znaleźć ostatnie dopasowanie dla danego wzorca.

Komenda:

tac accounting.log | grep $pattern

daje to, czego potrzebuję, ale jest zbyt wolne, ponieważ system operacyjny musi najpierw odczytać cały plik, a następnie wysłać go do potoku.

Potrzebuję więc czegoś szybkiego, co umożliwi odczytanie pliku od ostatniej linii do pierwszej.

Hábner Costa
źródło

Odpowiedzi:

44

tacpomaga tylko, jeśli użyjesz grep -m 1(zakładając GNU grep), aby grepzatrzymać po pierwszym dopasowaniu:

tac accounting.log | grep -m 1 foo

Od man grep:

   -m NUM, --max-count=NUM
          Stop reading a file after NUM matching lines.  

W przykładzie w twoim pytaniu jedno taci drugie grepmusi przetworzyć cały plik, więc używanie tacjest w pewnym sensie bezcelowe.

Tak więc, chyba że użyjesz grep -m, nie używaj tacw ogóle, po prostu przeanalizuj wynik, grepaby uzyskać ostatnie dopasowanie:

grep foo accounting.log | tail -n 1 

Innym podejściem byłoby użycie Perla lub innego języka skryptowego. Na przykład (gdzie $pattern=foo):

perl -ne '$l=$_ if /foo/; END{print $l}' file

lub

awk '/foo/{k=$0}END{print k}' file
terdon
źródło
1
Używam tac, ponieważ muszę znaleźć ostatnie dopasowanie dla danego wzorca. Używając Twojej sugestii „grep -m1” czas wykonania wynosi od 0m0,597s do 0m0,007s \ o /. Dzięki wszystkim!
Hábner Costa
1
@ HábnerCosta jesteś bardzo mile widziany. Rozumiem, dlaczego używasz tac, chodziło mi o to, że to nie pomaga, chyba że używasz również, -mponieważ plik musi być w całości odczytany przez dwa programy. W przeciwnym razie możesz po prostu wyszukać wszystkie wystąpienia i zachować tylko ostatnie, tak jak ja tail -n 1.
terdon
6
Dlaczego mówisz „tac [...] musi przetworzyć cały plik”? Pierwszą rzeczą, którą robi tac, jest szukanie końca pliku i odczytywanie bloku od końca. Możesz to sprawdzić samodzielnie za pomocą strace (1). W połączeniu z grep -mpowinien być dość wydajny.
camh
1
@camh w połączeniu z grep -mnim jest. OP nie korzystał, -mwięc zarówno grep, jak i tac przetwarzały całość.
terdon
Czy możesz rozwinąć znaczenie awklinii?
Sopalajo de Arrierez
12

Powód dlaczego

tac file | grep foo | head -n 1

nie kończy się przy pierwszym meczu z powodu buforowania.

Zwykle head -n 1kończy się po przeczytaniu linii. Więc greppowinien dostać SIGPIPE i wyjść tak szybko, jak tylko napisze drugą linię.

Ale dzieje się tak, ponieważ ponieważ jego wyjście nie trafia do terminala, grepbuforuje je. Oznacza to, że nie pisze tego, dopóki nie zgromadzi wystarczającej ilości (4096 bajtów w moim teście z GNU grep).

Oznacza to, że grepnie wyjdzie przed zapisaniem 8192 bajtów danych, więc prawdopodobnie sporo wierszy.

Dzięki GNU grepmożesz sprawić, że wyjdzie wcześniej, używając polecenia, --line-bufferedktóre wypisuje wiersze, gdy tylko zostaną znalezione, niezależnie od tego, czy przejdzie do terminala, czy nie. Opuszczałby grepwtedy drugą znalezioną linię.

Ale z GNU i greptak możesz użyć -m 1zamiast tego, jak pokazał @terdon, co jest lepsze, gdy wychodzi z pierwszego meczu.

Jeśli grepnie grepjesteś GNU , możesz użyć sedlub awkzamiast tego. Ale tac będąc poleceniem GNU, wątpię, abyś znalazł system, w tacktórym grepnie ma GNU grep.

tac file | sed "/$pattern/!d;q"                             # BRE
tac file | P=$pattern awk '$0 ~ ENVIRON["P"] {print; exit}' # ERE

Niektóre systemy muszą tail -rrobić to samo co GNU tac.

Zauważ, że w przypadku zwykłych (możliwych do przeglądania) plików taci tail -rsą one wydajne, ponieważ odczytują pliki do tyłu, nie tylko czytają plik w całości przed wydrukowaniem go do tyłu (tak jak w przypadku sed @ slm lub tacw przypadku plików nieregularnych) .

W systemach, w których ani tacnie tail -rsą dostępne, jedynymi opcjami są ręczne odczytywanie wstecz przy użyciu języków programowania takich jak perllub użycie:

grep -e "$pattern" file | tail -n1

Lub:

sed "/$pattern/h;$!d;g" file

Ale oznacza to znalezienie wszystkich dopasowań i wydrukowanie tylko ostatniego.

Stéphane Chazelas
źródło
4

Oto możliwe rozwiązanie, które znajdzie lokalizację pierwszego wystąpienia wzorca z ostatniego:

tac -s "$pattern" -r accounting.log | head -n 1

Wykorzystuje to następujące przełączniki -si :-rtac

-s, --separator=STRING
use STRING as the separator instead of newline

-r, --regex
interpret the separator as a regular expression
mkc
źródło
Tyle że stracisz wszystko, co jest między początkiem linii a wzorem.
ychaouche
2

Korzystanie z sed

Pokazywanie alternatywnych metod dla dokładnej odpowiedzi @ Terdona za pomocą sed:

$ sed '1!G;h;$!d' file | grep -m 1 $pattern
$ sed -n '1!G;h;$p' file | grep -m 1 $pattern

Przykłady

$ seq 10 > file

$ sed '1!G;h;$!d' file | grep -m 1 5
5

$ sed -n '1!G;h;$p' file | grep -m 1 5
5

Korzystanie z Perla

Jako bonus, oto nieco łatwiejszy do zapamiętania zapis w Perlu:

$ perl -e 'print reverse <>' file | grep -m 1 $pattern

Przykład

$ perl -e 'print reverse <>' file | grep -m 1 5
5
slm
źródło
1
To (szczególnie ten sed) będzie prawdopodobnie kilka rzędów wielkości wolniejsze niż grep 5 | tail -n1lub sed '/5/h;$!d;g'. Potencjalnie zużyje również dużo pamięci. Nie jest dużo bardziej przenośny, ponieważ nadal używasz GNU grep -m.
Stéphane Chazelas