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.
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
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:
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:
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 head
tail
:
$ 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 head
tail
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
sed
Perl
head|tail
awk
cut
Reihe 500.000.000
sed
Perl
head|tail
awk
cut
Zeile 3,338,559,320
sed
Perl
head|tail
awk
cut
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}
.
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
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 gawk
ausfü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.
# print line number 52
sed '52!d' file
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!
Sie können auch sed drucken und beenden:
sed -n '10{p;q;}' file # print line 10
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 sekhead -N | tail -1
: 4,6 seksed Nq;d
: 18,8 sekDie 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
Sie können dazu auch Perl verwenden:
Perl -wnl -e '$.== NUM && print && exit;' some.file
Die schnellste Lösung für große Dateien ist immer Tail, sofern die beiden Entfernungen:
S
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
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!
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 seek
s 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
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.
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
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
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.
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