Wie teile ich eine Datei und behalte die erste Zeile in jedem der Teile?

73

Gegeben: Eine große Textdatendatei (z. B. CSV-Format) mit einer speziellen ersten Zeile (z. B. Feldnamen).

Gesucht: Entspricht dem split -lBefehl coreutils , jedoch mit der zusätzlichen Anforderung, dass die Kopfzeile aus der Originaldatei am Anfang jedes der resultierenden Teile angezeigt wird.

Ich vermute eine Erfindung splitund headwerde den Trick machen?

Arkady
quelle
11
Es scheint vernünftig, dass jemand das als eingebaute Funktion von hinzufügt, splitnicht wahr?
Bis auf weiteres angehalten.
1
Wahrscheinlich der größte Faktor gegen diese eine eingebaute in immer ist , dass Sie in der Regel eine geteilte Datei rekonstruieren , indem Sie cat a b c > reconstructed. Überflüssige Zeilen in der Datei bedeuten, dass der normale Rekonstruktionsansatz die Originaldatei nicht reproduziert.
Mark Rushakoff
2
Dafür ist das kommende ( nicht ) " unsplit --remove-header" Dienstprogramm gedacht! Aber im Ernst, splitwenn es eine "Repeat-Header" -Option geben sollte, sollte es immer noch standardmäßig sein aktuelles Verhalten verwenden. Sie würden nur Header-Inhalte verwenden, wenn Sie es wirklich wollten.
Bis auf weiteres angehalten.
2
Ja, ich denke, es --keep-first Nwäre eine nette Option, splitdie sowohl im Zeilen- als auch im Byte-Modus nützlich wäre
Arkady
1
Ich denke, es ist eine gute Idee - absolut sehr nützlich, um eine Datei für die Verteilung und nicht für die Rekonstruktion aufzuteilen. Es ist eine dieser "so einfachen, wie ist es noch nicht da" -Funktionen eines so alten Unix-Dienstprogramms, dass ich skeptisch bin, dass die "Verantwortlichen" aus irgendeinem Grund frühere Vorschläge zur Ausführung dieser genauen Funktionalität nicht abgelehnt haben oder ein anderes.
Mark Rushakoff

Antworten:

62

Dies ist Robhruskas Drehbuch, das ein bisschen aufgeräumt wurde:

tail -n +2 file.txt | split -l 4 - split_
for file in split_*
do
    head -n 1 file.txt > tmp_file
    cat "$file" >> tmp_file
    mv -f tmp_file "$file"
done

Ich entfernte wc, cut, lsund echoan den Orten , wo sie nicht notwendig sind. Ich habe einige der Dateinamen geändert, um sie ein wenig aussagekräftiger zu machen. Ich habe es in mehrere Zeilen aufgeteilt, um das Lesen zu erleichtern.

Wenn Sie Lust haben, können Sie einen temporären Dateinamen verwenden mktempoder tempfileerstellen, anstatt einen fest codierten zu verwenden.

Bearbeiten

Mit GNU ist splitdies möglich:

split_filter () { { head -n 1 file.txt; cat; } > "$FILE"; }; export -f split_filter; tail -n +2 file.txt | split --lines=4 --filter=split_filter - split_

Aus Gründen der Lesbarkeit aufgeschlüsselt:

split_filter () { { head -n 1 file.txt; cat; } > "$FILE"; }
export -f split_filter
tail -n +2 file.txt | split --lines=4 --filter=split_filter - split_

Wenn --filterangegeben, wird splitder Befehl (in diesem Fall eine Funktion, die exportiert werden muss) für jede Ausgabedatei ausgeführt und die Variable FILEin der Befehlsumgebung auf den Dateinamen gesetzt.

Ein Filterskript oder eine Funktion kann jede gewünschte Manipulation des Ausgabeinhalts oder sogar des Dateinamens vornehmen. Ein Beispiel für Letzteres könnte die Ausgabe an einen festen Dateinamen in einem Variablenverzeichnis sein: > "$FILE/data.dat"zum Beispiel.

Bis auf weiteres angehalten.
quelle
Das wird sicherlich funktionieren. Ich hatte nur auf einen glatten Einzeiler gehofft wiefor $part in (split -l 1000 myfile); cat <(head -n1 myfile) $part > myfile.$part; done
Arkady
Das kann nicht funktionieren, weil es splitnotwendigerweise nicht ausgegeben wird stdout.
Bis auf weiteres angehalten.
split konnte geben die Namen der Dateien auf stdout, wenn auch (solange wir diskutieren , was split sollte tun :-)
Arkady
Du hast recht. Das könnte praktisch sein . Entschuldigung, ich habe Ihren Einzeiler falsch verstanden.
Bis auf weiteres angehalten.
1
@JohnathanElmore: Beachten Sie, dass GNU-Dienstprogramme für OS X verfügbar sind. Verwenden Sie beispielsweise Homebrew .
Bis auf weiteres angehalten.
15

Sie können die neue --filter-Funktionalität in GNU coreutils split> = 8.13 (2011) verwenden:

tail -n +2 FILE.in | split -l 50 - --filter='sh -c "{ head -n1 FILE.in; cat; } > $FILE"'
Pixelbeat
quelle
2
Ich mag die Einzeiler-Version. Nur um es allgemeiner für Bash zu machen, habe ich:tail -n +2 FILE.in | split -d --lines 50 - --filter='bash -c "{ head -n1 ${FILE%.*}; cat; } > $FILE"' FILE.in.x
KullDox
12

Sie können [mg] awk verwenden:

awk 'NR==1{
        header=$0; 
        count=1; 
        print header > "x_" count; 
        next 
     } 

     !( (NR-1) % 100){
        count++; 
        print header > "x_" count;
     } 
     {
        print $0 > "x_" count
     }' file

100 ist die Anzahl der Zeilen jeder Scheibe. Es erfordert keine temporären Dateien und kann in eine einzelne Zeile gestellt werden.

marco
quelle
9

Dieser Einzeiler teilt die große CSV in 999 Datensätze auf, wobei sich die Kopfzeile oben befindet (also 999 Datensätze + 1 Kopfzeile = 1000 Zeilen).

cat bigFile.csv | parallel --header : --pipe -N999 'cat >file_{#}.csv'

Basierend auf der Antwort von Ole Tange. (Zu Oles Antwort: Sie können die Zeilenanzahl nicht mit Pipepart verwenden.)

Tim Richardson
quelle
Bitte beachten Sie, dass jede kleinere Datei in dieser Lösung 1000 Zeilen enthält, wenn wir die Kopfzeile in jeder Datei berücksichtigen.
Peiti Li
Deshalb benutze ich 999 :)
Tim Richardson
1
Ich musste brew install parallelauf macOS. Klappt wunderbar!
Asimov4
7

Ich bin ein Neuling, wenn es um Bash-Fu geht, aber ich konnte diese Monstrosität mit zwei Befehlen erfinden. Ich bin sicher, es gibt elegantere Lösungen.

$> tail -n +2 file.txt | split -l 4
$> for file in `ls xa*`; do echo "`head -1 file.txt`" > tmp; cat $file >> tmp; mv -f tmp $file; done

Dies setzt voraus, dass Ihre Eingabedatei lautet file.txt, dass Sie das prefixArgument nicht verwenden splitund in einem Verzeichnis arbeiten, in dem keine anderen Dateien vorhanden sind, die mit splitdem Standardausgabeformat beginnen xa*. Ersetzen Sie außerdem die '4' durch die gewünschte Größe der Trennlinie.

Rob Hruska
quelle
2

Dies ist eine robustere Version von Denis Williamsons Skript. Das Skript erstellt viele temporäre Dateien, und es wäre eine Schande, wenn sie herumliegen würden, wenn der Lauf unvollständig wäre. Fügen wir also die Signalüberwachung hinzu (siehe http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html und dann http://tldp.org/LDP/abs/html/debugging.html ) und Entfernen Sie unsere temporären Dateien. Dies ist sowieso eine bewährte Methode.

trap 'rm split_* tmp_file ; exit 13' SIGINT SIGTERM SIGQUIT 
tail -n +2 file.txt | split -l 4 - split_
for file in split_*
do
    head -n 1 file.txt > tmp_file
    cat $file >> tmp_file
    mv -f tmp_file $file
done

Ersetzen Sie '13' durch einen beliebigen Rückkehrcode. Oh, und Sie sollten wahrscheinlich sowieso mktemp verwenden (wie einige bereits vorgeschlagen haben), also entfernen Sie 'tmp_file "aus dem rm in der Trap-Zeile. Weitere Signale zum Abfangen finden Sie auf der Signal-Manpage.

Sam Bisbee
quelle
2

Ich mochte die awk-Version von marco, die aus diesem vereinfachten Einzeiler übernommen wurde, bei dem Sie die geteilte Fraktion ganz einfach so detailliert angeben können, wie Sie möchten:

awk 'NR==1{print $0 > FILENAME ".split1";  print $0 > FILENAME ".split2";} NR>1{if (NR % 10 > 5) print $0 >> FILENAME ".split1"; else print $0 >> FILENAME ".split2"}' file
DreamFlasher
quelle
Ich mag diese Lösung, aber sie ist auf nur zwei geteilte Dateien beschränkt
Bas
Wenn es dir gefällt, gibt es die Upvote-Funktion dafür;) Es kann leicht an mehr Dateien angepasst werden, aber ja, es ist nicht so flexibel wie split -l
DreamFlasher
"one liner" ... pshh
Pandem1c
2

Ich mochte die Versionen von Rob und Dennis so sehr, dass ich sie verbessern wollte.

Hier ist meine Version:

in_file=$1
awk '{if (NR!=1) {print}}' $in_file | split -d -a 5 -l 100000 - $in_file"_" # Get all lines except the first, split into 100,000 line chunks
for file in $in_file"_"*
do
    tmp_file=$(mktemp $in_file.XXXXXX) # Create a safer temp file
    head -n 1 $in_file | cat - $file > $tmp_file # Get header from main file, cat that header with split file contents to temp file
    mv -f $tmp_file $file # Overwrite non-header containing file with header-containing file
done

Unterschiede:

  1. in_file ist das Dateiargument, das Sie aufteilen möchten, um Header beizubehalten
  2. Verwenden Sie awkstatt tailwegen der awkbesseren Leistung
  3. aufgeteilt in 100.000 Zeilendateien anstelle von 4
  4. Der Name der geteilten Datei wird als Name der Eingabedatei mit einem Unterstrich und Zahlen versehen (bis zu 99999 - aus dem Split-Argument "-d -a 5").
  5. Verwenden Sie mktemp, um temporäre Dateien sicher zu verarbeiten
  6. Verwenden Sie eine einzelne head | catZeile anstelle von zwei Zeilen
Garren S.
quelle
2

Verwenden Sie GNU Parallel:

parallel -a bigfile.csv --header : --pipepart 'cat > {#}'

Wenn Sie für jedes Teil einen Befehl ausführen müssen, kann GNU Parallel auch dabei helfen:

parallel -a bigfile.csv --header : --pipepart my_program_reading_from_stdin
parallel -a bigfile.csv --header : --pipepart --fifo my_program_reading_from_fifo {}
parallel -a bigfile.csv --header : --pipepart --cat my_program_reading_from_a_file {}

Wenn Sie in 2 Teile pro CPU-Kern aufteilen möchten (z. B. 24 Kerne = 48 gleich große Teile):

parallel --block -2 -a bigfile.csv --header : --pipepart my_program_reading_from_stdin

Wenn Sie in 10-MB-Blöcke aufteilen möchten:

parallel --block 10M -a bigfile.csv --header : --pipepart my_program_reading_from_stdin
Ole Tange
quelle
2

Unten finden Sie einen 4-Liner, mit dem Sie eine bigfile.csv in mehrere kleinere Dateien aufteilen und den CSV-Header beibehalten können. Verwendet nur integrierte Bash-Befehle (head, split, find, grep, xargs und sed), die auf den meisten * nix-Systemen funktionieren sollten. Sollte auch unter Windows funktionieren, wenn Sie mingw-64 / git-bash installieren.

csvheader = `head -1 bigfile.csv`
split -d -l10000 bigfile.csv smallfile_
find. | grep smallfile_ | xargs sed -i "1s / ^ / $ csvheader \ n /"
sed -i '1d' smallfile_00

Zeile für Zeile Erklärung:

  1. Erfassen Sie den Header in einer Variablen namens csvheader
  2. Teilen Sie die Datei bigfile.csv in mehrere kleinere Dateien mit dem Präfix smallfile_ auf
  3. Finde alle kleinen Dateien und füge den csvheader mit xargs und sed -i in die ERSTE Zeile ein . Beachten Sie, dass Sie sed in "doppelten Anführungszeichen" verwenden müssen, um Variablen verwenden zu können.
  4. Die erste Datei mit dem Namen smallfile_00 enthält jetzt redundante Header in den Zeilen 1 und 2 (aus den Originaldaten sowie aus der Einfügung des sed-Headers in Schritt 3). Wir können den redundanten Header mit dem Befehl sed -i '1d' entfernen.
Thyag
quelle
1

Inspiriert von @ Arkadys Kommentar zu einem Einzeiler.

  • MYFILE-Variable, um die Boilerplate zu reduzieren
  • splitDer Dateiname wird nicht angezeigt, aber mit dieser --additional-suffixOption können wir einfach steuern, was zu erwarten ist
  • Entfernen von Zwischendateien über rm $part(setzt keine Dateien mit demselben Suffix voraus)

MYFILE=mycsv.csv && for part in $(split -n4 --additional-suffix=foo $MYFILE; ls *foo); do cat <(head -n1 $MYFILE) $part > $MYFILE.$part; rm $part; done

Beweis:

-rw-rw-r--  1 ec2-user ec2-user  32040108 Jun  1 23:18 mycsv.csv.xaafoo
-rw-rw-r--  1 ec2-user ec2-user  32040108 Jun  1 23:18 mycsv.csv.xabfoo
-rw-rw-r--  1 ec2-user ec2-user  32040108 Jun  1 23:18 mycsv.csv.xacfoo
-rw-rw-r--  1 ec2-user ec2-user  32040110 Jun  1 23:18 mycsv.csv.xadfoo

und natürlich wird head -2 *fooder Header hinzugefügt.

user1043620
quelle
0

Eine einfache, aber vielleicht nicht so elegante Methode: Schneiden Sie den Header vorher ab, teilen Sie die Datei und fügen Sie dann den Header jeder Datei mit cat oder einer beliebigen Datei, die sie einliest, wieder zusammen.

  1. head -n1 file.txt> header.txt
  2. split -l file.txt
  3. cat header.txt f1.txt
Llewellyn Hinkes Jones
quelle