web-dev-qa-db-ger.com

Bash-Tool zum Abrufen der n-ten Zeile aus einer Datei

Gibt es einen "kanonischen" Weg, dies zu tun? Ich habe head -n | tail -1 verwendet, was den Trick tut, aber ich habe mich gefragt, ob es ein Bash-Tool gibt, das eine Zeile (oder einen Zeilenbereich) aus einer Datei extrahiert.

Mit "kanonisch" meine ich ein Programm, dessen Hauptfunktion dies ist.

451
Vlad Vivdovitch

head und Pipe mit tail sind für eine große Datei langsam. Ich würde sed so vorschlagen:

sed 'NUMq;d' file

Dabei ist NUM die Nummer der Zeile, die Sie drucken möchten. B. sed '10q;d' file, um die 10. Zeile von file zu drucken.

Erläuterung:

NUMq wird sofort beendet, wenn die Zeilennummer NUM ist.

d löscht die Zeile, anstatt sie zu drucken; Dies ist in der letzten Zeile gesperrt, da die q den Rest des Skripts beim Beenden überspringt.

Wenn Sie NUM in einer Variablen haben, möchten Sie Anführungszeichen anstelle von einfachen Anführungszeichen verwenden:

sed "${NUM}q;d" file
615
anubhava
sed -n '2p' < file.txt

druckt die 2. Zeile

sed -n '2011p' < file.txt

2011 Zeile

sed -n '10,33p' < file.txt

zeile 10 bis Zeile 33

sed -n '1p;3p' < file.txt

1. und 3. Zeile

und so weiter...

Um Linien mit sed hinzuzufügen, können Sie dies überprüfen:

sed: füge eine Zeile an einer bestimmten Position ein

242
jm666

Ich habe eine einzigartige Situation, in der ich die auf dieser Seite vorgeschlagenen Lösungen bewerten kann. Deshalb schreibe ich diese Antwort als eine Zusammenfassung der vorgeschlagenen Lösungen mit jeweils eingeschlossenen Laufzeiten.

Konfiguration

Ich habe eine Textdatei mit 3,261 Gigabyte ASCII und einem Schlüsselwertpaar pro Zeile. Die Datei enthält insgesamt 3.339.550.320 Zeilen und kann in keinem Editor, den ich versucht habe, geöffnet werden, einschließlich meines Go-to-Vim. Ich muss diese Datei als Teilmenge verwenden, um einige der Werte zu untersuchen, die ich entdeckt habe und die nur etwa 500.000.000 beginnen.

Weil die Datei so viele Zeilen hat:

  • Ich muss nur eine Teilmenge der Zeilen extrahieren, um mit den Daten nützliche Informationen zu erhalten.
  • Das Lesen jeder Reihe, die zu den Werten führt, die mich interessieren, wird lange dauern.
  • Wenn die Lösung über die Zeilen, die mir wichtig sind, vorliest, und den Rest der Datei weiter liest, wird es Zeit kosten, fast 3 Milliarden irrelevante Zeilen zu lesen und 6x länger als nötig zu dauern.

Mein Best-Case-Szenario ist eine Lösung, die nur eine einzige Zeile aus der Datei extrahiert, ohne die anderen Zeilen in der Datei zu lesen. Ich kann mir jedoch nicht vorstellen, wie ich dies in Bash erreichen würde.

Im Sinne meiner Vernunft werde ich nicht versuchen, die gesamten 500.000.000 Zeilen zu lesen, die ich für mein eigenes Problem brauche. Stattdessen werde ich versuchen, die Zeile 50.000.000 aus 3.339.550.320 zu extrahieren (was bedeutet, dass das vollständige Lesen der Datei 60x länger dauert als nötig).

Ich werde die eingebaute Variable time verwenden, um jeden Befehl zu vergleichen.

Baseline

Lassen Sie uns zuerst sehen, wie die Lösung headtail:

$ time head -50000000 myfile.ascii | tail -1
pgm_icnt = 0

real    1m15.321s

Der Grundwert für die Reihe 50 Millionen ist 00: 01: 15.321. Wenn ich 500 Millionen Reihen erreicht hätte, wären es wahrscheinlich ~ 12,5 Minuten.

Schnitt

Ich bin zweifelhaft, aber es ist einen Versuch wert:

$ time cut -f50000000 -d$'\n' myfile.ascii
pgm_icnt = 0

real    5m12.156s

Der Lauf dauerte 00: 05: 12.156, was viel langsamer ist als die Basislinie! Ich bin mir nicht sicher, ob er die gesamte Datei durchgelesen hat oder nur bis zu 50 Millionen Zeilen, bevor er angehalten wurde. Dies scheint jedoch keine Lösung für das Problem zu sein.

AWK

Ich habe die Lösung nur mit der Variable exit ausgeführt, weil ich nicht auf die vollständige Datei warten würde:

$ time awk 'NR == 50000000 {print; exit}' myfile.ascii
pgm_icnt = 0

real    1m16.583s

Dieser Code lief in 00: 01: 16.583, was nur ~ 1 Sekunde langsamer ist, aber immer noch keine Verbesserung der Basislinie darstellt. Bei dieser Geschwindigkeit hätte der Exit-Befehl etwa ~ 76 Minuten benötigt, um die gesamte Datei zu lesen!

Perl

Ich habe auch die vorhandene Perl-Lösung ausgeführt:

$ time Perl -wnl -e '$.== 50000000 && print && exit;' myfile.ascii
pgm_icnt = 0

real    1m13.146s

Dieser Code lief in 00: 01: 13.146, was etwa 2 Sekunden schneller ist als die Basislinie. Wenn ich das ganze 500.000.000 Mal laufen würde, würde es wahrscheinlich ~ 12 Minuten dauern.

sed

Die beste Antwort auf der Tafel, hier ist mein Ergebnis:

$ time sed "50000000q;d" myfile.ascii
pgm_icnt = 0

real    1m12.705s

Dieser Code lief in 00: 01: 12.705, was 3 Sekunden schneller als die Basislinie und ~ 0,4 Sekunden schneller als Perl ist. Wenn ich die gesamten 500.000.000 Zeilen ausgeführt hätte, hätte es wahrscheinlich ~ 12 Minuten gedauert.

mapfile

Ich habe Bash 3.1 und kann daher die Mapfile-Lösung nicht testen.

Fazit

Anscheinend ist es meist schwierig, die Lösung headtail zu verbessern. Im besten Fall bietet die sed-Lösung eine Effizienzsteigerung von ~ 3%.

(Prozentsätze berechnet mit der Formel % = (runtime/baseline - 1) * 100)

Reihe 50.000.000

  1. 00: 01: 12.705 (-00: 00: 02.616 = -3,47%) sed
  2. 00: 01: 13.146 (-00: 00: 02.175 = -2.89%) Perl
  3. 00: 01: 15.321 (+00: 00: 00.000 = + 0,00%) head|tail
  4. 00: 01: 16.583 (+00: 00: 01.262 = + 1,68%) awk
  5. 00: 05: 12.156 (+00: 03: 56.835 = + 314.43%) cut

Reihe 500.000.000

  1. 00: 12: 07.050 (-00: 00: 26.160) sed
  2. 00: 12: 11.460 (-00: 00: 21.750) Perl
  3. 00: 12: 33.210 (+00: 00: 00.000) head|tail
  4. 00: 12: 45.830 (+00: 00: 12.620) awk
  5. 00: 52: 01.560 (+00: 40: 31.650) cut

Zeile 3,338,559,320

  1. 01: 20: 54.599 (-00: 03: 05.327) sed
  2. 01: 21: 24.045 (-00: 02: 25.227) Perl
  3. 01: 23: 49.273 (+00: 00: 00.000) head|tail
  4. 01: 25: 13.548 (+00: 02: 35.735) awk
  5. 05: 47: 23.026 (+04: 24: 26.246) cut
71

Mit awk geht es ziemlich schnell:

awk 'NR == num_line' file

Wenn dies der Fall ist, wird das Standardverhalten von awk ausgeführt: {print $0}.


Alternative Versionen

Wenn Ihre Datei sehr groß ist, sollten Sie nach dem Lesen der erforderlichen Zeile exit lieber nacheinander suchen. Auf diese Weise sparen Sie CPU-Zeit.

awk 'NR == num_line {print; exit}' file

Wenn Sie die Zeilennummer einer bash-Variablen angeben möchten, können Sie Folgendes verwenden:

awk 'NR == n' n=$num file
awk -v n=$num 'NR == n' file   # equivalent
42
fedorqui

Wow, alle Möglichkeiten!

Versuche dies:

sed -n "${lineNum}p" $file

oder eine davon abhängig von deiner Awk-Version:

awk  -vlineNum=$lineNum 'NR == lineNum {print $0}' $file
awk -v lineNum=4 '{if (NR == lineNum) {print $0}}' $file
awk '{if (NR == lineNum) {print $0}}' lineNum=$lineNum $file

( Möglicherweise müssen Sie den Befehl nawk oder gawkausführen).

Gibt es ein Werkzeug, das nur diese bestimmte Zeile druckt? Kein Standardwerkzeug. sed ist jedoch wahrscheinlich am nächsten und am einfachsten zu verwenden.

26
David W.
# print line number 52
sed '52!d' file

Nützliche einzeilige Skripte für sed

20
Steven Penny

Diese Frage wird mit Bash getaggt. Hier ist die Methode von Bash (≥4): Verwenden Sie mapfile mit der Option -s (überspringen) und -n (count).

Wenn Sie die 42. Zeile einer Datei benötigen, file:

mapfile -s 41 -n 1 ary < file

An diesem Punkt haben Sie ein Array ary, dessen Felder die Zeilen von file enthalten (einschließlich der nachgestellten Newline), wobei wir die ersten 41 Zeilen (-s 41) übersprungen haben und nach dem Lesen einer Zeile (-n 1) angehalten haben. Das ist also wirklich die 42. Zeile. Um es auszudrucken:

printf '%s' "${ary[0]}"

Wenn Sie eine Reihe von Zeilen benötigen, sagen Sie den Bereich von 42–666 (einschließlich) und sagen, dass Sie die Berechnungen nicht selbst ausführen möchten, und drucken Sie sie auf stdout:

mapfile -s $((42-1)) -n $((666-42+1)) ary < file
printf '%s' "${ary[@]}"

Wenn Sie auch diese Zeilen bearbeiten müssen, ist es nicht besonders praktisch, die nachgestellte Zeile zu speichern. In diesem Fall verwenden Sie die -t-Option (Trimmen):

mapfile -t -s $((42-1)) -n $((666-42+1)) ary < file
# do stuff
printf '%s\n' "${ary[@]}"

Sie können eine Funktion für Sie haben:

print_file_range() {
    # $1-$2 is the range of file $3 to be printed to stdout
    local ary
    mapfile -s $(($1-1)) -n $(($2-$1+1)) ary < "$3"
    printf '%s' "${ary[@]}"
}

Keine externen Befehle, nur Bash Builtins!

19
gniourf_gniourf

Sie können auch sed drucken und beenden:

sed -n '10{p;q;}' file   # print line 10
10
bernd

Nach meinen Tests ist meine Empfehlung in Bezug auf Leistung und Lesbarkeit:

tail -n+N | head -1

N ist die gewünschte Zeilennummer. Beispielsweise druckt tail -n+7 input.txt | head -1 die 7. Zeile der Datei.

tail -n+N druckt alles ab Zeile N und head -1 stoppt nach einer Zeile.


Die Alternative head -N | tail -1 ist vielleicht etwas lesbarer. Zum Beispiel wird hier die 7. Zeile gedruckt:

head -7 input.txt | tail -1

Wenn es um die Leistung geht, gibt es für kleine Größen keinen großen Unterschied, aber der tail | head (von oben) übertrifft ihn, wenn die Dateien sehr groß werden.

Der Top-wählte sed 'NUMq;d' ist interessant zu wissen, aber ich würde behaupten, dass er von weniger Leuten als der Kopf/Schwanz-Lösung verstanden wird und er ist auch langsamer als Schwanz/Kopf.

In meinen Tests übertrafen beide Versionen der Schwänze/Köpfe konstant sed 'NUMq;d'. Dies steht im Einklang mit den anderen Benchmarks, die veröffentlicht wurden. Es ist schwer einen Fall zu finden, in dem die Schwänze wirklich schlecht waren. Es ist auch nicht überraschend, da diese Vorgänge in einem modernen Unix-System stark optimiert werden würden.

Um eine Vorstellung von den Leistungsunterschieden zu erhalten, sind dies die Zahlen, die ich für eine große Datei (9.3G) bekomme:

  • tail -n+N | head -1: 3,7 sek
  • head -N | tail -1: 4,6 sek
  • sed Nq;d: 18,8 sek

Die Ergebnisse können sich unterscheiden, aber die Leistung head | tail und tail | head ist im Allgemeinen für kleinere Eingaben vergleichbar, und sed ist immer um einen signifikanten Faktor (etwa 5x) langsamer.

Um meinen Benchmark zu reproduzieren, können Sie Folgendes versuchen, seien Sie jedoch gewarnt, dass im aktuellen Arbeitsverzeichnis eine 9.3G-Datei erstellt wird:

#!/bin/bash
readonly file=tmp-input.txt
readonly size=1000000000
readonly pos=500000000
readonly retries=3

seq 1 $size > $file
echo "*** head -N | tail -1 ***"
for i in $(seq 1 $retries) ; do
    time head "-$pos" $file | tail -1
done
echo "-------------------------"
echo
echo "*** tail -n+N | head -1 ***"
echo

seq 1 $size > $file
ls -alhg $file
for i in $(seq 1 $retries) ; do
    time tail -n+$pos $file | head -1
done
echo "-------------------------"
echo
echo "*** sed Nq;d ***"
echo

seq 1 $size > $file
ls -alhg $file
for i in $(seq 1 $retries) ; do
    time sed $pos'q;d' $file
done
/bin/rm $file

Hier ist die Ausgabe eines Laufs auf meinem Computer (ThinkPad X1 Carbon mit einer SSD und 16 GB Arbeitsspeicher). Ich gehe davon aus, dass im letzten Lauf alles vom Cache kommt, nicht von der Festplatte:

*** head -N | tail -1 ***
500000000

real    0m9,800s
user    0m7,328s
sys     0m4,081s
500000000

real    0m4,231s
user    0m5,415s
sys     0m2,789s
500000000

real    0m4,636s
user    0m5,935s
sys     0m2,684s
-------------------------

*** tail -n+N | head -1 ***

-rw-r--r-- 1 phil 9,3G Jan 19 19:49 tmp-input.txt
500000000

real    0m6,452s
user    0m3,367s
sys     0m1,498s
500000000

real    0m3,890s
user    0m2,921s
sys     0m0,952s
500000000

real    0m3,763s
user    0m3,004s
sys     0m0,760s
-------------------------

*** sed Nq;d ***

-rw-r--r-- 1 phil 9,3G Jan 19 19:50 tmp-input.txt
500000000

real    0m23,675s
user    0m21,557s
sys     0m1,523s
500000000

real    0m20,328s
user    0m18,971s
sys     0m1,308s
500000000

real    0m19,835s
user    0m18,830s
sys     0m1,004s
9
Philipp Claßen

Sie können dazu auch Perl verwenden:

Perl -wnl -e '$.== NUM && print && exit;' some.file
7
Timofey Stolbov

Die schnellste Lösung für große Dateien ist immer Tail, sofern die beiden Entfernungen:

  • vom Anfang der Datei bis zur Startzeile. Nennen wir es S
  • die Entfernung von der letzten Zeile bis zum Ende der Datei. Sei es E

sind bekannt. Dann könnten wir das verwenden:

mycount="$E"; (( E > S )) && mycount="+$S"
howmany="$(( endline - startline + 1 ))"
tail -n "$mycount"| head -n "$howmany"

wieviele Zeilen werden nur benötigt.

Weitere Details in https://unix.stackexchange.com/a/216614/79743

6
user2350426

Im Anschluss an die sehr hilfreiche Benchmarking-Antwort von CaffeineConnoisseur ... Ich war neugierig, wie schnell die Mapfile-Methode mit anderen verglichen wurde (da diese nicht getestet wurde), also versuchte ich einen schnellen Vergleich Ich habe bash 4 griffbereit. War in einem Test der "Schwanz" -Methode (anstatt des Kopfes), die in einem der Kommentare zu der obersten Antwort erwähnt wurde, während ich dabei war, während die Leute ihr Lob singen. Ich habe nichts in der Nähe der verwendeten Testdatei; Das Beste, was ich kurzfristig finden konnte, war eine 14M-Pedigree-Datei (lange Zeilen, die durch Leerzeichen getrennt sind, knapp 12000 Zeilen).

Kurzfassung: mapfile erscheint schneller als die Ausschneidemethode, aber langsamer als alles andere. Ich würde es als Dud bezeichnen. Schwanz | head, OTOH, sieht aus, als könnte es der schnellste sein, obwohl bei einer Datei dieser Größe der Unterschied im Vergleich zu sed nicht allzu groß ist.

$ time head -11000 [filename] | tail -1
[output redacted]

real    0m0.117s

$ time cut -f11000 -d$'\n' [filename]
[output redacted]

real    0m1.081s

$ time awk 'NR == 11000 {print; exit}' [filename]
[output redacted]

real    0m0.058s

$ time Perl -wnl -e '$.== 11000 && print && exit;' [filename]
[output redacted]

real    0m0.085s

$ time sed "11000q;d" [filename]
[output redacted]

real    0m0.031s

$ time (mapfile -s 11000 -n 1 ary < [filename]; echo ${ary[0]})
[output redacted]

real    0m0.309s

$ time tail -n+11000 [filename] | head -n1
[output redacted]

real    0m0.028s

Hoffe das hilft!

4

Alle obigen Antworten beantworten die Frage direkt. Aber hier ist eine weniger direkte Lösung, aber eine möglicherweise wichtigere Idee, um Gedanken zu provozieren. 

Da die Zeilenlängen beliebig sind, werden alle Bytes der Datei vor der n-ten Zeile brauchen gelesen. Wenn Sie eine große Datei haben oder diese Aufgabe mehrmals wiederholen müssen und dieser Vorgang zeitaufwändig ist, sollten Sie sich ernsthaft überlegen, ob Sie Ihre Daten überhaupt auf andere Weise speichern sollten. 

Die wirkliche Lösung besteht darin, einen Index zu haben, z. Am Anfang der Datei werden die Positionen angegeben, an denen die Zeilen beginnen. Sie können ein Datenbankformat verwenden oder einfach eine Tabelle am Anfang der Datei hinzufügen. Alternativ können Sie eine separate Indexdatei für Ihre große Textdatei erstellen. 

z.B. Sie können eine Liste von Zeichenpositionen für Zeilenumbrüche erstellen:

awk 'BEGIN{c=0;print(c)}{c+=length()+1;print(c+1)}' file.txt > file.idx

dann lese mit tail, was tatsächlich seeks direkt zum entsprechenden Punkt in der Datei führt!

z.B. Linie 1000 zu bekommen:

tail -c +$(awk 'NR=1000' file.idx) file.txt | head -1
  • Dies funktioniert möglicherweise nicht mit 2-Byte-/Multibyte-Zeichen, da awk "zeichenbewusst" ist, Tail jedoch nicht.
  • Ich habe dies nicht mit einer großen Datei getestet. 
  • Siehe auch diese Antwort .
  • Alternativ - teilen Sie Ihre Datei in kleinere Dateien auf!
4
Sanjay Manohar

Wenn Sie mehrere Zeilen haben, die durch\n (normalerweise neue Zeile) begrenzt werden Sie können auch 'Ausschneiden' verwenden:

echo "$data" | cut -f2 -d$'\n'

Sie erhalten die 2. Zeile aus der Datei. -f3 gibt Ihnen die 3. Zeile.

3
danger89

Viele gute Antworten schon. Ich persönlich gehe mit awk. Wenn Sie bash verwenden, fügen Sie Ihrem ~/.bash_profile einfach das Folgende hinzu. Wenn Sie sich das nächste Mal anmelden (oder wenn Sie nach diesem Update Ihr .bash_profile als Quelle verwenden), steht Ihnen eine neue "nth" -Funktion zur Verfügung, durch die Ihre Dateien geleitet werden können. 

Führen Sie dies aus oder legen Sie es in Ihr ~/.bash_profile (wenn Sie bash verwenden) und öffnen Sie bash erneut (oder führen Sie source ~/.bach_profile aus).

# print just the nth piped in line nth () { awk -vlnum=${1} 'NR==lnum {print; exit}'; }

Um es dann zu verwenden, leiten Sie es einfach durch. Z.B.,:

$ yes line | cat -n | nth 5 5 line

2
JJC

Mit dem, was andere erwähnt haben, wollte ich, dass dies eine schnelle und einfache Funktion in meiner Bash-Shell ist.

Erstellen Sie eine Datei: ~/.functions

Fügen Sie den Inhalt hinzu:

getline() { line=$1 sed $line'q;d' $2 }

Dann fügen Sie dies Ihrem ~/.bash_profile hinzu:

source ~/.functions

Wenn Sie jetzt ein neues Bash-Fenster öffnen, können Sie die Funktion einfach so aufrufen:

getline 441 myfile.txt

1
Mark Shust

So drucken Sie eine n-te Zeile mit sed mit einer Variablen als Zeilennummer:

a=4
sed -e $a'q:d' file

Das '-e' Flag dient zum Hinzufügen eines Skripts zum auszuführenden Befehl.

1
aliasav

Ich habe einige der obigen Antworten in ein kurzes Bash-Skript geschrieben, das Sie in eine Datei mit dem Namen get.sh einfügen und mit /usr/local/bin/get (oder einem beliebigen anderen Namen) verknüpfen können.

#!/bin/bash
if [ "${1}" == "" ]; then
    echo "error: blank line number";
    exit 1
fi
re='^[0-9]+$'
if ! [[ $1 =~ $re ]] ; then
    echo "error: line number arg not a number";
    exit 1
fi
if [ "${2}" == "" ]; then
    echo "error: blank file name";
    exit 1
fi
sed "${1}q;d" $2;
exit 0

Stellen Sie sicher, dass es mit ausführbar ist 

$ chmod +x get

Verknüpfen Sie es, um es auf der PATH verfügbar zu machen

$ ln -s get.sh /usr/local/bin/get

Verantwortungsvoll geniessen!

P

0
polarise