Verknüpfen von Textdateien mit 600M + Zeilen

2077
dnkb

Ich habe zwei Dateien huge.txtund small.txt. huge.txthat etwa 600 Millionen Zeilen und 14 GB. Jede Zeile enthält vier durch Leerzeichen getrennte Wörter (Token) und schließlich eine weitere durch Leerzeichen getrennte Spalte mit einer Zahl. small.txthat 150.000 Zeilen mit einer Größe von ~ 3M, einem durch Leerzeichen getrennten Wort und einer Zahl.

Beide Dateien werden mit dem Sortierbefehl ohne zusätzliche Optionen sortiert. Die Wörter in beiden Dateien können Apostrophe (') und Bindestriche (-) enthalten.

Die gewünschte Ausgabe würde alle Spalten aus der huge.txtDatei und die zweite Spalte (die Nummer) enthalten, von der small.txtdas erste Wort huge.txtund das erste Wort der small.txtÜbereinstimmung übereinstimmen.

Meine folgenden Versuche sind mit folgendem Fehler kläglich gescheitert:

cat huge.txt|join -o 1.1 1.2 1.3 1.4 2.2 - small.txt > output.txt  join: memory exhausted  

Was ich vermute, ist, dass die Sortierreihenfolge nicht stimmt, obwohl die Dateien vorsortiert wurden:

sort -k1 huge.unsorted.txt > huge.txt sort -k1 small.unsorted.txt > small.txt 

Die Probleme scheinen um Wörter herum zu erscheinen, die Apostrophe (') oder Bindestriche (-) enthalten. Ich habe auch die Wörterbuchsortierung mit der -dOption ausprobiert, die am Ende auf denselben Fehler stößt.

Ich habe versucht, die Dateien in MySQL zu laden, Indizes zu erstellen und ihnen beizutreten, aber es scheint Wochen auf meinem Laptop zu dauern. (Ich habe keinen Computer mit mehr Arbeitsspeicher oder schneller Festplatte / SSD für diese Aufgabe.)

Ich sehe zwei Wege, weiß aber nicht, wie ich sie umsetzen soll.

  1. Wie sortiere ich die Dateien so, dass sie vom join-Befehl als richtig sortiert betrachtet werden?

  2. Ich dachte daran, MD5 oder andere Hashes der Strings zu berechnen, um die Apostrophe und Bindestriche loszuwerden, aber die Zahlen am Ende der Zeilen intakt zu lassen. Machen Sie das Sortieren und Verbinden mit den Hashes anstelle der Strings selbst und "übersetzen" Sie die Hashes schließlich in Strings. Da es nur 150.000 Hashes geben würde, ist es nicht so schlimm. Was wäre ein guter Weg, um einzelne Hashes für jeden der Strings zu berechnen? Irgendein AWK-Zauber?

Siehe Dateibeispiele am Ende.

Beispiel für riesige.txt

had stirred me to 46  had stirred my corruption 57  had stirred old emotions 55  had stirred something in 69  had stirred something within 40  

Beispiel für small.txt

caley 114881  calf 2757974  calfed 137861  calfee 71143  calflora 154624  calfskin 148347  calgary 9416465  calgon's 94846  had 987654 

Gewünschte Leistung:

had stirred me to 46 987654 had stirred my corruption 57 987654 had stirred old emotions 55 987654 had stirred something in 69 987654 had stirred something within 40 987654 
7
ok, Sie haben huge.txt und small.txt angegeben. Können Sie bitte die gewünschte Ausgabe / das gewünschte Ergebnis angeben? akira vor 13 Jahren 1
siehe oben dnkb vor 13 Jahren 1
Neugierig hier zu sein, aber ich muss fragen. Welche Art von Analyse machen Sie mit all diesen Daten? Nifle vor 13 Jahren 0
@Nifle: Masterplan, um die Welt zu übernehmen :) akira vor 13 Jahren 1
@Nifle, @akira: fast :) Eigentlich geht es um die Verarbeitung des berühmten Google-Web-Corpus, um Stimuli für ein psycholinguistisches Experiment zusammenzustellen. Die Zahlen sind Frequenzen der Strings auf der englischen Sprache www, wie Google es 2006 gesehen hat. Es tut mir leid, wenn dies ein unkluger Grund ist, all diese Daten durchzublättern :) dnkb vor 13 Jahren 1
@dnkb: hast du meinen Ansatz versucht? akira vor 13 Jahren 0
noch nicht. Ich experimentiere mit etwas anderem. Wenn ich nach Hause komme, werde ich sehen, ob es funktioniert. wenn nicht, versuche ich deine. dnkb vor 13 Jahren 0
@akira: Yay, mein dummer Trick hat funktioniert, siehe neue Antwort unten. Vielen Dank für Ihre Hilfe, obwohl ich es zu schätzen weiß. dnkb vor 13 Jahren 0

6 Antworten auf die Frage

9
Michael Borgwardt

IMO the best way to do this would be to use the programming/scripting language you know best and:

  1. load small.txt into an in-memory hash/map/associative array keyed on the words
  2. Process huge.txt line by line, adding the column looked up from the hash and writing the result into an output file
  3. Buffer input and output so that it happens in chunks of at least 4K
Danke, Michael. Das Problem ist, dass das, was ich oben dargelegt habe, das einfachste Szenario ist. Ich muss die oben genannte Operation auch für zwei große Dateien (10+ GB) ausführen, bei denen das Laden einer in den Speicher keine Option ist. Deshalb suche ich nach vorsortierten Dateien und schließe mich an. dnkb vor 13 Jahren 1
@dnkb: Vorsortierte Dateien sind nicht hilfreich, wenn beide Dateien zu groß sind, um in den Arbeitsspeicher zu passen, da Sie immer noch Zugriff auf eine davon haben, was endloses HD-Thrashing bedeutet. Http://en.wikipedia.org/wiki/Hash_join ist ein Anmelde- oder Hybrid-Hash-Beitritt erforderlich. Dies wird jedoch durch ein professionelles RDBMS aus der Ferne implementiert. Ihre Zeit ist wahrscheinlich der beste Versuch, die MySQL-basierte Lösung zum Laufen zu bringen. Michael Borgwardt vor 13 Jahren 0
Ich möchte unterscheiden: Wenn die Dateien vorsortiert werden, können sie, wie in meiner Antwort, nur mit sequentiellem Zugriff zusammengeführt werden. David Z vor 13 Jahren 4
@ David: du hast recht. Ich sollte zu dieser Zeit keine Fragen beantworten ... Michael Borgwardt vor 13 Jahren 0
7
David Z

To build on Michael Borgwardt's answer: as long as both files are sorted, you can put them together by basically performing one step of a mergesort. It'll be a little different than standard mergesort because you only want to keep one of the files. This will, of course, have to be implemented in your favorite programming language.

Here's a sketch of the algorithm:

line1 = read a line from file 1 line2 = read a line from file 2 start of loop: if (first word of line1 == first word of line2) { write all fields of line1 and second field of line2 to output line1 = read a line from file 1 go to start of loop } else if (first word of line1 < first word of line2) { write line1 to output line1 = read a line from file 1 go to start of loop } else (first word of line1 > first word of line2) { line2 = read a line from file 2 go to start of loop } 

Here's a Python version (since Python is just what I happen to know best, not necessarily the best language for the job):

file1 = open('file1', 'r') file2 = open('file2', 'r') w2, n2 = file2.readline().split() for line1 in file1: w11, w12, w13, w14, n15 = line1.split() if w11 == w2: print w11, w12, w13, w14, n15, n2 continue elif w11 < w2: print w11, w12, w13, w14, n15 continue else: while w11 > w2: w2, n2 = file2.readline().split() if w11 == w2: print w11, w12, w13, w14, n15, n2 elif w11 < w2: print w11, w12, w13, w14, n15 

and for completeness, after some digging here's what I came up with for Awk:

BEGIN { getline line2 <"file2"; split(line2, a); } { if (a[1] == $1) print $0,a[2]; else if (a[1] < $1) print $0; else { getline line2 <"file2"; split(line2, a); } } 

Invoke as awk -f program.awk <file1.

Vielen Dank. Der Teufel ist in Sortierung und in den <und> Vergleichen. Art von GNU scheint irgendwie die Apostrophe zu ignorieren / zu misshandeln, da glaube ich, dass meine Probleme damit zusammenhängen. Wenn ich die Dateien "richtig" nach den Implementierungen von <,>, lt, gt-Operatoren sortieren könnte, gäbe es überhaupt kein Problem. In der Tat habe ich versucht, die obige Logik in Perl so gut wie möglich zu codieren, aber es fehlgeschlagen, was Perl und Sortierung als "größere" oder "kleinere" Zeichenfolge betrachtet. dnkb vor 13 Jahren 0
Hmm, gut, Sie könnten beim Zusammenführen eine benutzerdefinierte Vergleichsfunktion verwenden, die der Art entspricht, wie GNU Sort die Dateien behandelt. David Z vor 13 Jahren 0
Ja. Irgendwelche Tipps, wie das geht? Oder wie man herausfindet, was für eine Art es ist? dnkb vor 13 Jahren 0
Hervorragender Beitrag. Am längsten, was ich je gesehen habe. + 1 + 1 + 1 + 1 Puddingfox vor 13 Jahren 0
2
Michael H.

My answer is similar to Michael Borgwardt's, but you don't have to load all of either file into memory. If the files are both sorted, then you walk through first file one line at a time, and you do binary search on the second file to find the target line in question. That's a lot of HD access, but it's low memory consumption.

Ich unterstütze diese Antwort. Wenn ich das Problem ansprechen würde, würde ich wahrscheinlich Folgendes tun: Erstellen Sie so viele .cdb-Dateien aus small.txt, wie Sie benötigen, um eine sehr schnelle Suche zu ermöglichen. Gehen Sie dann Zeile für Zeile über huge.txt und fragen Sie den Begriff in allen .cdb-Dateien ab. Wenn Sie die binäre Suche selbst in Dateien implementieren möchten, ist dies ebenfalls in Ordnung. akira vor 13 Jahren 0
1
akira

OK, this approach uses http://cr.yp.to/cdb.html as a quicker way to look up the content of 'small.txt':

  • Go and install cdbmake (part of 'freecdb' package in Ubuntu, but there are a lot of implementations available.
  • Use awk to pipe small.txt to cdbmake.

    % awk ' { printf "+%d,%d:%s->%s\n", \ length($1),length($2),$1,$2 } \ END { print "" }' | cdbmake small.cdb small.cdbtmp 

(This transforms a line of 'small.txt' from something like "key value" into "+ks,vs:key->value".)

  • Now you go line by line over 'huge.txt' and print it out, looking up the first word in 'small.cdb':

    #!/bin/python import cdb import fileinput c = cdb.init("small.cdb") for l in fileinput.input(['huge.txt']): print l.strip(), v = c.get(l.split()[0]) print "" if v == None else v 

You would have to install python-cdb of course to make this tiny snippet work (and it works only for Python 2.5 because of the 'conditional expression'. Anyway, there are a lot of bindings for whatever language you like. You could also use cdbget(a command line tool) and invoke it over and over again but spawning a new process for millions of lines is a bit ineffective.

Anyway, keep this in mind:

  • Each .cdb file can not be bigger than 4 GB. So if you have to process 'small.txt' with a size of 10 GB you obviously have to split up that into multiple files and create 'small1.cdb', 'small2.cdb', 'small3.cbd' and so on. It should be an easy task.
  • You do not need to sort 'small.txt', a lookup in a cdb file is pretty fast anyway.
  • I have not timed my little test case here, it is based on what you provided. :)
1
dnkb

I know it's embarrassingly simple but it works.
Based on the assumption that my original files contain only lowercase characters, I simply replaced the problematic apostrophes and dashes with two uppercase letters, re-sorted than joined the files, finally changed back the letters back to the signs. That's it.

Thanks again for everyone contributing an answer or insightful comment.

The sorting took like 2 hours for huge.txt (14Gig), the joining less than an hour.

cat small.txt | tr "\'-" "AD" | sort -k1 > small.AD cat huge.txt | tr "\'-" "AD" | sort -k1 | cat huge.txt | join -o 1.1 1.2 1.3 1.4 2.2 - small.AD | tr "AD" "\'-" > output.txt 
Ich interessiere mich immer noch für die Geschwindigkeit meines Ansatzes. Kann ich die Dateien irgendwo herunterladen? oder sie hier neu erstellen mit .. was auch immer? akira vor 13 Jahren 0
@akira: Es ist nur auf 6 DVDs von UPenn erhältlich und kann leider nicht heruntergeladen werden. http://www.ldc.upenn.edu/Catalog/CatalogEntry.jsp?catalogId=LDC2006T13 Ich wäre auch sehr interessiert zu sehen. Mein Gefühl ist, dass bei einer herkömmlichen 2,5-Zoll-Laptop-Festplatte der nicht sequentielle Festplattenzugriff, der zum Durchlaufen des Index erforderlich ist, die Dinge wahrscheinlich verlangsamen würde. Bei einer anständigen SSD ist sie möglicherweise schneller. dnkb vor 13 Jahren 0
@akira: Sie können es jedoch testen, indem Sie beispielsweise 5M eindeutige zufällige Zeichenfolgen und entsprechende Ganzzahlen (Frequenzen) generieren und dann die 150K am häufigsten verwendeten Stücke auswählen. Dies wird small.txt sein. Dann werden mit den gleichen 5M-Zufallsstrings wieder zufällig vier Gramm konstruiert und anschließend eine weitere ganze Zahl eingefügt. Erzeugen Sie 600 Millionen Zeilen, um riesige.txt zu erstellen. ZB asdf wert dfw werhhyr 345345 frtko de serrte flxee 423443 Versuchen Sie abschließend (inner), sie auf einer beliebigen Säule zu verbinden. Dies sollte die Komplexität ziemlich gut wiedergeben. dnkb vor 13 Jahren 0
0
hemp

Instead of MySQL, you might try PostgreSQL which likely can handle this task more gracefully. See their guide on efficiently populating a database.

ein rdbms ist nicht der richtige hammer für diese art von nagel akira vor 13 Jahren 0