Katze Linie X bis Linie Y auf einer riesigen Datei


132

Angenommen, ich habe eine große Textdatei (> 2 GB) und möchte nur catdie Zeilen Xan Y(z. B. 57890000 bis 57890010).

Soweit ich weiß, kann ich dies durch Piping headin tailoder umgekehrt tun, d. H

head -A /path/to/file | tail -B

oder alternativ

tail -C /path/to/file | head -D

wobei A, B, Cund Dkann aus der Anzahl der Zeilen in der Datei berechnet werden, Xund Y.

Bei diesem Ansatz gibt es jedoch zwei Probleme:

  1. Sie müssen berechnen A, B, Cund D.
  2. Die Befehle könnten pipesich viel mehr Zeilen , als ich bei der Lektüre interessiert bin (zB wenn ich nur ein paar Zeilen in der Mitte einer großen Datei lese)

Gibt es eine Möglichkeit, die Shell nur mit den gewünschten Zeilen arbeiten und diese ausgeben zu lassen? (während nur Xund Y)?


1
Zu meiner Antwort wurde ein Vergleich der tatsächlichen Geschwindigkeit von 6 Methoden hinzugefügt.
Kevin

Antworten:


119

Ich schlage die sedLösung vor, aber der Vollständigkeit halber

awk 'NR >= 57890000 && NR <= 57890010' /path/to/file

Nach der letzten Zeile ausschneiden:

awk 'NR < 57890000 { next } { print } NR == 57890010 { exit }' /path/to/file

Geschwindigkeitstest:

  • 100.000.000-Zeilen-Datei generiert von seq 100000000 > test.in
  • Zeilen lesen 50.000.000-50.000.010
  • Tests in keiner bestimmten Reihenfolge
  • realzeit wie von bash's builtin gemeldettime
 4.373  4.418  4.395    tail -n+50000000 test.in | head -n10
 5.210  5.179  6.181    sed -n '50000000,50000010p;57890010q' test.in
 5.525  5.475  5.488    head -n50000010 test.in | tail -n10
 8.497  8.352  8.438    sed -n '50000000,50000010p' test.in
22.826 23.154 23.195    tail -n50000001 test.in | head -n10
25.694 25.908 27.638    ed -s test.in <<<"50000000,50000010p"
31.348 28.140 30.574    awk 'NR<57890000{next}1;NR==57890010{exit}' test.in
51.359 50.919 51.127    awk 'NR >= 57890000 && NR <= 57890010' test.in

Dies sind keine präzisen Benchmarks, aber der Unterschied ist deutlich und wiederholbar genug, um einen guten Eindruck von der relativen Geschwindigkeit jedes dieser Befehle zu erhalten.

*: Außer zwischen den ersten beiden sed -n p;qund head|tail, die im Wesentlichen gleich zu sein scheinen.


11
Aus Neugier: Wie haben Sie den Festplatten-Cache zwischen den Tests geleert?
Paweł Rumian

2
Was ist mit tail -n +50000000 test.in | head -n10, was anders tail -n-50000000 test.in | head -n10als das richtige Ergebnis geben würde?
Gilles

4
Ok, ich habe ein paar Benchmarks gemacht. tail | head ist viel schneller als sed, der Unterschied ist viel größer als ich erwartet hatte.
Gilles

3
@ Gilles du hast recht, meine schlechte. tail+|headist um 10-15% schneller als sed, ich habe diesen Benchmark hinzugefügt.
Kevin

1
Mir ist klar, dass die Frage nach Zeilen fragt, aber wenn Sie die -czum Überspringen von Zeichen verwenden, tail+|headerfolgt dies augenblicklich. Natürlich können Sie nicht "50000000" sagen und müssen möglicherweise den Anfang des gesuchten Abschnitts manuell suchen.
Danny Kirchmeier

51

Wenn Sie die Zeilen X bis Y (beginnend mit 1) verwenden möchten, verwenden Sie

tail -n +$X /path/to/file | head -n $((Y-X+1))

tailliest und verwirft die ersten X-1-Zeilen (daran führt kein Weg vorbei), liest und druckt dann die folgenden Zeilen. headliest und druckt die angeforderte Anzahl von Zeilen und beendet dann. Wenn headAusfahrten, tailempfangen ein SIGPIPE Signal und sterben, so wird es nicht mehr als die Wert einer Puffergröße gelesen hat ( in der Regel ein paar Kilobyte) von Zeilen aus der Eingabedatei.

Alternativ, wie von gorkypl vorgeschlagen, benutze sed:

sed -n -e "$X,$Y p" -e "$Y q" /path/to/file

Die sed-Lösung ist jedoch bedeutend langsamer (zumindest für GNU- und Busybox-Dienstprogramme; sed ist möglicherweise wettbewerbsfähiger, wenn Sie einen großen Teil der Datei auf einem Betriebssystem extrahieren, auf dem Piping langsam und sed schnell ist). Hier finden Sie schnelle Benchmarks unter Linux. Die Daten wurden von generiert seq 100000000 >/tmp/a, die Umgebung ist Linux / amd64, /tmpist tmpfs und der Computer ist ansonsten im Leerlauf und nicht austauschbar.

real  user  sys    command
 0.47  0.32  0.12  </tmp/a tail -n +50000001 | head -n 10 #GNU
 0.86  0.64  0.21  </tmp/a tail -n +50000001 | head -n 10 #BusyBox
 3.57  3.41  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #GNU
11.91 11.68  0.14  sed -n -e '50000000,50000010 p' -e '50000010q' /tmp/a #BusyBox
 1.04  0.60  0.46  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #GNU
 7.12  6.58  0.55  </tmp/a tail -n +50000001 | head -n 40000001 >/dev/null #BusyBox
 9.95  9.54  0.28  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #GNU
23.76 23.13  0.31  sed -n -e '50000000,90000000 p' -e '90000000q' /tmp/a >/dev/null #BusyBox

Wenn Sie den Byte-Bereich kennen, mit dem Sie arbeiten möchten, können Sie ihn schneller extrahieren, indem Sie direkt zur Startposition springen. Aber für Zeilen muss man von Anfang an lesen und Zeilenumbrüche zählen. So extrahieren Sie Blöcke von x inclusive bis y exclusive ab 0 mit einer Blockgröße von b:

dd bs=$b seek=$x count=$((y-x)) </path/to/file

1
Sind Sie sicher, dass kein Caching dazwischen liegt? Die Unterschiede zwischen tail | head und sed scheinen mir zu groß.
Paweł Rumian

@gorkypl Ich habe mehrere Maßnahmen ergriffen und die Zeiten waren vergleichbar. Wie ich geschrieben habe, geschieht dies alles im RAM (alles befindet sich im Cache).
Gilles

1
@Gilles tail will read and discard the first X-1 linescheint vermieden zu werden, wenn die Anzahl der Zeilen ab dem Ende angegeben wird. In diesem Fall scheint der Schwanz ab dem Ende entsprechend den Ausführungszeiten rückwärts zu lesen. Bitte lesen: http://unix.stackexchange.com/a/216614/79743.

1
@BinaryZebra Ja, wenn die Eingabe eine reguläre Datei ist, haben einige Implementierungen tail(einschließlich GNU-Tail) Heuristiken, die am Ende gelesen werden können. Das verbessert die tail | headLösung im Vergleich zu anderen Methoden.
Gilles

22

Der head | tailAnsatz ist einer der besten und "idiomatischsten" Wege, dies zu tun:

X=57890000
Y=57890010
< infile.txt head -n "$Y" | tail -n +"$X"

Ein schnellerer Weg ist, wie Gilles in den Kommentaren hervorhob

< infile.txt tail -n +"$X" | head -n "$((Y - X))"

Der Grund dafür ist, dass die ersten X - 1 - Linien im Vergleich zum head | tailAnflug nicht durch die Leitung gehen müssen .

Ihre formulierte Frage ist etwas irreführend und erklärt wahrscheinlich einige Ihrer unbegründeten Bedenken in Bezug auf diesen Ansatz.

  • Sie sagen , Sie berechnen müssen A, B, C, Daber wie Sie sehen können, die Zeilenzahl der Datei nicht benötigt wird und höchstens 1 Berechnung notwendig, was die Shell sowieso für Sie tun können.

  • Sie befürchten, dass die Rohrleitungen mehr Zeilen als nötig lesen. In der Tat ist dies nicht wahr: Es tail | headist ungefähr so ​​effizient, wie Sie es in Bezug auf Datei-E / A bekommen können. Betrachten Sie zunächst den minimalen Arbeitsaufwand: Um die X -te Zeile in einer Datei zu finden, müssen Sie im Allgemeinen jedes Byte lesen und anhalten, wenn Sie X Zeilenumbruchsymbole zählen, da es keine Möglichkeit gibt, die Datei zu teilen Versatz der X -ten Linie. Sobald Sie die * X * -te Zeile erreicht haben, müssen Sie alle Zeilen lesen, um sie zu drucken. Halten Sie an der Y -ten Zeile an. Daher kommt kein Ansatz durch, wenn weniger als Y- Zeilen gelesen werden. head -n $YLiest jetzt nicht mehr als YZeilen (auf die nächste Puffereinheit gerundet, aber bei korrekter Verwendung verbessern Puffer die Leistung, sodass Sie sich keine Gedanken über diesen Overhead machen müssen). Darüber hinaus tailwird nicht mehr als gelesen head, so dass wir gezeigt haben, dass head | taildie geringstmögliche Anzahl von Zeilen gelesen wird (wieder plus einige vernachlässigbare Puffer, die wir ignorieren). Der einzige Effizienzvorteil eines einzigen Tool-Ansatzes, bei dem keine Pipes verwendet werden, sind weniger Prozesse (und damit weniger Overhead).


1
Nie zuvor gesehen, dass die Umleitung zuerst in die Leitung geschaltet wurde. Cool, das macht den Rohrfluss klarer.
Clacke

14

Der orthodoxste Weg (aber nicht der schnellste, wie Gilles oben bemerkt hat ) wäre zu benutzen sed.

In deinem Fall:

X=57890000
Y=57890010
sed -n -e "$X,$Y p" -e "$Y q" filename

Die -nOption impliziert, dass nur die relevanten Zeilen auf stdout gedruckt werden.

Das p am Ende der Zielliniennummer bedeutet, dass Linien in einem bestimmten Bereich gedruckt werden. Das q im zweiten Teil des Skripts spart Zeit, indem der Rest der Datei übersprungen wird.


1
Ich erwartete , sedund tail | headetwa auf dem Niveau zu sein, aber es stellt sich heraus , dass tail | headist deutlich schneller (siehe meine Antwort ).
Gilles

1
Ich weiß nicht, was ich gelesen habe, tail/ headsind mehr „orthodox“ betrachtet, da Trimmen entweder Ende einer Datei ist genau das, was sie gemacht für. In diesen Materialien sedscheint es nur dann ins Bild zu kommen, wenn Substitutionen erforderlich sind - und schnell aus dem Bild gestoßen zu werden, wenn etwas viel Komplexeres passiert, da die Syntax für komplexe Aufgaben so viel schlechter ist als die von AWK, die dann übernimmt .
Underscore_d

7

Wenn wir den auszuwählenden Bereich kennen, können wir von der ersten lStartbis zur letzten Zeile lEndFolgendes berechnen:

lCount="$((lEnd-lStart+1))"

Wenn wir die Gesamtzahl der Zeilen kennen, können lAllwir auch die Entfernung zum Ende der Datei berechnen:

toEnd="$((lAll-lStart+1))"

Dann werden wir beide kennen:

"how far from the start"            ($lStart) and
"how far from the end of the file"  ($toEnd).

Die kleinste davon auswählen: tailnumberwie folgt:

tailnumber="$toEnd"; (( toEnd > lStart )) && tailnumber="+$linestart"

Ermöglicht die Verwendung des durchweg schnellsten Ausführungsbefehls:

tail -n"${tailnumber}" ${thefile} | head -n${lCount}

Bitte beachten Sie das zusätzliche Pluszeichen ("+"), wenn $linestartausgewählt.

Die einzige Einschränkung besteht darin, dass wir die Gesamtzahl der Zeilen benötigen. Das Auffinden kann einige zusätzliche Zeit in Anspruch nehmen.
Wie üblich mit:

linesall="$(wc -l < "$thefile" )"

Einige Male gemessen sind:

lStart |500| lEnd |500| lCount |11|
real   user   sys    frac
0.002  0.000  0.000  0.00  | command == tail -n"+500" test.in | head -n1
0.002  0.000  0.000  0.00  | command == tail -n+500 test.in | head -n1
3.230  2.520  0.700  99.68 | command == tail -n99999501 test.in | head -n1
0.001  0.000  0.000  0.00  | command == head -n500 test.in | tail -n1
0.001  0.000  0.000  0.00  | command == sed -n -e "500,500p;500q" test.in
0.002  0.000  0.000  0.00  | command == awk 'NR<'500'{next}1;NR=='500'{exit}' test.in


lStart |50000000| lEnd |50000010| lCount |11|
real   user   sys    frac
0.977  0.644  0.328  99.50 | command == tail -n"+50000000" test.in | head -n11
1.069  0.756  0.308  99.58 | command == tail -n+50000000 test.in | head -n11
1.823  1.512  0.308  99.85 | command == tail -n50000001 test.in | head -n11
1.950  2.396  1.284  188.77| command == head -n50000010 test.in | tail -n11
5.477  5.116  0.348  99.76 | command == sed -n -e "50000000,50000010p;50000010q" test.in
10.124  9.669  0.448  99.92| command == awk 'NR<'50000000'{next}1;NR=='50000010'{exit}' test.in


lStart |99999000| lEnd |99999010| lCount |11|
real   user   sys    frac
0.001  0.000  0.000  0.00  | command == tail -n"1001" test.in | head -n11
1.960  1.292  0.660  99.61 | command == tail -n+99999000 test.in | head -n11
0.001  0.000  0.000  0.00  | command == tail -n1001 test.in | head -n11
4.043  4.704  2.704  183.25| command == head -n99999010 test.in | tail -n11
10.346  9.641  0.692  99.88| command == sed -n -e "99999000,99999010p;99999010q" test.in
21.653  20.873  0.744  99.83 | command == awk 'NR<'99999000'{next}1;NR=='99999010'{exit}' test.in

Beachten Sie, dass sich die Zeiten drastisch ändern, wenn sich die ausgewählten Linien in der Nähe des Starts oder des Endes befinden. Ein Befehl, der auf einer Seite der Datei gut zu funktionieren scheint, kann auf der anderen Seite der Datei sehr langsam sein.


Kommentare sind nicht für eine längere Diskussion gedacht. Diese Unterhaltung wurde in den Chat verschoben .
Terdon

@BinaryZebra - viel besser.
mikeserv

0

Ich mache das oft genug und habe dieses Skript geschrieben. Ich muss die Zeilennummern nicht finden, das Skript erledigt alles.

#!/bin/bash

# $1: start time
# $2: end time
# $3: log file to read
# $4: output file

# i.e. log_slice.sh 18:33 19:40 /var/log/my.log /var/log/myslice.log

if [[ $# != 4 ]] ; then 
echo 'usage: log_slice.sh <start time> <end time> <log file> <output file>'
echo
exit;
fi

if [ ! -f $3 ] ; then
echo "'$3' doesn't seem to exit."
echo 'exiting.'
exit;
fi

sline=$(grep -n " ${1}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of start time
eline=$(grep -n " ${2}" $3|head -1|cut -d: -f1)  #what line number is first occurrance of end time

linediff="$((eline-sline))"

tail -n+${sline} $3|head -n$linediff > $4

2
Sie beantworten eine Frage, die nicht gestellt wurde. Ihre Antwort ist 10% tail|head, was in der Frage und den anderen Antworten ausführlich besprochen wurde, und 90% bestimmen die Zeilennummern, in denen bestimmte Zeichenfolgen / Muster erscheinen, die nicht Teil der Frage waren . PS Sie sollten immer Ihre Shell-Parameter und -Variablen angeben; zB "$ 3" und "$ 4".
G-Man
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.