Quality Improvement with Testing
Abstract. Testing is the most frequently used method to improve soft-
ware quality and in many cases it is the only method in software-quality
program. This paper gives answers to the following questions from a de-
veloper’s point of view: “Why should we test?”, “What do we possibly
do wrong?” and “How should we test effectively?”
Download link: quality_improvement_with_testing
After 1.5 days of struggling with different kinds of problems and frustrations I finally succeeded in installing and running seafile on my server(FreeBSD 10.1-RELEASE).
The greatest MISTAKE I made there was to follow the official installation guide for FreeBSD on the seafile server manual page. The preparation step is fine but for FreeBSD >= 10, you can NOT simply build the application from source provided there, because it has a lot of dependencies and will ask for library files FreeBSD does’t have. For example libresolv, which is in the package BIND and BIND is replaced by Unbound(https://www.freebsd.org/doc/en/books/handbook/network-dns.html). Even if you installed BIND, it will not provide the libresolv.so file.
So the correct way to install seafile is to use the ports collection. You need ports net-mgmt/seafile and www/seahub. Make sure you deinstall ccnet first before make install seafile, otherwise the files ccnet-init, ccnet-server and ccnet-servtool in seafile/bin will not be generated and that brings trouble and desperation.
cd /usr/ports/net-mgmt/seafile make config #[x] FUSE cd /usr/ports/www/seahub make config #choose ALL options make install
seahub has a dependency to seafile, so run make install in seahub should be sufficient.
If the installation is successful, you should find everything in /usr/local/www/haiwen/seafile-server
[root@server /usr/local/www/haiwen/seafile-server]# tree --dirsfirst -L 1 . |-- runtime |-- seafile |-- seahub |-- upgrade |-- check_init_admin.py |-- reset-admin.sh |-- seaf-cli-wrapper.sh |-- seaf-fuse.sh |-- seaf-gc.sh |-- seafile.sh |-- seahub.sh |-- setup-seafile-mysql.py |-- setup-seafile-mysql.sh |-- setup-seafile.sh |-- sqlite2mysql.py `-- sqlite2mysql.sh
And the files for ccnet are in seafile/bin.
[root@server /usr/local/www/haiwen/seafile-server/seafile]# tree -L 2 . |-- bin | |-- ccnet-init | |-- ccnet-server | |-- ccnet-servtool | |-- seaf-fsck | |-- seaf-fuse | |-- seaf-migrate | |-- seaf-server | |-- seaf-server-init | |-- seafile-controller | `-- seafserv-gc |-- lib | `-- python2.7 `-- share `-- doc
Run ./setup-seafile.sh to setup seafile-server and seahub.
For configuration with Nginx, just follow the official manual http://manual.seafile.com/deploy/deploy_with_nginx.html but you don’t need to install python-flup, it should already be installed with seahub.
Start with ./seafile.sh start and ./seafile.sh start-fastcgi
If you get any error messages like this: ImportError: No module named <module>, just install the python module with pip install <module> or easy_install <module>, but make sure the versions match those used by seahub.
DISTFILES= ${PORTNAME}-${PORTVERSION}.tar.gz:${PORTNAME} \ pip-1.5.6.tar.gz:pip \ pbr-0.10.0.tar.gz:pbr \ lockfile-0.10.2.tar.gz:lockfile \ six-1.8.0.tar.gz:six \ gunicorn-18.0.tar.gz:gunicorn \ flup-1.0.2.tar.gz:flup \ daemon-1.1.tar.gz:daemon \ chardet-2.3.0.tar.gz:chardet \ python-dateutil-2.2.tar.gz:dateutil \ Pillow-2.6.1.zip:Pillow \ Django-1.5.9.tar.gz:Django \ Djblets-0.6.14.tar.gz:Djblets
I’m a heavy Vim user and had only a few experience with Eclipse. Fortunately I could avoid using Eclipse the most time because I’m usually coding in languages other than Java. The only occasion for me to touch Eclipse is when I code for assignments from college. I tried it out several times and every time it was a big mess. I can’t understand the idea behind Eclipse distinguishing packages and folders at all. The whole interface is filled up with uncomprehensive buttons and configuration options. And the biggest problem on top of all, I’ve ever encountered, was to use the Eclipse Git plugin, which MIGHT work well, if every person working on the project uses Eclipse. But as soon as there’s someone using an other editor, the package structure gets so messed up after synching with remote branch that you can spend hours to figure out how to tell Eclipse that a folder is actually a package. The simplest solution there was always deleting everything and creating a new project.
After going through this mess twice and spending hours trying to figure out how to fix it, I decided to drop Eclipse and get my Vim to work with Java.
So here we go:
1.Syntax check
For getting Vim to check the Java syntax I installed the plugin syntastic using the Vim plugin manager Vundle.
Bundle 'https://github.com/scrooloose/syntastic.git'
Syntastic provides 2 different syntax checkers for Java, checkstyle
and javac
. I tried out checkstyle but it didn’t work for me (I’m on Linux version 3.13.6-1-ARCH).
For using javac add following lines to .vimrc
"syntastic" let g:syntastic_java_checker = 'javac' let g:syntastic_java_javac_classpath = "./lib/*.jar\n./src"
The javac_classpath
should be set to the folder, where javac can find the .jar
and the .class
files. For setting classpath for multiple files like the junit-4.11.jar + hamcrest-core-1.3.jar (which javac should be aware of otherwise the test files won’t be checked correctly), seperat them using "\n"
.
I’ve figured this out using the Vim command:
:verbose function SyntaxCheckers_java_GetLocList
This function gets the javac classpath string set in .vimrc and splits it on „\n“ delimiter.
2.File browser
I got the Vim plugin NERD Tree, which is an excellent file browser.
Bundle 'https://github.com/scrooloose/nerdtree'
Adding following lines to .vimrc will enable the toggle shortcut CTRL-n
"nerdtree" map <C-n> :NERDTreeToggle<CR>
To open the selected file in a new tab there’s the shortcut t
.
PS: I’m aware of the plugin Eclim. But I think I can do better without all the „convenient“ stuffs available on Eclipse like auto completion or run the code in editor.
Wenn man beim Kauf eines Ethernetkabels nicht genau auf die Produktbeschreibung schaut, könnte es passieren, dass man statt ein Standardanschlusskabel (Rj45/Rj45) ein Adapterkabel mit Rj45/SubD9(9-poliger Stecker) oder sonst was komisches bekommt. Was macht man in dem Fall:
1.Die einfachste Lösung ist natürlich, das Kabel zurückzuschicken und ein neues zubestellen.
2.Wenn die erste Lösung aus irgendwelchen Gründen nicht gehen sollte, z.B. die Firma hat bestellt und kann das nicht zurückgeben, dann muss man selber etwas daran basteln.
Man kann einfach ein altes Kabel nehmen und ein Ende abschneiden und mit dem falschen Ende des neuen Kabels verbinden. Dabei müssen nur 4 Litzen angeschlossen sein. Die anderen sind optional und werden nur für größere Datenübertragung gebraucht.
Für Rj45/SubD9 Kabel von CanCom:
Pin1: weiß/orange +orange
Pin2: orange + gelb
Pin3: weiß/grün + grün
Pin6: grün + grau
Glücklicherweise muss man auch nichts löten. Es reicht, wenn man die Enden von den Drähten zusammen dreht und zur Befestigung einmal Kreppband drumrum wickelt. Man sollte dabei aber aufpassen, dass die offenen Drähte einander nicht in Berührung kommen. Also am besten wickelt man die seperat ein.
So jetzt kann man testen ob alles funktioniert hat:
ping heise.de
sagt ja! 😉
3. (b) Zeigen Sie, dass 165 ein multiplikatives Inverses in Z637 besitzt und bestimmen Sie es mit der Hilfe des erweiterten Euklidischen Algorithmus.
So lautet eine Aufgabe auf meinem letzten Mathe-Übungsblatt.
Gemäß der Definitionen ist ein Element x
der Halbgruppe Zn
bezüglich der Multiplikation invertierbar wenn der größte gemeinsame Teiler von x
und n
1 ist.
Sei x ∈ (Zn, (·), 1) x hat ein Inverses in Ζn wenn: ggT(x, n) = 1 ≡ n*s + x*t = 1 mit: t(·)x = x(·)t = 1
Um das inverse Element t
herauszubekommen, muss man den erweiterten euklidischen Algorithmus benutzen. Dieser berechnet schrittweise den ggT
und auch die Faktoren s
und t
.
Z.B. hat man für die Zahlen 165 und 637 so eine Liste:
1 = ggT(637, 165) 1 = 637*s + 165*t 1 = (637*43) + (165*-166) 1 = (165*-37) + (142*43) 1 = (142*6) + (23*-37) 1 = (23*-1) + (4*6) 1 = (4*1) + (3*-1) 1 = (3*0) + (1*1) 1 = (1*1) + (0*0)
Wie man sehen kann, hat man bei dem Algorithmus für größere Zahlen ziemlich viel Rechenaufwand mit oder ohne Taschenrechner. Darum wäre es doch schön, wenn man ein Programm hätte, das einem bei ähnlichen Aufgaben in Zukunft helfen könnte.
Zum Glück ist mein Freund Ruby auch ein Freund von Euklid. Mit ein bisschen Zauberei ist das getan.
#encoding: utf-8 class Euklid attr_accessor :euklid_hash def initialize(n1, n2) @a = n1 @b = n2 @euklid_hash = apply_algorithm end private def apply_algorithm hash = {:a => [], :b => [], :s => [], :t => []} a = @a b = @b q = [] loop do hash[:a] << a hash[:b] << b break if b == 0 q << a/b a_alt = a a = b b = a_alt.modulo a end t = 0 s = 1 loop do hash[:s].insert(0, s) hash[:t].insert(0, t) break if q.empty? s_alt = s s = t t = s_alt - (q.pop * s) end return hash end end
Als Referenz habe ich dabei die rekursive Variante von Wikipedia genommen. Die Klasse Euklid bekommt 2 Zahlen vorzugsweise ganze Zahlen, auf die der Algorithmus angewendet wird. Die Zwischenergebnisse werden der Tabelle entsprechend in einem Hash gespeichert.
Zum Schluss kommen noch paar Zeilen Code hinzu, um die Ruby-Datei ausführbar zu machen und die Ausgabe direkt abschreibfertig anzuzeigen.
euklid = Euklid.new(ARGV[0].to_i, ARGV[1].to_i) hash = euklid.euklid_hash i = 0 hash[:a].each do |a| puts "#{hash[:a].last} = (#{a}*#{hash[:s][i]}) + (#{hash[:b][i]}*#{hash[:t][i]})" i += 1 end
Zum Testen kann man das Beispiel von Wikipedia nehmen:
~$ ruby euklid.rb 99, 78 3 = (99*-11) + (78*14) 3 = (78*3) + (21*-11) 3 = (21*-2) + (15*3) 3 = (15*1) + (6*-2) 3 = (6*0) + (3*1) 3 = (3*1) + (0*0)
~$ ruby euklid.rb 637, 165 1 = (637*43) + (165*-166) 1 = (165*-37) + (142*43) 1 = (142*6) + (23*-37) 1 = (23*-1) + (4*6) 1 = (4*1) + (3*-1) 1 = (3*0) + (1*1) 1 = (1*1) + (0*0)
Danke Euklid…danke Ruby…
PS: Sehr wahrscheinlich kann man den Code noch vereinfachen oder einige Zeilen sparen, indem man sich einer anderen Variante des Algorithmus bedient, z.B. der induktiven oder der Matrizenversion davon.
In unserem kleinen Rubykurs für Hobby-Rubyisten haben wir letztens gelernt, wie man einen Generator programmiert, der anhand von einem Textkorpus grammatisch halbwegs korrekte aber total sinnfreie Sätze erzeugen kann.
Die Funktionsweise des Generators ist angelehnt an die sogenannte Markov-Kette, bei der man anhand von einem aktuellen Zustand und dessen Wahrscheinlichkeitsverteilung Prognosen für den nächsten Zustand anstellen kann. Für unseren Zweck ist es so zu verstehen, dass man abhängig von einem Textkorpus für bestimmte Wortfolgen(oder n-Gramme) die Wahrscheinlichkeit berechnen kann. Z.B. ist in einem Liebesroman die Wahrscheinlichkeit von der Wortfolge „Ich liebe“ + „dich“ größer als „Ich liebe“ + „mein Lieblingsfilm: Alien“.
Unser Generator sollte ein gegebenes Textkorpus in 3-Grammen zerlegen und diese jeweils als Schlüssel-Wert-Paare im sogenannten Hash speichern, wobei der Schlüssel immer eindeutig ist. Z.B. der Text „Ich habe heute eine Katze gesehen. Ich habe gestern eine Katze gegessen.“ wird zu
{"Ich habe" => ["heute", "gestern"], "habe heute" => ["eine"], "habe gestern" => ["eine"], "heute eine" => ["Katze"], "gestern eine" => ["Katze"], "eine Katze" => ["gesehen.", "gegessen."]}.
Ein Hash funktioniert wie ein Wörterbuch. Wenn man beispielsweise in dem oben angegebenen Hash nach dem Schlüssel „Ich habe“ sucht, bekommt man als Wert [„heute“, „gestern“] angezeigt.
Mit Hilfe dieses Hashes kann man nun von Wort zu Wort weiterhangeln und verschiedene Satzkombinationen bilden.
In diesem Fall gibt es bspsweise für den Startwert „Ich habe“ nur 4 mögliche Kombinationen.
"Ich habe heute eine Katze gesehen." "Ich habe gestern eine Katze gegessen." "Ich habe gestern eine Katze gesehen." "Ich habe heute eine Katze gegessen."
Dieses Beispiel ist zwar nicht sehr spannend, aber es zeigt durchaus die Tendenz dass, je größer das Textkorpus ist, desto mehr Kombinationsmöglichkeiten man hat. Die Wahrscheinlichkeitsverteilung der Wortfolgen hängt von der Häufigkeit des Vorkommens ab. Je öfter eine Wortfolge in einem Text vorkommt, desto wahrscheinlicher wird sie in dem generierten Satz auftauchen. Z.B.:
Eingabe: {"Ich liebe" => ["dich", "dich", "dich", "Alien", "Alien", "Zombie", "Buttergemüse", "Buttergemüse", "dich", "Weltuntergang"]} Ausgabe mit Wahrscheinlichkeit: "Ich liebe dich" 40% "Ich liebe Alien" 20% "Ich liebe Zombie" 10% "Ich liebe Buttergemüse" 20% "Ich liebe Weltuntergang" 10%
Der obigen Überlegung entsprechend versuchen wir zunächst eine Klasse(MarkovGenerator) für die Objekte zu definieren, die die Eigenschaft haben, aus einer Textdatei einen Markov-Hash generieren zu können.
#encoding: utf-8 class MarkovGenerator #attr_reader/attr_writer attr_accessor :filename, :hash def initialize(filename) @filename = filename @hash = load_and_to_hash end private def load_and_to_hash data = open(@filename).read words = data.split(/[ \n\r]/).reject{|w| w == "" } words_cons = words.each_cons(3).to_a #mit der Methode each_cons kann man einen Array von allen n-grammen eines zählbaren Objekts erzeugen #z.B: [["Ich", "habe", "heute"], ["habe", "heute", "eine"], ["heute", "eine", "Katze"] ...] #Im nächsten Schritt wird der Markov-Hash erzeugt. #Dieser hat die Form {["a", "b"] => ["c", "d" ...]} markovhash = Hash.new words_cons.each do |word_group| key = word_group[0..1] value = word_group[2] markovhash[key] ? markovhash[key] << value : markovhash[key] = value #alternativ:if(?)...else(:) end return markovhash #gibt den Markov-Hash als Rückgabewert der Methode zurück end # alternativ: # markovhash = Hash.new{|hash, key| hash[key] = []} # words_cons.each do |word_group| # key = word_group[0..1] # value = word_group[2] # markovhash[key] << value # end
Objekte der Klasse MarkovGenerator
hat als Inhalt(oder Selektoren/Attribute) den Namen der Textdatei als String und einen Hash, der durch die geschützte Methode(private) „load_and_to_hash“ erzeugt wird.
Zur Kontrolle, ob alles richtig funktioniert, kann man im irb versuchen, ein solches Objekt zu erzeugen. Als Vorarbeit muss man eine neue Textdatei „test.txt“ mit z.B „Ich habe heute eine Katze gesehen. Ich habe gestern eine Katze gegessen.“ machen und im gleichen Ordner abspeichern wie die Rubydatei(rubykurs.rb). Wenn alles geklappt hat, sollte folgendes möglich sein:
ruby-1.9.2-p180 :051 > load "rubykurs.rb" => true ruby-1.9.2-p180 :052 > test = MarkovGenerator.new "test.txt" => #<MarkovGenerator:0x91b1260 @filename="test.txt", @hash={["Ich", "habe"]=>["heute", "gestern"], ["habe", "heute"]=>["eine"], ["heute", "eine"]=>["Katze"], ["eine", "Katze"]=>["gesehen.", "gegessen."], ["Katze", "gesehen."]=>["Ich"], ["gesehen.", "Ich"]=>["habe"], ["habe", "gestern"]=>["eine"], ["gestern", "eine"]=>["Katze"]}> ruby-1.9.2-p180 :053 > test.hash => {["Ich", "habe"]=>["heute", "gestern"], ["habe", "heute"]=>["eine"], ["heute", "eine"]=>["Katze"], ["eine", "Katze"]=>["gesehen.", "gegessen."], ["Katze", "gesehen."]=>["Ich"], ["gesehen.", "Ich"]=>["habe"], ["habe", "gestern"]=>["eine"], ["gestern", "eine"]=>["Katze"]}
Zur Erinnerung wollten wir als Ergebnis eine beliebige Satzkombination haben:
Eingabe: {"Ich liebe" => ["dich", "dich", "dich", "Alien", "Alien", "Zombie", "Buttergemüse", "Buttergemüse", "dich", "Weltuntergang"]} Ausgabe mit Wahrscheinlichkeit: "Ich liebe dich" 40% "Ich liebe Alien" 20% "Ich liebe Zombie" 10% "Ich liebe Buttergemüse" 20% "Ich liebe Weltuntergang" 10%
Dafür benötigen wir die 2 folgenden Methoden:
public def next(word1, word2) key = [word1, word2] value_lis = @hash[key] value_lis[rand(value_lis.length)] end def sentence(word1, word2) result = [word1, word2] loop do word3 = self.next(result[-2], result.last) result << word3 break if word3[/[\.!?]/] end result.join(" ") end
Die Methode „next“ gibt zu jedem gegebenen Wortpaar(Schlüssel) einen beliebigen Wert aus der zugehörigen Werte-Liste zurück. Die Methode „sentence“ generiert mit einem Start-Wortpaar einen Satz. Die Idee dabei ist, dass man eine Liste von Wörtern in jedem Schritt der Schleife mit einem weiteren Wort ergänzt, bis ein Satzzeichen kommt, das den Satz beendet.
#die Variable word3/w3 wird in jedem Schritt mit einem neuen Wert überschrieben z.B: Schritt0: [w1, w2] w3 = "a" | self.next(w1,w2) Schritt1: [w1, w2, "a"] w3 = "b" | self.next(w2,"a") Schritt2: [w1, w2, "a", "b"] w3 = "c" | self.next("a","b") Schritt3: [w1, w2, "a", "b", "c"] w3 = "d." | self.next("b","c") Schritt4: break da "d.".include?(".") Schritt5: [w1, w2, "a", "b", "c", "d."].join(" ")
Die Methode können wir auch in irb testen.
ruby-1.9.2-p180 :107 > test.sentence("Ich", "habe") => "Ich habe gestern eine Katze gegessen." ruby-1.9.2-p180 :108 > test.sentence("Ich", "habe") => "Ich habe heute eine Katze gesehen." ruby-1.9.2-p180 :111 > test2.sentence("Ich", "habe") => "Ich habe heute eine Katze gegessen." ruby-1.9.2-p180 :113 > test2.sentence("Ich", "habe") => "Ich habe gestern eine Katze gesehen."
Unser Programm ist in diesem Zustand aber leider noch nicht vollständig. Denn mit der Methode „sentence“ kann man zwar einen beliebigen Satz erzeugen. Aber man muss immer selbst das Start-Wortpaar aussuchen. Und wenn man aus Versehen ein Wortpaar eingibt, das nicht als Schlüssel gespeichert wurde, bekommt man eine schreckliche Fehlermeldung.
Das alles kann man vermeiden, indem man in der Methode „load_and_to_hash“, die bei der Initialisierung des Objektes aufgerufen wird, eine weitere Liste erzeugt, die alle Wortpaare am Anfang eines Satzes enthält. Wortpaare am Anfang eines Satzes lassen sich bspsweise dadurch erkenne, dass das Wort davor mit einem Punkt endet.
#... @begins = words_cons.inject([]) do |res, (word1, word2, word3)| if word1[/[\.!?]/] res << [word2, word3] else res end end #... #die Liste muss instanziert sein, damit man außerhalb der Methode darauf zugreifen kann #inject = fold und fold ist sehr mächtig...;D
Nachdem die Änderung vorgenommen wurde, kann man die Methode „random_sentence“ schreiben, die automatisch einen beliebigen Satz generiert.
def random_sentence self.sentence(*@begins[rand(@begins.length)]) end
Der gesamte Code sieht jetzt so aus:
#encoding: utf-8 class MarkovGenerator #attr_reader/attr_writer attr_accessor :filename, :hash def initialize(filename) @filename = filename @hash = load_and_to_hash end private def load_and_to_hash data = open(@filename).read words = data.split(/[ \n\r]/).reject{|w| w == "" } words_cons = words.each_cons(3).to_a @begins = words_cons.inject([]) do |res, (word1, word2, word3)| if word1[/[\.!?]/] res << [word2, word3] else res end end markovhash = Hash.new{|hash, key| hash[key] = []} words_cons.each do |word_group| key = word_group[0..1] value = word_group[2] markovhash[key] << value end return markovhash end public def next(word1, word2) key = [word1, word2] value_lis = @hash[key] value_lis[rand(value_lis.length)] end def sentence(word1, word2) result = [word1, word2] loop do word3 = self.next(result[-2], result.last) result << word3 break if word3[/[\.!?]/] end result.join(" ") end def random_sentence self.sentence(*@begins[rand(@begins.length)]) end end
Für meine eigene Version habe ich noch folgende Zeilen im Anschluss geschrieben, die mir ermöglichen den Generator außerhalb der irb-Umgebung zu benutzen.
unless ARGV.length == 1 puts "filename eingeben!" else text = MarkovGenerator.new(ARGV[0]) puts text.random_sentence end
Damit kann ich im Terminal äußerst philosophische Sätze wie diese generieren:
~/programme$ ruby rubykurs.rb jenseits_von_gut_und_boese.txt 245. Die "gute alte" Zeit ist dahin, in Mozart hat sie weggebannt: Nur wer sich nicht in Ordnung. ~/programme$ ruby rubykurs.rb jenseits_von_gut_und_boese.txt ...nicht aus Mangel an Noth, sondern aus Mangel an Fingern und Handhaben für seine Handlungen... ~/programme$ ruby rubykurs.rb jenseits_von_gut_und_boese.txt Ihr Denken ist eine Circe, auch die eigentliche Meisterschaft und Feinheit im Kriegführen mit sich, also Selbst-Beherrschung, Selbst-Überlistung hinzuvererbt und angezüchtet: so entstehen jene zauberhaften Unfassbaren und Unausdenklichen, jene zum Siege und zur Verführung vorherbestimmten Räthselmenschen, deren schönster Ausdruck Alciblades und Caesar (- denen ich gerne jenen ersten Europäer nach meinem Geschmack, den Hohenstaufen Friedrich den Zweiten zugesellen möchte), unter Künstlern und Gelehrten von ihrer Utilität denken.
WOW…
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.
In meinem letzten Post habe ich beschrieben, wie ich die Originaldatensätze des Wadoku-Wörterbuchs von Dr. Ulrich Apel extrahiert habe. Leider war das Ergebnis, was daraus kam, nicht ganz das, was sich mein Auftraggeber wünschte.
Zur Erinnerung: so sah mein Ergebnis aus:
1655423 あ [1]; ア あ [1] <TrE: <HW n: a>> 1655423 あ [1]; ア あ [1] <TrE: <HW n: A>> 1655423 あ [1]; ア あ [1] <TrE: <HW m: Vokal> „<Topic: a>“> 1655423 あ [1]; ア あ [1] <TrE: <HW m: Lautwert> „<Topic: a>“>> 1655423 あ [1]; ア あ [1] <TrE: <HW m: Lautwert> „<Topic: a>“ in der 50-Laute-Tafel> 1655423 あ [1]; ア あ [1] <TrE: erstes <HW n: Zeichen> der ersten Reihe der 50-Laute-Tafel> 1655423 あ [1]; ア あ [1] <TrE: 36. <HW n: Zeichen> des Iroha-Gedichtes> 1655423 あ [1]; ア あ [1] <TrE: <HW n: Hiragana> „<Topic: a>“> 1655423 あ [1]; ア あ [1] <TrE: <Jap.: あ>> 1655423 あ [1]; ア あ [1] <TrE: <HW n: Katakana> „<Topic: a>“> 1655423 あ [1]; ア あ [1] <TrE: <Jap.: ア>>
Also schematisch gesehen
ID \t Schreibung 1; Schreibung 2; Schreibung 3 \t Lesung in Kana \t Übersetzungsäquivalent 1 \n ID \t Schreibung 1; Schreibung 2; Schreibung 3 \t Lesung in Kana \t Übersetzungsäquivalent 2 \n ID \t Schreibung 1; Schreibung 2; Schreibung 3 \t Lesung in Kana \t Übersetzungsäquivalent 3 \n
Das gewünschte Ergebnis sollte aber wie folgt ausehen:
ID \t Schreibung 1 \t Lesung in Kana \t Übersetzungsäquivalent 1 \n ID \t Schreibung 1 \t Lesung in Kana \t Übersetzungsäquivalent 2 \n ID \t Schreibung 1 \t Lesung in Kana \t Übersetzungsäquivalent 3 \n ID \t Schreibung 2 \t Lesung in Kana \t Übersetzungsäquivalent 1 \n ID \t Schreibung 2 \t Lesung in Kana \t Übersetzungsäquivalent 2 \n ID \t Schreibung 2 \t Lesung in Kana \t Übersetzungsäquivalent 3 \n ID \t Schreibung 3 \t Lesung in Kana \t Übersetzungsäquivalent 1 \n ID \t Schreibung 3 \t Lesung in Kana \t Übersetzungsäquivalent 2 \n ID \t Schreibung 3 \t Lesung in Kana \t Übersetzungsäquivalent 3 \n
Der Unterschied besteht darin, dass in meiner Version, die Schreibung1,2,3 nicht getrennt waren, was aber gewünscht wurde. Also musste ich mir was neues einfallen lassen.
Mein erster Gedanke war, dass ich auf die aktuelle „falsche“ Datei „wdk_list2“ korrigieren könnte, indem ich den zweiten Teil(im Array) jedes Eintrags spalte und den Rest(ID, Lesung und TrE) einfach jeweils wieder in der richtigen Reihenfolge dranhänge.
["ID", "Schreibung1;Schreibung2;Schreibung3", "Lesung in Kana", "TrE"] => [["ID", "Schreibung1", "Lesung in Kana", "TrE"] ["ID", "Schreibung2", "Lesung in Kana", "TrE"] ["ID", "Schreibung3", "Lesung in Kana", "TrE"]]
Also habe ich zunächst eine Methode geschrieben, die mir in einem Eintrag die verschiedenen Schreibungen trennt und als Array zurückgibt.
"ID \t Schreibung 1; Schreibung 2; Schreibung 3 \t Lesung in Kana \t Übersetzungsäquivalent 1" => ["ID \t Schreibung 1 \t Lesung in Kana \t Übersetzungsäquivalent 1", "ID \t Schreibung 2 \t Lesung in Kana \t Übersetzungsäquivalent 1", "ID \t Schreibung 3 \t Lesung in Kana \t Übersetzungsäquivalent 1"]
#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
Dabei ist mir aufgefallen, dass die Methode „insert“ für die Klasse Array das eigentliche Objekt verändert, was ich in diesem Fall nicht gebrauchen kann.
Bsp:
ruby-1.9.2-p180 :207 > a = [1,3] => [1, 3] ruby-1.9.2-p180 :208 > a.insert(1,2) => [1, 2, 3] ruby-1.9.2-p180 :209 > a => [1, 2, 3] # #ich hätte aber gerne a unverändert # ruby-1.9.2-p180 :207 > a = [1,3] => [1, 3] ruby-1.9.2-p180 :208 > a.myinsert(1,2) => [1, 2, 3] ruby-1.9.2-p180 :209 > a => [1, 3]
Also musste ich noch eine eigene „insert“-Methode für die Klasse Array schreiben(„my_ultra_sexy_insert(pos, el)“…;D).
class Array def myinsert(pos, el) self.dup.insert(pos, el) end end
Als das getan war, probierte ich die Methode an einigen Einträgen aus der „wdk_list2“-Datei aus. Das Ergebnis war nicht ganz zufriedenstellend, denn die Reihenfolge der Schreibungen war falsch.
Statt der gewünschten Reihenfolge:
ID \t Schreibung 1 \t Lesung in Kana \t Übersetzungsäquivalent 1 \n ID \t Schreibung 1 \t Lesung in Kana \t Übersetzungsäquivalent 2 \n ID \t Schreibung 1 \t Lesung in Kana \t Übersetzungsäquivalent 3 \n ID \t Schreibung 2 \t Lesung in Kana \t Übersetzungsäquivalent 1 \n ID \t Schreibung 2 \t Lesung in Kana \t Übersetzungsäquivalent 2 \n ID \t Schreibung 2 \t Lesung in Kana \t Übersetzungsäquivalent 3 \n ID \t Schreibung 3 \t Lesung in Kana \t Übersetzungsäquivalent 1 \n ID \t Schreibung 3 \t Lesung in Kana \t Übersetzungsäquivalent 2 \n ID \t Schreibung 3 \t Lesung in Kana \t Übersetzungsäquivalent 3 \n
kam dabei raus:
ID \t Schreibung 1 \t Lesung in Kana \t Übersetzungsäquivalent 1 \n ID \t Schreibung 2 \t Lesung in Kana \t Übersetzungsäquivalent 1 \n ID \t Schreibung 3 \t Lesung in Kana \t Übersetzungsäquivalent 1 \n ID \t Schreibung 1 \t Lesung in Kana \t Übersetzungsäquivalent 2 \n ID \t Schreibung 2 \t Lesung in Kana \t Übersetzungsäquivalent 2 \n ID \t Schreibung 3 \t Lesung in Kana \t Übersetzungsäquivalent 2 \n ID \t Schreibung 1 \t Lesung in Kana \t Übersetzungsäquivalent 3 \n ID \t Schreibung 2 \t Lesung in Kana \t Übersetzungsäquivalent 3 \n ID \t Schreibung 3 \t Lesung in Kana \t Übersetzungsäquivalent 3 \n
Das Problem lag daran, dass als ich die Datei „wdk_list2“ aus „wdk_list“ generiert habe, ich zunächst die Übersetzungsäquivalente getrennt habe. Folglich müsste ich die „generate2“-Methode so modifizieren, dass zunächst die Schreibungen getrennt werden und dann die Übersetzungsäquivalente.
Die alte „generate2“-Methode sieht so aus:
def generate2 wdk content = File.open(wdk, "r").readlines #hier kann ich die neue Methode sep_w einbauen new_content = content.map do |entry| #der vordere Teil mit "wadoku_id", "writing", "midashigo" oder "kana" pre = entry.split("\t").values_at(0,1,2).join("\t") #die TrEs zusammengefasst in Array tre_array = parse(entry.split("\t")[3]) #pre + tre tre_array.map{|tre| pre + "\t" + tre}.join("\n") end #das ganze wird in die neue Datei "wdk_list2" reingeschrieben file = File.new("wdk_list2", "w") file.puts new_content file.close end
Die neu geschriebene Methode „sep_w“ wird hinzugefügt:
def generate2 wdk content = File.open(wdk, "r").readlines.map{|l| sep_w l}.flatten #aus einem verschachtelten Array wird ein Array new_content = content.map do |entry| pre = entry.split("\t").values_at(0,1,2).join("\t") tre_array = parse(entry.split("\t")[3]) tre_array.map do |tre| n_tre = tre.end_with?(">") ? tre : tre + ">" pre + "\t" + n_tre end.join("\n") end file = File.new("wdk_list2", "w") file.puts new_content file.close end
Daraufhin habe ich noch einmal die Methode „generate2“ mit der alten Datei „wdk_list“(nicht wdk_list2!, die ist ja schon modifiziert worden) ausgeführt.
Diesmal war das Ergebnis wesentlich besser anzusehen:
9110017 手いけ ていけ <TrE: <Def.: <HW n: Gestalten> seines eigenen Blumenarrangements>> 9110017 手いけ ていけ <TrE: (<Usage: übertr.>) <Def.: <HW m: Freikauf> einer Geisha, um sie zur Geliebten oder Ehefrau zu machen>> 9110017 手活け ていけ <TrE: <Def.: <HW n: Gestalten> seines eigenen Blumenarrangements>> 9110017 手活け ていけ <TrE: (<Usage: übertr.>) <Def.: <HW m: Freikauf> einer Geisha, um sie zur Geliebten oder Ehefrau zu machen>> 9110017 手活 ていけ <TrE: <Def.: <HW n: Gestalten> seines eigenen Blumenarrangements>> 9110017 手活 ていけ <TrE: (<Usage: übertr.>) <Def.: <HW m: Freikauf> einer Geisha, um sie zur Geliebten oder Ehefrau zu machen>> 9110017 手生け ていけ <TrE: <Def.: <HW n: Gestalten> seines eigenen Blumenarrangements>> 9110017 手生け ていけ <TrE: (<Usage: übertr.>) <Def.: <HW m: Freikauf> einer Geisha, um sie zur Geliebten oder Ehefrau zu machen>> 9110017 手生 ていけ <TrE: <Def.: <HW n: Gestalten> seines eigenen Blumenarrangements>> 9110017 手生 ていけ <TrE: (<Usage: übertr.>) <Def.: <HW m: Freikauf> einer Geisha, um sie zur Geliebten oder Ehefrau zu machen>>
Achja…nachdem ich die Datei generiert hatte, musste ich noch die bekannte „kill_n“-Methode ausführen, damit die überflüssigen „\n“ schön brav „weggeblieben“ werden.
Obwohl diese Version im Vergleich zu den anderen richtiger aussieht, habe ich trotzdem einige Einträge gefunden, die fehlerhaft angezeigt werden z.B:
7198845 泥砂 でいしゃ <TrE: <HW m: Schlamm> und <HW m: Sand>> 7198845 b でいしゃ <TrE: <HW m: Schlamm> und <HW m: Sand>> 7198845 泥沙 でいしゃ <TrE: <HW m: Schlamm> und <HW m: Sand>>
oder
5885835 出かけ でかけ <TrE: <HW m: Aufbruch> 5885835 出かけ でかけ <TrE: <HW n: Fortgehen>> 5885835 出掛け でかけ <TrE: <HW m: Aufbruch> 5885835 出掛け でかけ <TrE: <HW n: Fortgehen>> 5885835 a でかけ <TrE: <HW m: Aufbruch> 5885835 a でかけ <TrE: <HW n: Fortgehen>> 5885835 出掛 でかけ <TrE: <HW m: Aufbruch> 5885835 出掛 でかけ <TrE: <HW n: Fortgehen>> 5885835 でかけ でかけ <TrE: <HW m: Aufbruch> 5885835 でかけ でかけ <TrE: <HW n: Fortgehen>> 4371129 出掛け でがけ <TrE: <HW m: Aufbruch> 4371129 出掛け でがけ <TrE: <HW m: Zeitpunkt>, wenn man weggeht> 4371129 出掛け でがけ <TrE: <HW m: Zeitpunkt>, gleich nach dem Aufbruch> 4371129 b でがけ <TrE: <HW m: Aufbruch> 4371129 b でがけ <TrE: <HW m: Zeitpunkt>, wenn man weggeht> 4371129 b でがけ <TrE: <HW m: Zeitpunkt>, gleich nach dem Aufbruch> 4371129 出がけ でがけ <TrE: <HW m: Aufbruch> 4371129 出がけ でがけ <TrE: <HW m: Zeitpunkt>, wenn man weggeht> 4371129 出がけ でがけ <TrE: <HW m: Zeitpunkt>, gleich nach dem Aufbruch> 4371129 出掛 でがけ <TrE: <HW m: Aufbruch> 4371129 出掛 でがけ <TrE: <HW m: Zeitpunkt>, wenn man weggeht> 4371129 出掛 でがけ <TrE: <HW m: Zeitpunkt>, gleich nach dem Aufbruch> 4371129 でがけ でがけ <TrE: <HW m: Aufbruch> 4371129 でがけ でがけ <TrE: <HW m: Zeitpunkt>, wenn man weggeht> 4371129 でがけ でがけ <TrE: <HW m: Zeitpunkt>, gleich nach dem Aufbruch>
Die Einträge mit nur „a“ oder „b“ als Schreibung kommen daher, da es auch Einträge gibt mit Buchstaben-Indexe, die so aussehen:
5885835 出かけ; 出掛け [a]; 出掛; でかけ... 4371129 出掛け [b]; 出がけ; 出掛; でがけ...
Mein Regex hat zwar Zahlen 1-9 in eckigen Klammern rausgefiltert, aber die westlichen Buchstaben sind nicht davon betroffen.
/([^;\(\)\ 1-9\[\]\s]+)/
Das heißt: to be continued… …ó__ò
WadokuJT ist ein freies Japanisch-Deutsch Wörterbuch von Dr. Ulrich Apel, der momentan in Tübingen lehrt.
Ich hatte letztens die Aufgabe, seine Originaldatensätze für eine weitere Verarbeitung zu extrahieren.
Da es sich dabei um mehr als 1000,000 Datensätze handelt, kann man das leider nicht im Word/Open Office per Hand und Copy/Paste überarbeiten…wie schade…;D.
Ich habe mich für eine programmatische Lösung dieses Problems entschieden, dafür wählte ich die Sprache Ruby(in Java wäre ich dabei sicherlich gestorben…bin ja noch ein Babyprogrammierer…ó_ò).
Die Originaldatensätze sehen bspsweise so aus:
1655423 あ [1]; ア あ [1] (<POS: N.>) [1]<MGr: <TrE: <HW n: a>>; <TrE: <HW n: A>>; <TrE: <HW m: Vokal> „<Topic: a>“>; <TrE: <HW m: Lautwert> „<Topic: a>“>> // <MGr: <TrE: <HW m: Lautwert> „<Topic: a>“ in der 50-Laute-Tafel>; <TrE: erstes <HW n: Zeichen> der ersten Reihe der 50-Laute-Tafel>; <TrE: 36. <HW n: Zeichen> des Iroha-Gedichtes>>. [2]<MGr: <TrE: <HW n: Hiragana> „<Topic: a>“>; <TrE: <Jap.: あ>>; <TrE: <HW n: Katakana> „<Topic: a>“>; <TrE: <Jap.: ア>>>. 名 HE あ
Als Endergebnis sollte rauskommen:
1655423 あ [1]; ア あ [1] <TrE: <HW n: a>> 1655423 あ [1]; ア あ [1] <TrE: <HW n: A>> 1655423 あ [1]; ア あ [1] <TrE: <HW m: Vokal> „<Topic: a>“> 1655423 あ [1]; ア あ [1] <TrE: <HW m: Lautwert> „<Topic: a>“>> 1655423 あ [1]; ア あ [1] <TrE: <HW m: Lautwert> „<Topic: a>“ in der 50-Laute-Tafel> 1655423 あ [1]; ア あ [1] <TrE: erstes <HW n: Zeichen> der ersten Reihe der 50-Laute-Tafel> 1655423 あ [1]; ア あ [1] <TrE: 36. <HW n: Zeichen> des Iroha-Gedichtes> 1655423 あ [1]; ア あ [1] <TrE: <HW n: Hiragana> „<Topic: a>“> 1655423 あ [1]; ア あ [1] <TrE: <Jap.: あ>> 1655423 あ [1]; ア あ [1] <TrE: <HW n: Katakana> „<Topic: a>“> 1655423 あ [1]; ア あ [1] <TrE: <Jap.: ア>>
Ein vollständiger Wadoku-Eintrag, wie im ersten Beispiel, setzt sich zusammen aus:
„wadoku_id“ \t „writing“ \t „midashigo“ \t „kana“ \t „definition“ \t der Rest(interessiert erst mal nicht)…
Da für meine Aufgabe nur der Teil bis inklusiv „definition“ gebraucht wird, habe ich zunächst eine neue Datei erstellt mit den vereinfachten Datensätzen.
#Hilfsprozedur um einzelne Einträge zu kürzen def truncate entry array = entry.split("\t") #Die Fallunterscheidung, falls "kana" nicht vorhanden ist...eigentl. nicht nötig...was mir erst jetzt aufgefallen ist array[3] == "" ? array.values_at(0,1,3,4).join("\t") : array.values_at(0,1,2,4).join("\t") end #eine neue Datei aus der alten generieren #wdk = Name der Wadoku-Raw-Datei def generate wdk content = File.open(wdk, "r").readlines new_content = content.map{|entry| truncate entry}.join("\n") file = File.new("wdk_list", "w") #neue Datei öffnen mit Schreibrecht file.puts new_content file.close end
Die neue Datei mit dem Namen „wdk_list“ enthält nur die 4 Komponente, die für die weitere Verarbeitung wichtig sind(Dieser Schritt hätte man sich eigentl. auch sparen können…aber sowas fällt nur hinterher auf…MANN):
1655423 あ [1]; ア あ; ア あ [1] (<POS: N.>) [1]<MGr: <TrE: <HW n: a>>; <TrE: <HW n: A>>; <TrE: <HW m: Vokal> „<Topic: a>“>; <TrE: <HW m: Lautwert> „<Topic: a>“>> // <MGr: <TrE: <HW m: Lautwert> „<Topic: a>“ in der 50-Laute-Tafel>; <TrE: erstes <HW n: Zeichen> der ersten Reihe der 50-Laute-Tafel>; <TrE: 36. <HW n: Zeichen> des Iroha-Gedichtes>>. [2]<MGr: <TrE: <HW n: Hiragana> „<Topic: a>“>; <TrE: <Jap.: あ>>; <TrE: <HW n: Katakana> „<Topic: a>“>; <TrE: <Jap.: ア>>>.
Anschließend konzentrieren wir uns auf die „definition“, in der die „TrE“-Tags sind, die wir aber einzel haben möchten.(TrE = Translations Equivalent; HW = Headword; MGr = Meaning Group)
Meine Überlegung war, dass ich zunächst einen Array haben möchte, mit nur den „TrE“-Tags.
=> [<TrE: <HW n: a>>, <TrE: <HW n: A>>, <TrE: <HW m: Vokal> „<Topic: a>“>, <TrE: <HW m: Lautwert> „<Topic: a>“>>, <TrE: <HW m: Lautwert> „<Topic: a>“ in der 50-Laute-Tafel>, <TrE: erstes <HW n: Zeichen> der ersten Reihe der 50-Laute-Tafel>, <TrE: 36. <HW n: Zeichen> des Iroha-Gedichtes>,...]
Dafür musste ich mir einen passenden Regex(regulären Ausdruck) ausdenken, der alles innerhalb des „TrE“-Tags parset.
Das erste, was mir eingefallen ist(oder ergegooglet habe) war:
/(<TrE.+?>)/
Aber leider parset der nicht richtig, weil der nur bis zum ersten „>“ geht. Bei Tags wie z.B: <TrE: 36. <HW n: Zeichen> des Iroha-Gedichtes>> in der 50-Laute-Tafel> hat die Hälfte gefehlt.
Mit Hilfe von Rubular konnte ich aber schließlich einen Ausdruck finden, der richtig zu sein schien:
(<TrE.+?)(>;|>\.|\s\/\/)
Und damit konnte ich meine Methode schreiben:
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
Nachdem es nun möglich war, die „TrE“-Tags zu extrahieren, musste ich nur noch die einzelnen Übersetzungsequivalente aus dem Array, mit dem entsprechenden „wadoku_id“, „writing“, „midashigo“ oder „kana“ richtig in ein neues Dokument eintragen.
def generate2 wdk content = File.open(wdk, "r").readlines new_content = content.map do |entry| #der vordere Teil mit "wadoku_id", "writing", "midashigo" oder "kana" pre = entry.split("\t").values_at(0,1,2).join("\t") #die TrEs zusammengefasst in Array tre_array = parse(entry.split("\t")[3]) #pre + tre tre_array.map{|tre| pre + "\t" + tre}.join("\n") end #das ganze wird in die neue Datei "wdk_list2" reingeschrieben file = File.new("wdk_list2", "w") file.puts new_content file.close end
Die Methoden habe ich in der Datei „wdk_method.rb“ gespeichert und wie folgt in irb ausgeführt:
ruby-1.9.2-p180 :098 > load "wdk_method.rb" => true ruby-1.9.2-p180 :099 > generate "WaDokuNormal.tab" #die wadoku-raw Datei mit heimtückischen Groß- und Kleinschreibungen => nil ruby-1.9.2-p180 :100 > generate2 "wdk_list" => nil
Voila!…da habe ich meine extrahierte Version der Datensätze.
Leider mit Schönheitsfehlern wie z.B., dass bei manchen „TrE“-Tags der Schluss Tag „>“ fehlt, wegen meines nicht ganz so professionellen Regexes. Außerdem enthält die Datei massenhafte „\n“s („Carriage Return“-Zeichen), aus unerfindlichen Gründen(liegt wahrscheinl. daran, dass ich die Funktionsweise von IO.readlines nicht ganz kapiert habe. Und im Moment scheint die ruby-doc keine Auskunft über IO geben zu wollen).
Bevor die Profis jetzt anfangen über mich zu lachen, zeig ich noch, was ich daraufhin aus meiner Lächerlichkeit zur Verbesserung der Datei gemacht habe:
def kill_n wdk t = File.open(wdk, "r").readlines.uniq file = File.open(wdk, "w+") file.puts t file.close end
Die „kill_n“-Methode soll alle überflüssigen „\n“s in der Datei killen…;D So jetzt habe ich es verdient, ausgelacht zu werden.