WadokuJT-Datensätze extrahieren mit Ruby III
Wie schon in den letzten zwei Posts berichtet wurde, arbeite ich zur Zeit daran, WadokuJT-Datensätze zu extrahieren.
Rückblickend hatte ich zwei Hauptprobleme, die sich mir bei der Überarbeitung der Daten in den Weg gestellt haben.
- Extrahieren der Übersetzungsäquivalente aus der Definition
- Trennung der Schreibungen
Nun kam noch dazu, dass ich die Nummern- und Buchstabenindex z.B. [1], [a] und bestimmte Tags wie z.B.<Prior_1>, <GENKI_L14-II> entfernen sollte.
Es wäre zwar ein Leichtes, die aktuelle Version(bereits extrahiert) ein drittes mal durch einen Parser zu jagen. Aber die Erfahrung aus den 2 harten Semestern Info-Studium sagte mir, dass ich in so einem Fall lieber nochmal den Code überdenken sollte(Macht der Abstraktion lässt grüßen!..;D). Denn ich möchte ja in der Zukunft möglichst flexibel bleiben und jedem Änderungswunsch unabhängig von den Versionen, die ich zuvor erstellt habe, entgegenkommen können.
Dafür war mein alter Code an einigen Stellen ziemlich unflexibel. Zum Beispiel habe ich jedes mal, wenn was geändert werden muss, die alte Version durchlesen und eine neue erstellen lassen, was ziemlich zeitaufwendig war.
def generate2 wdk content = File.open(wdk, "r").readlines # ... ... # ... ... # ... ... file = File.new("wdk_list2", "w") file.puts new_content file.close end
Zudem waren die Methoden bspsweise für das Extrahieren von den Übersetzungsäquivalenten und die Trennung von den Schreibungen ziemlich ähnlich:
#seperate writing for an entry def sep_w entry rest = entry.split("\t").values_at(0,2,3) writing = entry.split("\t")[1].scan(/([^;\(\)\ 1-9\[\]\s]+)/).flatten #Der Regex /([^;\(\)\ 1-9\[\]\s]+)/ matcht alles außer ;,(,),[,],1-9 und \s white-space character in mehrfacher Ausführung acc = [] puts writing #um zu sehen ob das programm richtig tut writing.each{|w| acc = acc << rest.myinsert(1, w.strip).join("\t")} #myinsert gibt ein Duplikat vom Anfangsarray zurück return acc end
def parse defi array = defi.scan(/(<TrE.+?)(>;|>\.|\s\/\/)/).flatten #bei einigen Tags fehlt der ">" am Ende array.select{|e| e.include? "<TrE"}.map{|e| e.end_with?(">") ? e : e + ">"} end
Beide Methoden bekommen einen Eintrag oder einen Teil des Eintrags als Argument und lassen einen Regex(verschiedenen) darüber laufen und bekommen als Ergebnis einen Array von Strings zurück. Der einzige Unterschied lag darin, dass die „sep_w“-Methode einen Array von den ganzen Einträgen zurückgibt, die „parse“ aber einen Array von nur den Übersetzungsäquivalenten.
Also könnte ich meinen Code effizienter und einfacher machen, indem ich diese beiden Methoden zunächst zusammen fasse.
#seperiert Teil vom Eintrag mit dem gegebenen Index nach dem gegebenen Regex def sep(entry, index, regex) entry.split("\t")[index].scan(regex).flatten end
Damit könnte ich jeden Teil in dem Eintrag nach einem beliebigen Regex extrahieren lassen.
#für Schreibung(Index 1) mit dem Regex /[^;\ \(\)]+/ sähe es so aus: # # entry.split("\t")[1].scan(/[^;\ \(\)]+/).flatten # #für die Definition(Index 4) mit dem Regex /(<TrE.+?)>(?=;|\.|\ )/ # # entry.split("\t")[4].scan(/(<TrE.+?)>(?=;|\.|\ )/).flatten #
Die folgende Überlegung zur Vereinfachung war, dass ich die „generate“-Methode auf einen einzigen Eintrag begrenze, damit ich diese nachher in irb(dem Ruby-Interpreter) besser testen kann, ohne immer wieder eine riesige Datei erstellen zu müssen.
#generiert einen Eintrag def generate entry e_array = entry.split("\t") kana = e_array[3] ? 3 : 2 #die Abfrage zur Sicherheit rest = e_array.values_at(0,kana) writings = sep(entry , 1, /[^;\ \(\)]+/) tres = sep(entry, 4, /(<TrE.+?)>(?=;|\.|\ )/).map{|tre| tre.end_with?(">") ? tre : tre + ">"} #bei manchen Einträgen fehlt ">" am Ende #Doppelschleife mit einem Akkumulator, der die Ergebnisse zwischen speichert acc = [] writings.each do |w| tres.each{|tre| acc = acc << rest.myinsert(1, w).myinsert(4, tre).join("\t")} end puts acc #um zu schauen ob alles richtig läuft return acc end
Die Idee hinter der Methode ist:
Wenn ich einen Eintrag nach „\t“ spalte, ergibt es sich einen Array mit [„wadoku_id“, „writing“, „kana“, „definition“, …]. Da „wadoku_id“ und „kana“ in dem Fall nicht verändert werden, können sie im Array bleiben. Für „writing“ und „definition“ kann ich meine „sep“-Methode anwenden und bekomme jeweils einen Array mit [„writing1“, „writing2“, „writing3“ …] und [„TrE1“, „TrE2“, „TrE3“, „TrE4“ …]. In einer Doppelschleife werden die Elemente in den zwei Array jeweils an die richtige Position hinzugefügt(mit der „myinsert“-Methode).
Nachdem dies möglich war, fehlte noch, dass man die Index + bestimmte/unbestimmte Tags entfernte. Dabei wäre es natürlich geschickter, wenn man die riesige Wadoku-Datei nicht zweimal durchlaufen lassen müsste, weil das einfach zeit- und arbeitsaufwendiger wäre. Daher habe ich mich für eine seperate „filter“-Methode für einzelne Einträge entschieden, die in der „generate“-Methode als erstes aufgerufen wird um alle Nummern- und Buchstabenindex und unerwünschte Tags in einem Eintrag zu entfernen.
#filtert die Index raus + beliebige Tags(regex in der Methode modifizierbar) def filter entry filter_regex = /(\[.+?\]|<Prior.+?>|<JLPT.+?>|<GENKI.+?>)/ entry.gsub(filter_regex, "") end # #übernommen in die generate-Methode #generiert einen Eintrag def generate entry filtered_entry = filter entry e_array = filtered_entry.split("\t") kana = e_array[3] ? 3 : 2 rest = e_array.values_at(0,kana) writings = sep(filtered_entry , 1, /[^;\ \(\)]+/) tres = sep(filtered_entry, 4, /(<TrE.+?)>(?=;|\.|\ )/).map{|tre| tre.end_with?(">") ? tre : tre + ">"} acc = [] writings.each do |w| tres.each{|tre| acc = acc << rest.myinsert(1, w).myinsert(4, tre).join("\t")} end puts acc return acc end
Zu guter Letzt sollte die „generate“-Methode auf alle Einträge aus der Wadoku-Raw-Datei angewendet und das Ergebnis in eine neue Datei übertragen werden.
#Datei extrahieren und in eine neue Datei schreiben def extract(wdk_raw, wdk_new) content = File.readlines(wdk_raw).drop(1) new_content = content.map{|entry| generate entry}.flatten.join("\n") file = File.new(wdk_new, "w") file.puts new_content file.close end
Der komplette Code sieht wie folgt aus:
#seperiert Teil vom Eintrag mit dem gegebenen Index nach dem gegebenen Regex def sep(entry, index, regex) entry.split("\t")[index].scan(regex).flatten end #filtert die Index und co. raus def filter entry filter_regex = /(\[.+?\]|<Prior.+?>|<JLPT.+?>|<GENKI.+?>)/ entry.gsub(filter_regex, "") end #generiert einen Eintrag def generate entry filtered_entry = filter entry e_array = filtered_entry.split("\t") kana = e_array[3] ? 3 : 2 rest = e_array.values_at(0,kana) writings = sep(filtered_entry , 1, /[^;\ \(\)]+/) tres = sep(filtered_entry, 4, /(<TrE.+?)>(?=;|\.|\ )/).map{|tre| tre.end_with?(">") ? tre : tre + ">"} acc = [] writings.each do |w| tres.each{|tre| acc = acc << rest.myinsert(1, w).myinsert(4, tre).join("\t").gsub("\t\t", "\t")} #siehe Erklärung unten end puts acc return acc end #myinsert dupliziert den Array class Array def myinsert(pos, el) self.dup.insert(pos, el) end end #Datei extrahieren und in eine neue Datei schreiben def extract(wdk_raw, wdk_new) content = File.readlines(wdk_raw).drop(1) new_content = content.map{|entry| generate entry}.flatten.join("\n") file = File.new(wdk_new, "w") file.puts new_content file.close end
¿Man könnte fast meinen, dass diesmalige Lösung fast perfekt wäre, weil man bei dieser Version nur einmal die Methode „extract“ auf die Wadoku-Datei aufzurufen brauch und nicht mal hinterher die schöne „kill_n“ benutzen muss. Aber leider gab es wieder einen kleinen „Fehler“, der wohl durch die Doppelschleife verursacht wurde. Statt gewünschtem Eintrag mit jeweils einem „\t“, hatte ich ein „\t“ zu viel zwischen der Lesung und dem Übersetzungsäquivalent:
#richtige Version: ID \t Schreibung \t Lesung in Kana \t Übersetzungsäquivalent \n #meine Version: ID \t Schreibung \t Lesung in Kana \t\t Übersetzungsäquivalent \n
Trotz angestrengtem Nachdenken, konnte ich mir dieses Verhalten in der Schleife nicht erklären. Als Notlösung musste ich noch ein .gsub(„\t\t“, „\t“) in die Schleife mitgeben, was alle Doppel-„\t“s im Eintrag vereinfacht.