Ich habe ein seltsames Problem mit großen Dateien und bash
. Dies ist der Kontext:
- Ich habe eine große Datei: 75G und mehr als 400.000.000 Zeilen (es ist eine Protokolldatei, meine schlechte, ich lasse es wachsen).
- Die ersten 10 Zeichen jeder Zeile sind Zeitstempel im Format JJJJ-MM-TT.
- Ich möchte diese Datei aufteilen: eine Datei pro Tag.
Ich habe es mit dem folgenden Skript versucht, das nicht funktioniert hat. Meine Frage ist, dass dieses Skript nicht funktioniert, keine alternativen Lösungen .
while read line; do
new_file=${line:0:10}_file.log
echo "$line" >> $new_file
done < file.log
Nach dem Debuggen habe ich das Problem in der new_file
Variablen gefunden. Dieses Skript:
while read line; do
new_file=${line:0:10}_file.log
echo $new_file
done < file.log | uniq -c
gibt das Ergebnis wie folgt aus (Ich setze die x
es, um die Daten vertraulich zu behandeln, andere Zeichen sind die wirklichen). Beachten Sie die dh
und die kürzeren Saiten:
...
27402 2011-xx-x4
27262 2011-xx-x5
22514 2011-xx-x6
17908 2011-xx-x7
...
3227382 2011-xx-x9
4474604 2011-xx-x0
1557680 2011-xx-x1
1 2011-xx-x2
3 2011-xx-x1
...
12 2011-xx-x1
1 2011-xx-dh
1 2011-xx-x1
1 208--
1 2011-xx-x1
1 2011-xx-dh
1 2011-xx-x1
...
Es ist kein Problem im Format meiner Datei . Das Skript cut -c 1-10 file.log | uniq -c
gibt nur gültige Zeitstempel aus. Interessanterweise wird ein Teil der obigen Ausgabe mit cut ... | uniq -c
:
3227382 2011-xx-x9
4474604 2011-xx-x0
5722027 2011-xx-x1
Wir können sehen, dass 4474604
mein anfängliches Skript nach der Anzahl der Unikate fehlgeschlagen ist.
Habe ich ein Limit in Bash erreicht, das ich nicht kenne, habe ich einen Fehler in Bash gefunden (es ist unwahrscheinlich), oder habe ich etwas falsch gemacht?
Update :
Das Problem tritt auf, nachdem 2G der Datei gelesen wurden. Es Nähte read
und Umleitung nicht wie größere Dateien als 2G. Aber immer noch auf der Suche nach einer genaueren Erklärung.
Update2 :
Es sieht definitiv aus wie ein Bug. Es kann reproduziert werden mit:
yes "0123456789abcdefghijklmnopqrs" | head -n 100000000 > file
while read line; do file=${line:0:10}; echo $file; done < file | uniq -c
Aber dies funktioniert gut als Workaround (es scheint, dass ich eine nützliche Verwendung gefunden habe cat
):
cat file | while read line; do file=${line:0:10}; echo $file; done | uniq -c
Ein Fehler wurde bei GNU und Debian gemeldet. Betroffen sind die Versionen bash
4.1.5 unter Debian Squeeze 6.0.2 und 6.0.4.
echo ${BASH_VERSINFO[@]}
4 1 5 1 release x86_64-pc-linux-gnu
Update3:
Dank Andreas Schwab, der schnell auf meinen Fehlerbericht reagiert hat, ist dieser Patch die Lösung für dieses Fehlverhalten. Die betroffene Datei ist, lib/sh/zread.c
wie Gilles früher bemerkte:
diff --git a/lib/sh/zread.c b/lib/sh/zread.c index 0fd1199..3731a41 100644
--- a/lib/sh/zread.c
+++ b/lib/sh/zread.c @@ -161,7 +161,7 @@ zsyncfd (fd)
int fd; { off_t off;
- int r;
+ off_t r;
off = lused - lind; r = 0;
Die r
Variable wird verwendet, um den Rückgabewert von zu halten lseek
. Da lseek
der Offset vom Anfang der Datei zurückgegeben wird, ist der int
Wert negativ , wenn er über 2 GB liegt, was dazu führt, dass der Test if (r >= 0)
dort fehlschlägt, wo er erfolgreich sein sollte.
read
Anweisung in Bash zu zeigen.Antworten:
Sie haben einen Fehler in der Bash gefunden. Es ist ein bekannter Fehler mit einem bekannten Fix.
Programme repräsentieren einen Versatz in einer Datei als Variable in einem ganzzahligen Typ mit einer endlichen Größe. Früher wurde jeder
int
für so gut wie alles verwendet, und derint
Typ war auf 32 Bit beschränkt, einschließlich des Vorzeichenbits, sodass Werte von -2147483648 bis 2147483647 gespeichert werden konnten. Heutzutage gibt es verschiedene Typnamen für verschiedene Dinge , einschließlichoff_t
für eine Offset in einer Datei.Standardmäßig
off_t
ist dies ein 32-Bit-Typ auf einer 32-Bit-Plattform (maximal 2 GB) und ein 64-Bit-Typ auf einer 64-Bit-Plattform (maximal 8 GB). Es ist jedoch üblich, Programme mit der Option LARGEFILE zu kompilieren, die den Typoff_t
auf 64 Bit Breite umschaltet und das Programm geeignete Implementierungen von Funktionen wie zlseek
.Es scheint, dass Sie Bash auf einer 32-Bit-Plattform ausführen und Ihre Bash-Binärdatei nicht mit Unterstützung für große Dateien kompiliert wurde. Wenn Sie nun eine Zeile aus einer regulären Datei lesen, verwendet bash einen internen Puffer, um Zeichen in Batches aus Gründen der Leistung zu lesen (weitere Informationen finden Sie in der Quelle in
builtins/read.def
). Wenn die Zeile vollständig ist, ruft bashlseek
auf, um den Versatz der Datei an die Position des Zeilenendes zurückzuspulen, falls die Position in dieser Datei von einem anderen Programm geändert wurde. Der Aufruflseek
erfolgt in derzsyncfc
Funktion inlib/sh/zread.c
.Ich habe die Quelle nicht genau gelesen, aber ich vermute, dass am Übergangspunkt, an dem der absolute Versatz negativ ist, etwas nicht reibungslos abläuft. Daher liest bash am Ende an den falschen Offsets, wenn der Puffer nach dem Überschreiten der 2-GB-Marke wieder aufgefüllt wird.
Wenn meine Schlussfolgerung falsch ist und Ihre Bash tatsächlich auf einer 64-Bit-Plattform läuft oder mit Unterstützung für große Dateien kompiliert wurde, ist das definitiv ein Fehler. Bitte melden Sie es Ihrer Distribution oder dem Upstream .
Eine Shell ist ohnehin nicht das richtige Werkzeug, um so große Dateien zu verarbeiten. Es wird langsam sein. Verwenden Sie sed wenn möglich, sonst awk.
quelle
Ich weiß nichts falsches, aber es ist sicherlich verworren. Wenn Ihre Eingabezeilen so aussehen:
Dann gibt es wirklich keinen Grund dafür:
Sie machen eine Menge Teilstring-Arbeit, um etwas zu erhalten, das genau so aussieht, wie es bereits in der Datei aussieht. Wie wäre es damit?
Damit werden nur die ersten 10 Zeichen der Zeile erfasst. Sie könnten auch
bash
ganz darauf verzichten und einfach verwendenawk
:Dabei wird das Datum
$1
(die erste durch Leerzeichen getrennte Spalte in jeder Zeile) erfasst und zur Generierung des Dateinamens verwendet.Beachten Sie, dass Ihre Dateien möglicherweise falsche Protokollzeilen enthalten. Das Problem liegt möglicherweise bei der Eingabe, nicht bei Ihrem Skript. Sie können das
awk
Skript so erweitern, dass falsche Zeilen wie folgt gekennzeichnet werden:Dies schreibt Zeilen, die
YYYY-MM-DD
mit Ihren Protokolldateien übereinstimmen , und kennzeichnet Zeilen, die nicht mit einem Zeitstempel auf stdout beginnen.quelle
cut -c 1-10 file.log | uniq -c
Gibt mir das erwartete Ergebnis. Ich verwende,${line:0:4}-${line:5:2}-${line:8:2}
weil ich die Datei in einem Verzeichnis${line:0:4}/${line:5:2}/${line:8:2}
ablegen und das Problem vereinfacht habe (ich werde die Problembeschreibung aktualisieren). Ich weiß,awk
kann mir hier helfen, aber ich habe andere Probleme damit. Was ich will ist das Problem zu verstehenbash
, keine alternativen Lösungen zu finden.cut
funktionierende Anweisung. Da ich Äpfel mit Äpfeln vergleichen möchte, nicht mit Orangen, muss ich die Dinge so ähnlich wie möglich gestalten.Klingt so, als ob Sie Folgendes tun möchten:
Das
close
hält die geöffnete Datei Tabelle von füllen.quelle