873 Stimmen

Bash-Tool, um die n-te Zeile aus einer Datei zu erhalten

Gibt es einen "kanonischen" Weg, um das zu tun? Ich benutze head -n | tail -1, was den Trick macht, aber ich frage mich, ob es ein Bash-Tool gibt, das speziell eine Zeile (oder einen Bereich von Zeilen) aus einer Datei extrahiert.

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

11 Stimmen

Die "Unix-Methode" besteht darin, Tools miteinander zu verketten, die ihre jeweilige Aufgabe gut erledigen. Ich denke also, du hast bereits eine sehr geeignete Methode gefunden. Andere Methoden umfassen awk und sed und ich bin mir sicher, dass jemand auch mit einer Perl-One-Liner oder so etwas aufkommen kann ;)

4 Stimmen

Der Doppelbefehl legt nahe, dass die Lösung head | tail nicht optimal ist. Es wurden andere, fast optimale Lösungen vorgeschlagen.

0 Stimmen

Haben Sie irgendwelche Benchmarks durchgeführt, um herauszufinden, welche Lösung im Durchschnittsfall am schnellsten ist?

1121voto

anubhava Punkte 713155

head und Pipe mit tail werden für eine riesige Datei langsam sein. Ich würde sed wie folgt vorschlagen:

sed 'NUMq;d' file

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

Erklärung:

NUMq wird sofort beenden, wenn die Zeilennummer NUM ist.

d wird die Zeile löschen anstatt sie zu drucken; dies wird auf der letzten Zeile gehemmt, weil das q bewirkt, dass der Rest des Skripts übersprungen wird beim Beenden.

Wenn Sie NUM in einer Variablen haben, sollten Sie doppelte Anführungszeichen anstelle von einfachen verwenden:

sed "${NUM}q;d" file

61 Stimmen

Für diejenigen, die sich fragen, scheint diese Lösung etwa 6 bis 9 mal schneller zu sein als die sed -n 'NUMp' und sed 'NUM!d' Lösungen, die unten vorgeschlagen wurden.

88 Stimmen

Ich denke, tail -n+NUM Datei | head -n1 könnte genauso schnell oder schneller sein. Zumindest war es auf meinem System (deutlich) schneller, als ich es mit NUM als 250000 auf einer Datei mit einer halben Million Zeilen versucht habe. Ihr Ergebnis kann variieren, aber ich sehe nicht wirklich, warum es das sollte.

0 Stimmen

@rici und du kannst ganz einfach auswählen, wie viele Zeilen über diesem Punkt liegen, indem du head -n1 in head -nNUM2 änderst, du solltest diese Antwort zu deiner eigenen machen.

416voto

jm666 Punkte 58205
sed -n '2p' < file.txt

wird die 2. Zeile ausdrucken

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

usw...

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

sed: eine Zeile an einer bestimmten Stelle einfügen

5 Stimmen

Warum ist das '<' in diesem Fall notwendig? Würde ich nicht das gleiche Ergebnis ohne erreichen?

11 Stimmen

@RafaelBarbosa das < ist in diesem Fall nicht notwendig. Einfach, es ist meine Vorliebe, Umleitungen zu verwenden, weil ich oft Umleitungen wie sed -n '100p' < <(some_command) verwende - also, universelle Syntax :). Es ist NICHT weniger effektiv, weil Umleitungen im Shell-Forking selbst gemacht werden, also... es ist nur eine Vorliebe... (und ja, es ist ein Zeichen länger) :)

2 Stimmen

@jm666 Eigentlich ist es 2 Zeichen länger, da Sie normalerweise das "<" sowie einen zusätzlichen Leerzeichen "'" nach dem "<" hinzufügen würden, im Gegensatz zu nur einem Leerzeichen, wenn Sie das "<" nicht verwendet hätten :)

132voto

Ich habe eine einzigartige Situation, in der ich die Lösungen, die auf dieser Seite vorgeschlagen wurden, benchmarken kann, und deshalb schreibe ich diese Antwort als Zusammenfassung der vorgeschlagenen Lösungen mit eingebundenen Laufzeiten für jede.

Einrichtung

Ich habe eine 3,261 Gigabyte ASCII-Textdatei mit einem Schlüssel-Wert-Paar pro Zeile. Die Datei enthält insgesamt 3.339.550.320 Zeilen und lässt sich in keinem der Editoren öffnen, die ich ausprobiert habe, einschließlich meinem bevorzugten Vim. Ich muss diese Datei subsetten, um einige der Werte, die ich entdeckt habe und die erst ab ungefähr Zeile ~500.000.000 beginnen, zu untersuchen.

Weil die Datei so viele Zeilen hat:

  • Ich muss nur einen Teil der Zeilen extrahieren, um etwas Nützliches mit den Daten anzufangen.
  • Das Durchlesen jeder Zeile, die zu den Werten führt, die mich interessieren, dauert lange.
  • Wenn die Lösung über die Zeilen hinaus liest, die mich interessieren, und weiterhin die restlichen Zeilen der Datei liest, verschwendet sie Zeit damit, fast 3 Milliarden irrelevante Zeilen zu lesen und dauert 6-mal länger als nötig.

Mein Best-Case-Szenario ist eine Lösung, die nur eine einzige Zeile aus der Datei extrahiert, ohne irgendwelche anderen Zeilen in der Datei zu lesen, aber ich weiß nicht, wie ich das in Bash erreichen könnte.

Zum Wohl meiner geistigen Gesundheit werde ich nicht versuchen, die vollständigen 500.000.000 Zeilen zu lesen, die ich für mein eigenes Problem benötigen würde. Stattdessen werde ich versuchen, Zeile 50.000.000 von 3.339.550.320 zu extrahieren (was bedeutet, dass das Lesen der gesamten Datei 60-mal länger dauern wird als nötig).

Ich werde das eingebaute time verwenden, um jeden Befehl zu benchmarken.

Ausgangspunkt

Lassen Sie uns zunächst sehen, wie die head tail Lösung aussieht:

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

real    1m15.321s

Die Ausgangsbasis für Zeile 50 Millionen beträgt 00:01:15.321, wenn ich direkt zu Zeile 500 Millionen gegangen wäre, wäre es wahrscheinlich ~12,5 Minuten.

cut

Ich bin skeptisch bei dieser, aber es ist einen Versuch wert:

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

real    5m12.156s

Dies dauerte 00:05:12.156, was viel langsamer ist als der Ausgangspunkt! Ich bin mir nicht sicher, ob es die gesamte Datei gelesen hat oder nur bis zur Zeile 50 Millionen bevor es aufgehört hat, aber unabhängig davon scheint dies keine praktikable Lösung für das Problem zu sein.

AWK

Ich habe die Lösung nur mit dem exit ausgeführt, weil ich nicht auf das gesamte Ausführen der Datei warten wollte:

$ 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 gegenüber der Ausgangsbasis darstellt. Bei diesem Tempo hätte es wahrscheinlich rund ~76 Minuten gedauert, die gesamte Datei zu lesen, wenn der Exit-Befehl ausgeschlossen worden wäre!

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 ~2 Sekunden schneller ist als die Ausgangsbasis. Wenn ich es auf den vollständigen 500.000.000 Zeilen ausgeführt hätte, hätte es wahrscheinlich ~12 Minuten gedauert.

sed

Die beste Antwort im Board, 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 ist als die Ausgangsbasis und ~0,4 Sekunden schneller als Perl. Wenn ich es auf den vollständigen 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

Es scheint, dass es größtenteils schwierig ist, die head tail Lösung zu verbessern. Am besten bietet die sed Lösung eine ~3%ige Steigerung der Effizienz.

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

Zeile 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

Zeile 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

8 Stimmen

Ich frage mich, wie lange es dauern würde, die gesamte Datei einfach in /dev/null zu katzieren. (Was wäre, wenn dies nur ein Festplatten-Benchmark wäre?)

3 Stimmen

Ich fühle mich dazu veranlasst, vor Ihrem Besitz eines 3+ Gigabyte Textdatei-Wörterbuchs zu verbeugen. Was auch immer der Grund sein mag, das umarmt die Textualität so sehr :)

0 Stimmen

Der Overhead beim Ausführen von zwei Prozessen mit head + tail ist für eine einzelne Datei vernachlässigbar, macht sich jedoch bemerkbar, wenn Sie dies bei vielen Dateien tun.

65voto

fedorqui Punkte 249453

Mit awk geht das ziemlich schnell:

awk 'NR == num_line' file

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


Alternative Versionen

Wenn Ihre Datei zufällig riesig ist, ist es besser, nach dem Lesen der erforderlichen Zeile exit zu verwenden. Auf diese Weise sparen Sie CPU-Zeit Siehe Zeitvergleich am Ende der Antwort.

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

Wenn Sie die Zeilennummer aus 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

Sehen Sie, wie viel Zeit durch Verwendung von exit gespart wird, insbesondere wenn die Zeile zufällig im ersten Teil der Datei liegt:

# Lassen Sie uns eine Datei mit 10 Mio. Zeilen erstellen
for ((i=0; i<100000; i++)); do echo "bla bla"; done > 100Klines
for ((i=0; i<100; i++)); do cat 100Klines; done > 10Mlines

$ time awk 'NR == 1234567 {print}' 10Mlines
bla bla

real    0m1.303s
user    0m1.246s
sys 0m0.042s
$ time awk 'NR == 1234567 {print; exit}' 10Mlines
bla bla

real    0m0.198s
user    0m0.178s
sys 0m0.013s

Der Unterschied beträgt also 0,198s gegenüber 1,303s, ungefähr 6-mal schneller.

3 Stimmen

Diese Methode wird immer langsamer sein, da awk versucht, Feldaufspaltung durchzuführen. Der Overhead der Feldaufspaltung kann reduziert werden durch awk 'BEGIN{FS=RS}(NR == num_line) {print; exit}' file

0 Stimmen

Die eigentliche Stärke von awk in dieser Methode zeigt sich, wenn Sie Zeile n1 von file1, n2 von file2, n3 oder file3 verketten möchten ... awk 'FNR==n' n=10 file1 n=30 file2 n=60 file3. Mit GNU awk kann dies beschleunigt werden mit awk 'FNR==n{print;nextfile}' n=10 file1 n=30 file2 n=60 file3.

0 Stimmen

@kvantour in der Tat, GNU awk's nextfile ist großartig für solche Dinge. Wie kommt es, dass FS=RS das Aufteilen von Feldern vermeidet?

48voto

Philipp Claßen Punkte 36619

Laut meinen Tests lautet meine Empfehlung in Bezug auf Leistung und Lesbarkeit:

tail -n+N | head -1

N ist die Zeilennummer, die Sie möchten. Zum Beispiel wird mit tail -n+7 input.txt | head -1 die 7. Zeile der Datei ausgegeben.

tail -n+N gibt alles ab Zeile N aus, und head -1 lässt es nach einer Zeile stoppen.


Die alternative head -N | tail -1 ist vielleicht etwas lesbbarer. Zum Beispiel wird damit die 7. Zeile ausgegeben:

head -7 input.txt | tail -1

In Bezug auf die Leistung gibt es für kleinere Größen nicht viel Unterschied, aber bei riesigen Dateien wird sie von tail | head (siehe zuvor) übertroffen.

Das am meisten bewertete sed 'NUMq;d' ist interessant zu wissen, aber ich würde argumentieren, dass es von weniger Leuten sofort verstanden wird als die head/tail-Lösung und auch langsamer als tail/head ist.

In meinen Tests wurden beide tail/head-Versionen konsequent schneller als sed 'NUMq;d' ausgeführt. Das entspricht auch den anderen benchmarks, die veröffentlicht wurden. Es ist schwer einen Fall zu finden, in dem tails/heads wirklich schlecht abschneiden. Es ist auch nicht überraschend, da man erwarten würde, dass diese Operationen in einem modernen Unix-System stark optimiert sind.

Um eine Vorstellung von den Leistungsunterschieden zu bekommen, hier sind die Zahlen, die ich für eine riesige Datei (9,3G) erhalte:

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

Die Ergebnisse können variieren, aber die Leistung von head | tail und tail | head ist im Allgemeinen vergleichbar für kleinere Eingaben, während sed immer langsamer ist, sogar um einen signifikanten Faktor (ungefähr 5x oder so).

Um meinen Benchmark zu reproduzieren, können Sie Folgendes ausprobieren, aber seien Sie gewarnt, dass damit eine 9,3G-Datei im aktuellen Arbeitsverzeichnis 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 Rechner (ThinkPad X1 Carbon mit einer SSD und 16G Speicher). Ich gehe davon aus, dass in der endgültigen Ausführung alles aus dem Cache und nicht von der Festplatte kommt:

*** 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

2 Stimmen

Ist die Leistung unterschiedlich zwischen head | tail vs tail | head? Oder hängt es davon ab, welche Zeile gedruckt wird (Anfang der Datei vs. Ende der Datei)?

1 Stimmen

@wisbucky Ich habe keine genauen Zahlen, aber ein Nachteil des zuerst verwendeten Schwanzes, gefolgt von einem "Kopf -1", ist, dass du die Gesamtlänge im Voraus kennen musst. Wenn du es nicht weißt, müsstest du es zuerst zählen, was leistungsbezogen ein Verlust wäre. Ein weiterer Nachteil ist, dass es weniger intuitiv ist zu benutzen. Wenn du zum Beispiel die Zahlen 1 bis 10 hast und die 3. Zeile erhalten möchtest, müsstest du "tail -8 | head -1" verwenden. Das ist fehleranfälliger als "head -3 | tail -1".

1 Stimmen

Entschuldigung, ich hätte ein Beispiel hinzufügen sollen, um klar zu sein. head -5 | tail -1 vs tail -n+5 | head -1. Tatsächlich habe ich eine andere Antwort gefunden, die einen Testvergleich durchgeführt und festgestellt hat, dass tail | head schneller ist. stackoverflow.com/a/48189289

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X