Wie kann ich die erste Zeile einer Textdatei mit dem Skript bash / sed entfernen?

554

Ich muss die erste Zeile wiederholt mit einem Bash-Skript aus einer riesigen Textdatei entfernen.

Im Moment verwende ich sed -i -e "1d" $FILE- aber das Löschen dauert ungefähr eine Minute.

Gibt es einen effizienteren Weg, um dies zu erreichen?

Brent
quelle
Wofür steht -i?
Cikatomo
4
@cikatomo: Es steht für Inline-Bearbeitung - es bearbeitet die Datei mit allem, was Sie generieren.
Drawrockshard
4
Schwanz ist viel langsamer als sed. Schwanz braucht 13,5s, sed braucht 0,85s. Meine Datei hat ~ 1M Zeilen, ~ 100MB. MacBook Air 2013 mit SSD.
Jcsahnwaldt sagt GoFundMonica

Antworten:

1029

Versuchen Sie Schwanz :

tail -n +2 "$FILE"

-n x: Drucken Sie einfach die letzten xZeilen aus. tail -n 5würde Ihnen die letzten 5 Zeilen der Eingabe geben. Das +Zeichen kehrt das Argument um und lässt tailalles andere als die ersten x-1Zeilen drucken . tail -n +1würde die ganze Datei drucken, tail -n +2alles außer der ersten Zeile usw.

GNU tailist viel schneller als sed. tailist auch für BSD verfügbar und das -n +2Flag ist für beide Tools konsistent. Weitere Informationen finden Sie in den FreeBSD- oder OS X- Manpages.

Die BSD-Version kann jedoch viel langsamer sein als sed. Ich frage mich, wie sie das geschafft haben. tailsollte nur eine Datei Zeile für Zeile lesen, während sedziemlich komplexe Vorgänge ausgeführt werden, bei denen ein Skript interpretiert, reguläre Ausdrücke angewendet werden und dergleichen.

Hinweis: Sie könnten versucht sein, zu verwenden

# THIS WILL GIVE YOU AN EMPTY FILE!
tail -n +2 "$FILE" > "$FILE"

Dies gibt Ihnen jedoch eine leere Datei . Der Grund ist, dass die Umleitung ( >) erfolgt, bevor sie tailvon der Shell aufgerufen wird:

  1. Shell schneidet Datei ab $FILE
  2. Shell erstellt einen neuen Prozess für tail
  3. Shell leitet stdout des tailProzesses an weiter$FILE
  4. tail liest aus dem jetzt leer $FILE

Wenn Sie die erste Zeile in der Datei entfernen möchten, sollten Sie Folgendes verwenden:

tail -n +2 "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE"

Dadurch &&wird sichergestellt, dass die Datei bei einem Problem nicht überschrieben wird.

Aaron Digulla
quelle
3
Laut dieser ss64.com/bash/tail.html beträgt der typische Puffer standardmäßig 32 KB , wenn BSD 'tail' mit der -rOption verwendet wird. Vielleicht gibt es irgendwo im System eine Puffereinstellung? Oder -nist eine 32-Bit-Nummer signiert?
Yzmir Ramirez
41
@Eddie: user869097 sagte, dass es nicht funktioniert, wenn eine einzelne Zeile 15 MB oder mehr ist. Solange die Zeilen kürzer sind, tailfunktioniert dies für jede Dateigröße.
Aaron Digulla
6
Könntest du diese Argumente erklären?
Dreampuf
17
@ Dreampuf - von der Manpage:-n N means output the last N lines, instead of the last 10; or use +N to output lines starting with the Nth
Will Sheppard
11
Ich wollte @JonaChristopherSahnwaldt zustimmen - Schwanz ist um eine Größenordnung viel, viel langsamer als die sed-Variante. Ich teste es in einer Datei mit 500.000.000 Zeilen (nicht mehr als 50 Zeichen pro Zeile). Dann wurde mir jedoch klar, dass ich die FreeBSD-Version von tail verwendete (die standardmäßig mit OS X geliefert wird). Als ich zu GNU Tail wechselte, war der Tail Call zehnmal schneller als der Sed Call (und auch der GNU Sed Call). AaronDigulla ist hier richtig, wenn Sie GNU verwenden.
Dan Nguyen
179

Sie können -i verwenden, um die Datei zu aktualisieren, ohne den Operator '>' zu verwenden. Der folgende Befehl löscht die erste Zeile aus der Datei und speichert sie in der Datei.

sed -i '1d' filename
amit
quelle
1
Ich bekomme eine Fehlermeldung:unterminated transform source string
Daniel Kobe
10
Dies funktioniert jedes Mal und sollte wirklich die beste Antwort sein!
xtheking
4
Zur Erinnerung: Für den Mac muss ein Suffix angegeben werden, wenn sed mit direkten Änderungen verwendet wird. Führen Sie die obigen Schritte mit -i.bak
mjp
3
Nur eine Notiz - um mehrere Zeilen zu entfernen, verwenden Siesed -i '1,2d' filename
The Godfather
4
Diese Version ist wirklich viel lesbarer und universeller als tail -n +2. Ich bin mir nicht sicher, warum es nicht die beste Antwort ist.
Luke Davis
74

Für diejenigen, die unter SunOS arbeiten, das kein GNU ist, hilft der folgende Code:

sed '1d' test.dat > tmp.dat 
Nasri Najib
quelle
18
Interessante demografische
Kapitän
17

Nein, das ist ungefähr so ​​effizient, wie Sie es sich vorstellen können. Sie könnten ein C-Programm schreiben, das die Arbeit etwas schneller erledigen könnte (weniger Startzeit und weniger Verarbeitungsargumente), aber es tendiert wahrscheinlich zu der gleichen Geschwindigkeit wie sed, wenn Dateien groß werden (und ich gehe davon aus, dass sie groß sind, wenn es eine Minute dauert ).

Ihre Frage leidet jedoch unter dem gleichen Problem wie so viele andere, dass sie die Lösung voraussetzt. Wenn Sie uns im Detail mitteilen, was Sie versuchen und nicht wie , können wir Ihnen möglicherweise eine bessere Option vorschlagen.

Wenn dies beispielsweise eine Datei A ist, die von einem anderen Programm B verarbeitet wird, besteht eine Lösung darin, die erste Zeile nicht zu entfernen, sondern Programm B so zu ändern, dass sie anders verarbeitet wird.

Angenommen, alle Ihre Programme hängen an diese Datei A an, und Programm B liest und verarbeitet derzeit die erste Zeile, bevor sie gelöscht wird.

Sie können Programm B so umgestalten, dass es nicht versucht, die erste Zeile zu löschen, sondern einen dauerhaften (wahrscheinlich dateibasierten) Offset in der Datei A beibehält, sodass es bei der nächsten Ausführung nach diesem Offset-Prozess suchen kann die Linie dort, und aktualisieren Sie den Versatz.

Dann könnte es zu einer ruhigen Zeit (Mitternacht?) Eine spezielle Verarbeitung von Datei A durchführen, um alle aktuell verarbeiteten Zeilen zu löschen und den Versatz auf 0 zurückzusetzen.

Es wird sicherlich schneller für ein Programm sein, eine Datei zu öffnen und zu suchen, als sie zu öffnen und neu zu schreiben. Diese Diskussion setzt natürlich voraus, dass Sie die Kontrolle über Programm B haben. Ich weiß nicht, ob dies der Fall ist, aber es kann andere mögliche Lösungen geben, wenn Sie weitere Informationen bereitstellen.

paxdiablo
quelle
Ich denke, das OP versucht zu erreichen, warum ich diese Frage gefunden habe. Ich habe 10 CSV-Dateien mit jeweils 500.000 Zeilen. Jede Datei hat dieselbe Kopfzeile wie die erste Zeile. Ich fasse diese Dateien in eine Datei zusammen und importiere sie dann in eine Datenbank, damit die Datenbank Spaltennamen aus der ersten Zeile erstellen kann. Natürlich möchte ich nicht, dass diese Zeile in Datei 2-10 wiederholt wird.
DB
1
@db In diesem Fall awk FNR-1 *.csvist wahrscheinlich schneller.
Jinawee
10

Sie können die vorhandenen Dateien bearbeiten: Verwenden Sie einfach das Perl- -iFlag wie folgt:

perl -ni -e 'print unless $. == 1' filename.txt

Dadurch verschwindet die erste Zeile, wenn Sie fragen. Perl muss die gesamte Datei lesen und kopieren, sorgt jedoch dafür, dass die Ausgabe unter dem Namen der Originaldatei gespeichert wird.

alexis
quelle
10

Sie können dies leicht tun mit:

cat filename | sed 1d > filename_without_first_line

in der Kommandozeile; oder um die erste Zeile einer Datei dauerhaft zu entfernen, verwenden Sie den In-Place-Modus von sed mit dem -iFlag:

sed -i 1d <filename>
Ingo Baab
quelle
9

Wie Pax sagte, werden Sie wahrscheinlich nicht schneller werden. Der Grund dafür ist, dass es fast keine Dateisysteme gibt, die das Abschneiden vom Anfang der Datei unterstützen. Dies ist also eine O ( n) -Operation, bei der ndie Größe der Datei angegeben ist. Was Sie jedoch viel schneller tun können, ist, die erste Zeile mit der gleichen Anzahl von Bytes (möglicherweise mit Leerzeichen oder einem Kommentar) zu überschreiben, was für Sie möglicherweise funktioniert, je nachdem, was Sie genau versuchen (was ist das übrigens?).

Robert Gamble
quelle
Betreff "... fast keine Dateisysteme, die das Abschneiden unterstützen ..." : das ist interessant; Bitte fügen Sie eine Anmerkung in Klammern hinzu, in der ein solches Dateisystem genannt wird.
Agc
1
@agc: Jetzt irrelevant, aber mein erster Job in den 70ern war bei Quadex, einem kleinen Startup (jetzt weg und unabhängig von den beiden Unternehmen, die jetzt diesen Namen verwenden). Sie hatten ein Dateisystem, das das Hinzufügen oder Entfernen am Anfang oder Ende einer Datei ermöglichte und hauptsächlich zum Implementieren der Bearbeitung in weniger als 3 KB verwendet wurde, indem Dateien über und unter Fenster eingefügt wurden. Es hatte keinen eigenen Namen, es war nur ein Teil von QMOS, dem Quadex Multiuser-Betriebssystem. ('Multi' war normalerweise 2-3 auf einem LSI-11/02 mit weniger als 64 KB RAM und normalerweise ein paar 8 "-Disketten vom Typ RX01 mit jeweils 250 KB.) :-)
dave_thompson_085
9

Das spongeUtil vermeidet das Jonglieren einer temporären Datei:

tail -n +2 "$FILE" | sponge "$FILE"
agc
quelle
spongeist in der Tat viel sauberer und robuster als die akzeptierte Lösung ( tail -n +2 "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE")
Jealie
1
Es sollte klargestellt werden, dass für "Schwamm" das Paket "moreutils" installiert werden muss.
FedFranzoni
Dies ist die einzige Lösung, mit der ich eine Systemdatei (auf einem Debian-Docker-Image) ändern konnte. Andere Lösungen sind aufgrund des Fehlers "Gerät oder Ressource ausgelastet" beim Versuch, die Datei zu schreiben, fehlgeschlagen.
FedFranzoni
Aber spongepuffert die gesamte Datei im Speicher? Das wird nicht funktionieren, wenn es Hunderte von GB sind.
OrangeDog
@OrangeDog, Solange das Dateisystem es speichern kann, spongenimmt es auf, da es eine / tmp- Datei als Zwischenschritt verwendet, die anschließend verwendet wird, um das Original zu ersetzen.
Agc
8

Wenn Sie die Datei an seinem Platz ändern möchten, können Sie immer die Original verwenden edstatt dessen s treaming Nachfolger sed:

ed "$FILE" <<<$'1d\nwq\n'

Der edBefehl war der ursprüngliche UNIX-Texteditor, bevor es überhaupt Vollbild-Terminals gab, geschweige denn grafische Workstations. Der exEditor, am besten bekannt als das, was Sie verwenden , wenn die Eingabe an dem Doppelpunkt prompt in vi, ist eine ex neigten Version ed, so viele der gleichen Befehle zu arbeiten. Während edes interaktiv verwendet werden soll, kann es auch im Batch-Modus verwendet werden, indem eine Reihe von Befehlen an ihn gesendet wird, was diese Lösung tut.

Die Sequenz <<<$'1d\nwq\n'nutzt Unterstützung des Schlages - für-strings hier ( <<<) und POSIX Anführungszeichen ( $'... ') einzuspeisen Eingabe in den edBefehl , bestehend aus zwei Leitungen: 1d, die d eletes Linie 1 , und dann wq, die w Riten die Datei wieder heraus Scheibe und dann q UITS die Bearbeitungssitzung.

Mark Reed
quelle
das ist elegant. +1
Armin
Sie müssen jedoch die gesamte Datei in den Speicher einlesen, was bei Hunderten von GB nicht funktioniert.
OrangeDog
5

sollte die Zeilen mit Ausnahme der ersten Zeile anzeigen:

cat textfile.txt | tail -n +2
serup
quelle
4
- Sie sollten "tail -n +2 textfile.txt"
niglesias
5
@niglesiais Ich bin mit der "nutzlosen Verwendung von Katze" nicht einverstanden, da klar ist, dass diese Lösung für weitergeleitete Inhalte und nicht nur für Dateien in Ordnung ist.
Titou
5

Könnte vim verwenden, um dies zu tun:

vim -u NONE +'1d' +'wq!' /tmp/test.txt

Dies sollte schneller sein, da vim beim Prozess nicht die gesamte Datei liest.

Hongbo Liu
quelle
Möglicherweise müssen Sie das zitieren, +wq!wenn Ihre Shell Bash ist. Wahrscheinlich nicht, da das !nicht am Anfang eines Wortes steht, aber die Gewohnheit, Dinge zu zitieren, ist wahrscheinlich überall gut. (Und wenn Sie Super-Effizienz anstreben, indem Sie nicht unnötig zitieren, brauchen Sie auch keine Anführungszeichen 1d.)
Mark Reed
vim muss die gesamte Datei lesen. Wenn die Datei größer als der Speicher ist, wie in dieser Frage beschrieben, liest vim die gesamte Datei und schreibt sie (oder den größten Teil davon) in eine temporäre Datei. Nach dem Bearbeiten wird alles zurückgeschrieben (in die permanente Datei). Ich weiß nicht, wie Sie denken, dass es ohne dies möglicherweise funktionieren könnte .
Dave_thompson_085
4

Wie wäre es mit csplit?

man csplit
csplit -k file 1 '{1}'
Shahbaz
quelle
Diese Syntax würde auch funktionieren, aber nur zwei Ausgabedateien anstelle von drei generieren : csplit file /^.*$/1. Oder einfacher : csplit file //1. Oder noch einfacher : csplit file 2.
Marco Roy
1

Da es sich so anhört, als könnte ich das Löschen nicht beschleunigen, könnte ein guter Ansatz darin bestehen, die Datei in Stapeln wie diesen zu verarbeiten:

While file1 not empty
  file2 = head -n1000 file1
  process file2
  sed -i -e "1000d" file1
end

Der Nachteil davon ist, dass, wenn das Programm in der Mitte beendet wird (oder wenn es eine schlechte SQL darin gibt - was dazu führt, dass der "Prozess" -Teil stirbt oder blockiert), Zeilen vorhanden sind, die entweder übersprungen oder zweimal verarbeitet werden .

(Datei1 enthält Zeilen mit SQL-Code)

Brent
quelle
Was enthält die erste Zeile? Kannst du es einfach mit einem SQL-Kommentar überschreiben, wie ich es in meinem Beitrag vorgeschlagen habe?
Robert Gamble
0

Wenn Sie nach einem Fehler eine Wiederherstellung durchführen möchten, können Sie einfach eine Datei erstellen, die das enthält, was Sie bisher getan haben.

if [[ -f $tmpf ]] ; then
    rm -f $tmpf
fi
cat $srcf |
    while read line ; do
        # process line
        echo "$line" >> $tmpf
    done
Tim
quelle
0

Dieser eine Liner reicht aus:

echo "$(tail -n +2 "$FILE")" > "$FILE"

Es funktioniert, da tailes vor ausgeführt wird echound dann die Datei entsperrt wird, sodass keine temporäre Datei erforderlich ist.

egors
quelle
-1

Würde es funktionieren, Tail in N-1-Zeilen zu verwenden und diese in eine Datei zu leiten, gefolgt vom Entfernen der alten Datei und dem Umbenennen der neuen Datei in den alten Namen?

Wenn ich dies programmgesteuert tun würde, würde ich die Datei durchlesen und mich nach dem Lesen jeder Zeile an den Dateiversatz erinnern, damit ich an diese Position zurückkehren könnte, um die Datei mit einer Zeile weniger darin zu lesen.

EvilTeach
quelle
Die erste Lösung ist im Wesentlichen identisch mit der, die Brent jetzt macht. Ich verstehe Ihren programmatischen Ansatz nicht, nur die erste Zeile muss gelöscht werden. Sie würden einfach die erste Zeile lesen und verwerfen und den Rest in eine andere Datei kopieren, die wiederum mit den Ansätzen sed und tail identisch ist.
Robert Gamble
Die zweite Lösung hat zur Folge, dass die Datei nicht jedes Mal um die erste Zeile verkleinert wird. Das Programm verarbeitet es einfach so, als wäre es geschrumpft, beginnt aber jedes Mal in der nächsten Zeile
EvilTeach
Ich verstehe immer noch nicht, was Ihre zweite Lösung ist.
Robert Gamble