Verwenden von awk zum Teilen der Textdatei alle 10.000 Zeilen

7

Ich habe eine große gzip'd Textdatei. Ich möchte etwas wie:

zcat BIGFILE.GZ | \
    awk (snag 10,000 lines and redirect to...) | \
    gzip -9 smallerPartFile.gz

Der awk-Teil dort oben soll im Grunde genommen 10.000 Zeilen dauern und an gzip senden und dann wiederholen, bis alle Zeilen in der ursprünglichen Eingabedatei verbraucht sind. Ich habe ein Skript gefunden, das behauptet, dies zu tun, aber wenn ich es auf meinen Dateien ausführe und dann das Original von denjenigen unterscheide, die geteilt und dann zusammengeführt wurden, fehlen Zeilen. Also stimmt etwas mit dem awk-Teil nicht und ich bin mir nicht sicher, welcher Teil kaputt ist.

Das Ziel:

  • Lesen Sie die Quelldatei einmal für den gesamten Vorgang durch
  • Teilen Sie die Quelle in kleinere Teile auf, die durch Zeilenumbrüche begrenzt sind. Angenommen, 10.000 Zeilen pro Datei
  • Komprimieren Sie die Zieldateien, die als Ergebnis der Aufteilungsaktion erstellt wurden, und tun Sie dies ohne einen zusätzlichen Schritt, nachdem dieses Skript verarbeitet wurde.

Hier ist der Code. Kann mir jemand sagen, warum dies keine Datei ergibt, die geteilt und zusammengeführt und dann erfolgreich zum Original verschoben werden kann?

# Generate files part0.dat.gz, part1.dat.gz, etc.
# restore with: zcat foo* | gzip -9 > restoredFoo.sql.gz (or something like that)
prefix="foo"
count=0
suffix=".sql"

lines=10000 # Split every 10000 line.

zcat /home/foo/foo.sql.gz |
while true; do
  partname=${prefix}${count}${suffix}

  # Use awk to read the required number of lines from the input stream.
  awk -v lines=${lines} 'NR <= lines {print} NR == lines {exit}' >${partname}

  if [[ -s ${partname} ]]; then
    # Compress this part file.
    gzip -9 ${partname}
    (( ++count ))
  else
    # Last file generated is empty, delete it.
    rm -f ${partname}
    break
  fi
done
Hinterhältiger Wombat
quelle

Antworten:

5

Ich würde vorschlagen, die ganze Hausarbeit im Haus zu erledigen awk, das funktioniert hier mit GNU awk:

BEGIN { file = "1" }

{ print | "gzip -9 > " file ".gz" }

NR % 10000 == 0 {
  close("gzip -9 > " file ".gz")
  file = file + 1
}

Dadurch werden 10000 Zeilen bis 1.gz, die nächsten 10000 bis 2.gzusw. gespeichert. Verwenden sprintfSie diese Option, wenn Sie mehr Flexibilität bei der Dateinamengenerierung wünschen.

Mit einem Test aktualisiert

Die verwendeten Testdaten sind Primzahlen bis zu 300.000, die hier zu finden sind .

wc -lc primes; md5sum primes

Ausgabe:

25997 196958 primes
547d527ec50c2799fa6ce96dba3c26c0  primes

Wenn das obige awk-Programm split.awkwie folgt gespeichert wurde und ausgeführt wird (mit GNU awk):

awk -f split.awk primes

Es werden drei Dateien (1.gz, 2.gz und 3.gz) erstellt. Testen dieser Dateien:

for f in {1..3}; do gzip -dc $f.gz >> foo; done

Prüfung:

diff source.file foo

Die Ausgabe sollte nichts sein, wenn die Dateien identisch sind.

Und die gleichen Tests wie oben:

gzip -dc [1-3].gz | tee >(wc -lc) >(md5sum) > /dev/null

Ausgabe:

25997  196958
547d527ec50c2799fa6ce96dba3c26c0  -

Dies zeigt, dass der Inhalt derselbe ist und dass die Dateien wie erwartet aufgeteilt werden.

Thor
quelle
Dies scheint immer die letzten 34 Zeichen der letzten Zeile für jede geteilte Datei abzuschneiden.
Sneaky Wombat
1
Klingt seltsam. Ich habe der Antwort ein Beispiel hinzugefügt, um zu sehen, ob Sie das gleiche Ergebnis erhalten.
Thor
Danke Thor! Ich musste die for-Schleife ändern, weil ich viele geteilte Dateien hatte, aber diff sagt mir, dass das Original und die geteilten Dateien, dann sind die zusammengeführten Dateien gleich.
Sneaky Wombat
@Thor: Sie haben Recht in dem unwahrscheinlichen Fall, dass das Limit überschritten wird. Aber dann musst du close("gzip -9 > " file ".gz")nicht nur close(file). Ansonsten hat awk keine Ahnung, was zu schließen ist.
Sparkie
@sparkie: Du hast recht close(file)war falsch. Die Wahrscheinlichkeit, dass dies ein Problem darstellt, hängt von der Dateigröße, der Anzahl der verfügbaren Dateideskriptoren und der Anzahl der Zeilen in jeder Datei ab. Es ist sauberer, jede Datei zu schließen, wenn wir damit fertig sind.
Thor
3

Die kürzere (und nützlichere) Antwort: Haben Sie sich den Unix- splitBefehl angesehen?

Scott
quelle
Ja, ich weiß über Split Bescheid und das ist nicht sinnvoll. Ein aufmerksamer Leser wird feststellen, dass beim Teilen unkomprimierte Dateien generiert werden. Ich brauche sie komprimiert.
Sneaky Wombat
Das Problem bei der Teilung ist, dass er erst dann mit der Komprimierung der geteilten Ausgabe beginnen kann, wenn er mit jedem Teil der Teilung fertig ist. Wenn es sich also um eine große gzip-Teilung handelt, ist die Verwendung einfach nicht möglich.
Tapferkeit
@ Valor - genau. Ich habe 400 dieser Dateien, jede ist ungefähr 400 GB unkomprimiert. :(
Sneaky Wombat
@Valor: Nun, @Sneaky könnte parallel zu dem ein Shell-Skript ausführen split, das wartet, bis fooabes erstellt wird, und dann zippt fooaa, dann wartet, bis fooaces erstellt wird, und dann zippt fooabund so weiter. Aber das ist ein Kluge und garantiert nicht zu funktionieren.
Scott
@ Sneaky: Ich würde streiten, dass die Anzahl der Dateien kein Faktor ist. Ja, die Größe ist offensichtlich. Aber wenn Sie ungefähr 401 GB freien Speicherplatz haben, verstehe ich nicht, warum Sie nicht verwenden konnten split. … Aber warten Sie - bedeutet das, dass Sie erwarten, jede Datei in Millionen von Teilen zu zerlegen (400G ÷ 80 = 5000000)? Das könnte ausschließen split- ich weiß nicht, ob es mehr als 676 (26²) Ausgabedateien verarbeiten kann.
Scott
3

Die kurze Antwort lautet, dass awkdie Eingabe ( zcatin diesem Fall die Pipe von ) blockweise gelesen wird (wobei ein Block 512 Byte oder ein Vielfaches davon ist, abhängig von Ihrem Betriebssystem). Wenn sich also das 10000. Zeilenumbruchzeichen (Zeilenende-Marker) im Speicher befindet, befindet sich auch die 10001. Zeile, die 10002. und höchstwahrscheinlich auch mehr (oder möglicherweise weniger) im Speicher. Dies ist ein Problem, da diese Zeichen aus der Pipe ausgelesen wurden und nicht mehr für die nächste Iteration awkzum Lesen verfügbar sind .

Scott
quelle
Das macht Sinn. Hmm. Gibt es eine Möglichkeit, diese zu erfassen und zu puffern und das gewünschte Ergebnis zu erzielen? Übrigens ist dies Ubuntu 12.04 LTS.
Sneaky Wombat
3

Ich habe darüber nachgedacht und einen Weg gefunden, der überhaupt nicht effizient ist und bei dem jede Datei nutzlos vollständig dekomprimiert wird, um jedes Stück aufzunehmen. Wenn Sie also in 20 Teile teilen möchten, werden die großen Dateien 20 Mal dekomprimiert. Es wird jedoch nicht die gesamte Datei gespeichert, sondern nur das komprimierte Teil. Obwohl es speichereffizient ist, ist die CPU ineffizient.

Das Skript sollte mit dem ersten Argument der großen gzip-Datei und dem zweiten Argument der Anzahl der zu teilenden Zeilen ausgeführt werden.

#!/bin/bash
GZIP_FILE=$1
SPLIT_LINES=$2
TOTAL_LINES=`zcat $GZIP_FILE|wc -l`
START=0
NEXT_START=0
while [ $NEXT_START -lt $TOTAL_LINES ]; do
        NEXT_START=$(( $NEXT_START + $SPLIT_LINES ))
        echo .
        zcat $GZIP_FILE|sed -n ${START},${NEXT_START}p |gzip -9 > ${GZIP_FILE}.lines-${START}-${NEXT_START}.gz
        START=$NEXT_START
done

Dadurch wird im selben Verzeichnis für jedes Stück eine Datei mit dem Namen gzip-Datei erstellt und ".lines- $ startline- $ endline.gz" angehängt.

Hoffe du bist in Ordnung CPU zu verschwenden :)

Tapferkeit
quelle
Das wollte ich als nächstes sagen! :)
Scott
ihr seid komisch. Ich denke, ich werde versuchen, etwas über Python oder so etwas zu schreiben. Die Idee war, die Quelldatei einmal durchzulesen und sie beim Lesen aufzuteilen. Die Variable TOTAL_LINES liest die gesamte Datei durch, um die Anzahl zu ermitteln, und durchläuft dann das geschäftliche Ende der Arbeit. Haha. Ich werde Ihnen eine Gegenstimme für die Anstrengung geben.
Sneaky Wombat
1

Sie haben eine schlechte Alternative. Hier erfahren Sie, wie Sie dies mit GNU Split oder GNU Parallel tun können.

GNU Split hat eine --filterOption und etwas, das dem sehr nahe kommt, was Sie versuchen, wird im Handbuch beschrieben:

`--filter=COMMAND'
     With this option, rather than simply writing to each output file,
     write through a pipe to the specified shell COMMAND for each
     output file.  COMMAND should use the $FILE environment variable,
     which is set to a different output file name for each invocation
     of the command.  For example, imagine that you have a 1TiB
     compressed file that, if uncompressed, would be too large to
     reside on disk, yet you must split it into individually-compressed
     pieces of a more manageable size.  To do that, you might run this
     command:

          xz -dc BIG.xz | split -b200G --filter='xz > $FILE.xz' - big-

     Assuming a 10:1 compression ratio, that would create about fifty
     20GiB files with names `big-xaa.xz', `big-xab.xz', `big-xac.xz',
     etc.

In Ihrem Fall könnten Sie also Folgendes tun:

zcat bigfile.gz | split -l 10000 --filter='gzip -9 > $FILE.gz' - big-

Eine gute Alternative zum Teilen wäre die Verwendung von GNU parallel. Auf diese Weise können Sie die Komprimierung parallelisieren:

zcat bigfile.gz | parallel --pipe -N 10000 'gzip > {#}.gz'
Thor
quelle