Grep vom Ende einer Datei bis zum Anfang

38

Ich habe eine Datei mit ca. 30.000.000 Zeilen (Radius Accounting) und muss die letzte Übereinstimmung eines bestimmten Musters finden.

Der Befehl:

tac accounting.log | grep $pattern

gibt was ich brauche, ist aber zu langsam, da das OS erst die ganze Datei lesen und dann an die Pipe senden muss.

Ich brauche also etwas Schnelles, das die Datei von der letzten bis zur ersten Zeile lesen kann.

Hábner Costa
quelle

Antworten:

44

tacHilft nur, wenn Sie grep -m 1(unter der Annahme von GNU grep) auch verwenden, grepum nach dem ersten Match zu stoppen:

tac accounting.log | grep -m 1 foo

Von man grep:

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

In dem Beispiel in Ihrer Frage ist beides tacund die grepNotwendigkeit, die gesamte Datei zu verarbeiten, tacirgendwie sinnlos.

Wenn Sie grep -malso nicht verwenden tac, analysieren Sie einfach die Ausgabe von grep, um die letzte Übereinstimmung zu erhalten:

grep foo accounting.log | tail -n 1 

Ein anderer Ansatz wäre die Verwendung von Perl oder einer anderen Skriptsprache. Zum Beispiel (wo $pattern=foo):

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

oder

awk '/foo/{k=$0}END{print k}' file
terdon
quelle
1
Ich benutze tac, weil ich die letzte Übereinstimmung eines bestimmten Musters finden muss. Mit Ihrem Vorschlag "grep -m1" geht die Ausführungszeit von 0m0.597s auf 0m0.007s \ o /. Danke an alle!
Hábner Costa
1
@ HábnerCosta gern geschehen. Ich verstehe, warum Sie verwenden tac. Mein Punkt war, dass es nur hilft, wenn Sie es auch verwenden, -mda die Datei von zwei Programmen noch vollständig gelesen werden muss. Ansonsten könnten Sie einfach nach allen Vorkommen suchen und nur das letzte behalten, wie ich es tue tail -n 1.
Terdon
6
Warum sagen Sie "tac muss die gesamte [...] Datei verarbeiten"? Das erste, was tac macht, ist, zum Ende der Datei zu suchen und einen Block vom Ende zu lesen. Sie können dies selbst mit strace (1) überprüfen. In Kombination mit grep -msollte es sehr effizient sein.
Camh
1
@camh in Kombination grep -mdamit ist. Das OP verwendete nicht, -mso dass sowohl grep als auch tac das Ganze verarbeiteten.
Terdon
Könnten Sie bitte die Bedeutung der awkZeile erläutern?
Sopalajo de Arrierez
12

Der Grund warum

tac file | grep foo | head -n 1

hört nicht beim ersten Treffer auf, liegt an der Zwischenspeicherung.

Wird normalerweise head -n 1nach dem Lesen einer Zeile beendet. Also grepsollte man sich ein SIGPIPE zulegen und gleich beenden, sobald es seine zweite Zeile schreibt.

Aber was passiert ist, dass, weil seine Ausgabe nicht an ein Terminal geht, es greppuffert. Das heißt, es wird nicht geschrieben, bis es genug akkumuliert hat (4096 Bytes in meinem Test mit GNU grep).

Das heißt, es grepwird nicht beendet, bevor 8192 Datenbytes geschrieben wurden, also wahrscheinlich ziemlich viele Zeilen.

Mit GNU grepkönnen Sie das Programm schneller beenden, indem --line-bufferedSie festlegen, dass Zeilen geschrieben werden sollen, sobald sie gefunden werden, unabhängig davon, ob sie zu einem Terminal geleitet werden oder nicht. So grepwürde dann die zweite Zeile verlassen, die es findet.

Aber mit GNU grepkannst du es trotzdem verwenden, -m 1wie @terdon gezeigt hat, was besser ist, wenn es beim ersten Match beendet wird.

Wenn Sie grepnicht GNU sind grep, können Sie stattdessen sedoder awkverwenden. Da tac es sich jedoch um einen GNU-Befehl handelt, bezweifle ich, dass Sie ein System finden, bei tacdem grepGNU nicht verwendet wird grep.

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

Einige Systeme müssen tail -rdasselbe tun wie GNU tac.

Beachten Sie, dass für eine regelmäßige (durchsuchbar) Dateien, tacund tail -rsind effizient , weil sie die Dateien rückwärts lesen tun, sind sie nicht nur die Datei vollständig im Speicher zu lesen , bevor sie rückwärts Druck (wie @ SLMs sed Ansatz oder tacauf nicht-regulären Dateien würde) .

Auf Systemen, auf denen weder verfügbar tacnoch tail -rverfügbar sind, besteht die einzige Möglichkeit darin, das Rückwärtslesen von Hand mit folgenden Programmiersprachen zu implementieren perl:

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

Oder:

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

Aber das bedeutet, alle Übereinstimmungen zu finden und nur die letzte zu drucken.

Stéphane Chazelas
quelle
4

Hier ist eine mögliche Lösung, die den Ort des ersten Auftretens des Musters vom letzten findet:

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

Hierfür werden die folgenden Schalter -sund verwendet :-rtac

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

-r, --regex
interpret the separator as a regular expression
mkc
quelle
Mit der Ausnahme, dass Sie alles verlieren, was zwischen dem Anfang der Linie und dem Muster liegt.
Ychaouche
2

Mit sed

Es werden einige alternative Methoden @ Terdon der feinen Antwort mit sed:

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

Beispiele

$ 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

Perl verwenden

Als Bonus ist hier eine etwas einfachere Notation in Perl zu merken:

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

Beispiel

$ perl -e 'print reverse <>' file | grep -m 1 5
5
slm
quelle
1
Das ist (vor allem das sedeine) wahrscheinlich mehrere Größenordnungen langsamer als grep 5 | tail -n1oder sed '/5/h;$!d;g'. Es wird möglicherweise auch viel Speicher verbrauchen. Es ist nicht viel portabler, da Sie immer noch GNUs verwenden grep -m.
Stéphane Chazelas