Zum Inhalt springen

WadokuJT-Datensätze extrahieren mit Ruby III

17/09/2011

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.

  1. Extrahieren der Übersetzungsäquivalente aus der Definition
  2. 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.

From → Programmierung

Kommentar verfassen

Hinterlasse einen Kommentar