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


604

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

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


10
Der "Unix-Weg" besteht darin, Werkzeuge zu verketten, die ihre jeweilige Aufgabe gut erfüllen. Ich denke, Sie haben bereits eine sehr geeignete Methode gefunden. Andere Methoden sind awkund sedund ich bin sicher, dass jemand auch einen Perl-
Einzeiler

3
Der Doppelbefehl legt nahe, dass die head | tailLösung nicht optimal ist. Andere nahezu optimale Lösungen wurden vorgeschlagen.
Jonathan Leffler

Haben Sie Benchmarks durchgeführt, bei denen die Lösung für einen Durchschnittsfall am schnellsten ist?
Marcin

5
Benchmarks (für einen Bereich) in Cat-Zeile X bis Zeile Y für eine große Datei unter Unix und Linux . (cc @Marcin, falls Sie sich nach mehr als zwei Jahren noch fragen)
Kevin

6
Die head | tailLösung funktioniert nicht, wenn Sie eine Zeile abfragen, die in der Eingabe nicht vorhanden ist: Sie druckt die letzte Zeile.
jarno

Antworten:


801

headund Pipe mit tailwird für eine große Datei langsam sein. Ich würde so vorschlagen sed:

sed 'NUMq;d' file

Wo NUMist die Nummer der Zeile, die Sie drucken möchten? So sed '10q;d' filedrucken Sie beispielsweise die 10. Zeile von file.

Erläuterung:

NUMqwird sofort beendet, wenn die Zeilennummer lautet NUM.

dlöscht die Zeile, anstatt sie zu drucken; Dies ist in der letzten Zeile gesperrt, da dadurch der qRest des Skripts beim Beenden übersprungen wird.

Wenn Sie NUMeine Variable haben, möchten Sie doppelte Anführungszeichen anstelle von einfachen verwenden:

sed "${NUM}q;d" file

44
Für diejenigen fragen, scheint diese Lösung etwa 6 bis 9 - mal schneller als die sed -n 'NUMp'und sed 'NUM!d'Lösungen unten vorgeschlagen.
Skippy le Grand Gourou

75
Ich denke, es tail -n+NUM file | head -n1ist wahrscheinlich genauso schnell oder schneller. Zumindest war es auf meinem System (erheblich) schneller, als ich es mit NUM 250000 in einer Datei mit einer halben Million Zeilen versuchte. YMMV, aber ich verstehe nicht wirklich, warum es so wäre.
Rici

2
@rici (Überarbeitung des früheren Kommentars) Unter Linux (Ubuntu 12.04, Fedora 20) ist die Verwendung catzwar schneller (fast doppelt so schnell), aber nur, wenn die Datei noch nicht zwischengespeichert wurde . Sobald die Datei zwischengespeichert ist , ist die direkte Verwendung des Dateinamenarguments schneller (etwa 1/3 schneller), während die catLeistung gleich bleibt. Seltsamerweise scheint unter OS X 10.9.3 nichts davon einen Unterschied zu machen: cat/ nein cat, zwischengespeicherte Datei oder nicht. @anubhava: mein Vergnügen.
mklement0

2
@SkippyleGrandGourou: der gegebenen besonderen Charakter dieser Optimierung , auch Ihre Bereiche von Zahlen sind sinnlos , da eine allgemeine Aussage . Der einzige allgemeine Vorteil ist folgender: (a) Diese Optimierung kann sicher auf alle Eingaben angewendet werden. (B) Die Auswirkungen reichen von keiner bis dramatisch , abhängig vom Index der gesuchten Linie im Verhältnis zur Anzahl der Gesamtlinien.
mklement0

17
sed 'NUMqgibt die ersten NUMDateien aus und ;dlöscht alle bis auf die letzte Zeile.
Anubhava

304
sed -n '2p' < file.txt

druckt die 2. Zeile

sed -n '2011p' < file.txt

2011th line

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 Zeilen mit sed hinzuzufügen, können Sie Folgendes überprüfen:

sed: Fügen Sie eine Linie an einer bestimmten Position ein


6
@ RafaelBarbosa das ist <in diesem Fall nicht nötig. Es ist einfach meine Präferenz, Weiterleitungen zu verwenden, weil ich oft Weiterleitungen wie sed -n '100p' < <(some_command)- also universelle Syntax :) verwendet habe. Es ist NICHT weniger effektiv, da die Umleitung mit der Shell erfolgt, wenn es sich selbst gabelt, also ... es ist nur eine Präferenz ... (und ja, es ist ein Zeichen länger) :)
jm666

1
@ jm666 Eigentlich sind es 2 Zeichen länger, da Sie normalerweise das '<' sowie ein zusätzliches Leerzeichen '' nach <setzen würden, im Gegensatz zu nur einem Leerzeichen, wenn Sie das <:)
rasen58

2
@ rasen58 ist das Leerzeichen auch ein Charakter? :) / okay, nur ein Scherz - du hast Recht / :)
jm666

1
@duhaime natürlich, wenn jemand Optimierungen vornehmen muss. Aber meiner Meinung nach ist es für die "häufigen" Probleme in Ordnung und der Unterschied ist nicht bemerkbar. Außerdem löst das head/ taildas sed -n '1p;3p'Szenario nicht - auch bekannt als mehr nicht benachbarte Zeilen drucken ...
jm666

1
@duhaime natürlich - die Notiz ist korrekt und benötigt. :)
jm666

93

Ich habe eine einzigartige Situation, in der ich die auf dieser Seite vorgeschlagenen Lösungen vergleichen kann. Daher schreibe ich diese Antwort als Konsolidierung der vorgeschlagenen Lösungen mit jeweils enthaltenen Laufzeiten.

Installieren

Ich habe eine 3,261-Gigabyte-ASCII-Textdatendatei mit einem Schlüssel-Wert-Paar pro Zeile. Die Datei enthält insgesamt 3.339.550.320 Zeilen und kann nicht in jedem Editor geöffnet werden, den ich ausprobiert habe, einschließlich meines Go-to-Vim. Ich muss diese Datei unterteilen, um einige der Werte zu untersuchen, die ich entdeckt habe. Beginnen Sie erst bei Zeile ~ 500.000.000.

Weil die Datei so viele Zeilen hat:

  • Ich muss nur eine Teilmenge der Zeilen extrahieren, um etwas Nützliches mit den Daten zu tun.
  • Das Lesen jeder Zeile, die zu den Werten führt, die mir wichtig sind, wird lange dauern.
  • Wenn die Lösung über die Zeilen hinaus liest, die mir wichtig sind, und den Rest der Datei weiter liest, verschwendet sie Zeit mit dem Lesen von fast 3 Milliarden irrelevanten Zeilen und dauert 6x länger als nötig.

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

Aus Gründen meiner geistigen Gesundheit werde ich nicht versuchen, die gesamten 500.000.000 Zeilen zu lesen, die ich für mein eigenes Problem benötige. Stattdessen werde ich versuchen, Zeile 50.000.000 aus 3.339.550.320 zu extrahieren (was bedeutet, dass das Lesen der vollständigen Datei 60x länger dauert als nötig).

Ich werde das timeeingebaute verwenden, um jeden Befehl zu bewerten.

Basislinie

Lassen Sie uns zuerst sehen, wie die head tailLösung:

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

real    1m15.321s

Die Basis für Zeile 50 Millionen ist 00: 01: 15.321. Wenn ich direkt für Zeile 500 Millionen gegangen wäre, 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

Dieser dauerte 00: 05: 12.156, was viel langsamer als die Grundlinie ist! Ich bin mir nicht sicher, ob es die gesamte Datei durchliest oder nur bis zu 50 Millionen Zeilen vor dem Stoppen, aber unabhängig davon scheint dies keine praktikable Lösung für das Problem zu sein.

AWK

Ich habe die Lösung nur mit ausgeführt, exitweil ich nicht auf die Ausführung der vollständigen 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 der Basislinie darstellt. Bei dieser Geschwindigkeit hätte das Lesen der gesamten Datei wahrscheinlich ungefähr 76 Minuten gedauert, 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 wurde in 00: 01: 13.146 ausgeführt, was ~ 2 Sekunden schneller als die Basislinie ist. Wenn ich es mit den vollen 500.000.000 laufen lassen würde, würde es wahrscheinlich ~ 12 Minuten dauern.

sed

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

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

real    1m12.705s

Dieser Code wurde in 00: 01: 12.705 ausgeführt, was 3 Sekunden schneller als die Basislinie und ~ 0,4 Sekunden schneller als Perl ist. Wenn ich es auf den vollen 500.000.000 Zeilen ausführen würde, hätte es wahrscheinlich ~ 12 Minuten gedauert.

Mapfile

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

Fazit

Es sieht zum größten Teil so aus, als ob es schwierig ist, die head tailLösung zu verbessern . Bestenfalls sedbietet die Lösung eine Effizienzsteigerung von ~ 3%.

(Prozentsätze berechnet mit der Formel % = (runtime/baseline - 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

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

Ich verspüre den perversen Drang, mich vor Ihrem Besitz eines 3+ Gig-Textdatei-Wörterbuchs zu verbeugen. Was auch immer die
Gründe sein mögen

51

Damit awkgeht es ziemlich schnell:

awk 'NR == num_line' file

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


Alternative Versionen

Wenn Ihre Datei sehr groß ist, sollten Sie exitdie gewünschte Zeile lesen. Auf diese Weise sparen Sie CPU-Zeit. Siehe Zeitvergleich am Ende der Antwort .

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

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

# Let's create a 10M lines file
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,198 Sekunden gegenüber 1,303 Sekunden und ist damit etwa sechsmal schneller.


Diese Methode wird immer langsamer sein, da awk versucht, Felder aufzuteilen. Der Aufwand für Feldaufspaltung kann reduziert werdenawk 'BEGIN{FS=RS}(NR == num_line) {print; exit}' file
kvantour

Die wahre Kraft von awk bei dieser Methode entsteht, wenn Sie Zeile n1 von Datei1, n2 von Datei2, n3 oder Datei3 ... verketten möchten awk 'FNR==n' n=10 file1 n=30 file2 n=60 file3. Mit GNU awk kann dies mit beschleunigt werden awk 'FNR==n{print;nextfile}' n=10 file1 n=30 file2 n=60 file3.
kvantour

@kvantour in der Tat, GNU awks nächste Datei ist großartig für solche Dinge. Wie kommt es, dass FS=RSFeldspaltungen vermieden werden?
fedorqui 'SO hör auf zu schaden'

1
FS=RSkeine Feldaufspaltung vermeiden, aber es parst nur die $ 0 Einsen und weist nur ein Feld , weil es keine RSin$0
kvantour

@kvantour Ich habe einige Tests mit gemacht FS=RSund keinen Unterschied in den Timings gesehen. Was ist mit mir, wenn ich eine Frage dazu stelle, damit Sie expandieren können? Vielen Dank!
fedorqui 'SO hör auf zu schaden'

29

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

tail -n+N | head -1

Nist die gewünschte Zeilennummer. Druckt beispielsweise tail -n+7 input.txt | head -1die 7. Zeile der Datei.

tail -n+Ndruckt alles beginnend mit der Zeile Nund lässt head -1es nach einer Zeile anhalten.


Die Alternative head -N | tail -1ist vielleicht etwas besser lesbar. Dies druckt beispielsweise die 7. Zeile:

head -7 input.txt | tail -1

Wenn es um die Leistung geht, gibt es keinen großen Unterschied für kleinere Größen, aber es wird von tail | head(von oben) übertroffen, wenn die Dateien riesig werden.

Das Top-Voting sed 'NUMq;d'ist interessant zu wissen, aber ich würde argumentieren, dass es von weniger Leuten verstanden wird als die Head / Tail-Lösung und es ist auch langsamer als Tail / Head.

In meinen Tests übertrafen beide Schwanz- / Kopfversionen sed 'NUMq;d'konstant. Dies steht im Einklang mit den anderen veröffentlichten Benchmarks. Es ist schwer, einen Fall zu finden, in dem Schwänze / Köpfe wirklich schlecht waren. Es ist auch nicht überraschend, da dies Vorgänge sind, von denen Sie erwarten würden, dass sie in einem modernen Unix-System stark optimiert werden.

Um eine Vorstellung von den Leistungsunterschieden zu bekommen, ist dies die Nummer, die ich für eine große Datei (9,3 G) 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 | tailund tail | headist in der Regel für kleinere Eingänge vergleichbar und sedist immer langsamer um einen signifikanten Faktor (etwa 5 - fach oder so).

Um meinen Benchmark zu reproduzieren, können Sie Folgendes versuchen, aber seien Sie gewarnt, dass 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 Computer (ThinkPad X1 Carbon mit einer SSD und 16 GB Speicher). Ich gehe davon aus, dass im letzten Durchlauf alles aus dem 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

1
Unterscheidet sich die Leistung zwischen head | tailvs tail | head? Oder hängt es davon ab, welche Zeile gedruckt wird (Dateianfang gegen Dateiende)?
wisbucky

1
@wisbucky Ich habe keine harten Zahlen, aber ein Nachteil der ersten Verwendung von Schwanz gefolgt von einem "Kopf -1" ist, dass Sie die Gesamtlänge im Voraus kennen müssen. Wenn Sie es nicht wissen, müssen Sie es zuerst zählen, was in Bezug auf die Leistung einen Verlust bedeutet. Ein weiterer Nachteil ist, dass die Bedienung weniger intuitiv ist. Wenn Sie beispielsweise die Nummern 1 bis 10 haben und die 3. Zeile erhalten möchten, müssen Sie "tail -8 | head -1" verwenden. Das ist fehleranfälliger als "head -3 | tail -1".
Philipp Claßen

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

1
@ wisbucky Danke, dass du es erwähnt hast! Ich habe einige Tests durchgeführt und muss zustimmen, dass es immer etwas schneller war, unabhängig von der Position der Linie von dem, was ich gesehen habe. Vor diesem Hintergrund habe ich meine Antwort geändert und auch den Benchmark aufgenommen, falls jemand ihn reproduzieren möchte.
Philipp Claßen

27

Wow, alle Möglichkeiten!

Versuche dies:

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

oder eine davon, abhängig von Ihrer Version von Awk:

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 nawkoder ausprobierengawk .)

Gibt es ein Werkzeug, mit dem nur diese bestimmte Zeile gedruckt wird? Keines der Standardwerkzeuge. Ist sedjedoch wahrscheinlich am nächsten und am einfachsten zu bedienen.



21

Diese Frage ist mit Bash gekennzeichnet. Hier ist die Vorgehensweise für Bash (≥4): Verwenden Sie sie mapfilemit den Optionen -s(Überspringen) und -n(Zählen).

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

mapfile -s 41 -n 1 ary < file

Zu diesem Zeitpunkt haben Sie ein Array, arydessen Felder die Zeilen von file(einschließlich der nachfolgenden neuen Zeile) enthalten, in denen wir die ersten 41 Zeilen ( -s 41) übersprungen und nach dem Lesen einer Zeile ( -n 1) angehalten haben . Das ist also wirklich die 42. Zeile. So drucken Sie es aus:

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

Wenn Sie einen Zeilenbereich benötigen, sagen Sie den Bereich 42–666 (einschließlich) und sagen Sie, dass Sie die Berechnung nicht selbst durchführen möchten, und drucken Sie sie auf stdout:

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

Wenn Sie diese Zeilen auch verarbeiten müssen, ist es nicht sehr praktisch, die nachfolgende neue Zeile zu speichern. Verwenden Sie in diesem Fall die -tOption (Trimmen):

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

Sie können eine Funktion haben, die das für Sie erledigt:

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 eingebaute Bash!


11

Sie können auch sed print verwenden und beenden:

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

6
Die -nOption deaktiviert die Standardaktion zum Drucken jeder Zeile, wie Sie sicherlich durch einen kurzen Blick auf die Manpage herausgefunden hätten.
Tripleee

In GNU sind sed alle sedAntworten ungefähr gleich schnell. Daher (für GNU sed ) ist dies die beste sedAntwort, da dies Zeit für große Dateien und kleine n-te Zeilenwerte sparen würde .
Agc

7

Sie können auch Perl dafür verwenden:

perl -wnl -e '$.== NUM && print && exit;' some.file

6

Die schnellste Lösung für große Dateien ist immer tail | head, vorausgesetzt, die beiden Entfernungen:

  • vom Anfang der Datei bis zur Startzeile. Nennen wir esS
  • der Abstand von der letzten Zeile bis zum Ende der Datei. Sei esE

sind bekannt. Dann könnten wir dies verwenden:

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

Howmany ist nur die Anzahl der erforderlichen Zeilen.

Weitere Informationen finden Sie unter https://unix.stackexchange.com/a/216614/79743


1
Bitte klären Sie die Einheiten von Sund E(dh Bytes, Zeichen oder Zeilen).
Agc

6

Alle oben genannten Antworten beantworten die Frage direkt. Aber hier ist eine weniger direkte Lösung, aber eine möglicherweise wichtigere Idee, um zum Nachdenken anzuregen.

Da Leitungslängen beliebig sind, werden alle Bytes der Datei vor der n - ten Zeile Bedarf zu lesen. Wenn Sie eine große Datei haben oder diese Aufgabe viele Male wiederholen müssen und dieser Vorgang zeitaufwändig ist, sollten Sie ernsthaft darüber nachdenken, ob Sie Ihre Daten überhaupt anders speichern sollten.

Die eigentliche Lösung besteht darin, einen Index zu haben, z. B. am Anfang der Datei, der die Positionen angibt, an denen die Linien beginnen. Sie können ein Datenbankformat verwenden oder einfach eine Tabelle am Anfang der Datei hinzufügen. Alternativ können Sie eine separate Indexdatei erstellen, die Ihrer großen Textdatei beiliegt.

Sie können beispielsweise 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 lesen Sie mit tail, was eigentlich seekdirekt zum entsprechenden Punkt in der Datei ist!

zB um Zeile 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 gegen eine große Datei getestet.
  • Siehe auch diese Antwort .
  • Alternativ - teilen Sie Ihre Datei in kleinere Dateien!

5

Als Folge der sehr hilfreichen Benchmarking-Antwort von CaffeineConnoisseur ... Ich war neugierig, wie schnell die 'Mapfile'-Methode mit anderen verglichen wurde (da diese nicht getestet wurde), und habe selbst einen schnellen und schmutzigen Geschwindigkeitsvergleich versucht Ich habe Bash 4 zur Hand. Wirf einen Test der "tail | head" -Methode (anstelle von head | tail) durch, die in einem der Kommentare zur Top-Antwort erwähnt wurde, als ich dabei war, während die Leute ihr Lob singen. Ich habe nicht annähernd die Größe der verwendeten Testdatei. Das Beste, was ich kurzfristig finden konnte, war eine 14-Millionen-Stammbaumdatei (lange Zeilen, die durch Leerzeichen getrennt sind, knapp 12000 Zeilen).

Kurzversion: Mapfile erscheint schneller als die Cut-Methode, aber langsamer als alles andere, also würde ich es einen Dud nennen. Schwanz | Kopf, OTOH, sieht so aus, als könnte es das 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

Mit dem, was andere erwähnten, wollte ich, dass dies eine schnelle und schnelle 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üge dies zu deinem hinzu ~/.bash_profile:

source ~/.functions

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

getline 441 myfile.txt


3

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

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

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


1
Kann auch zum Anzeigen mehrerer Zeilen verwendet werden: cat FILE | cut -f2,5 -d$'\n'Zeigt die Zeilen 2 und 5 der DATEI an. (Aber es wird die Ordnung nicht bewahren.)
Andriy Makukha

2

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

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

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


2
Der Doppelpunkt ist ein Syntaxfehler und sollte ein Semikolon sein.
Tripleee

2

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

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

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

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

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


1

Nachdem ich mir die Top-Antwort und den Benchmark angesehen habe , habe ich eine winzige Hilfsfunktion implementiert:

function nth {
    if (( ${#} < 1 || ${#} > 2 )); then
        echo -e "usage: $0 \e[4mline\e[0m [\e[4mfile\e[0m]"
        return 1
    fi
    if (( ${#} > 1 )); then
        sed "$1q;d" $2
    else
        sed "$1q;d"
    fi
}

Grundsätzlich können Sie es auf zwei Arten verwenden:

nth 42 myfile.txt
do_stuff | nth 42

0

Ich habe einige der oben genannten Antworten in ein kurzes Bash-Skript eingefügt, das Sie in eine Datei mit dem Namen get.shund einem Link zu /usr/local/bin/get(oder einem anderen von Ihnen bevorzugten Namen) einfügen 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 dem PATHmit verfügbar zu machen

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

Verantwortungsvoll geniessen!

P.

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.