Zum Inhalt springen

Term Paper for Seminar „Software Construction Techniques: Writing Good Code“

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

Install seafile server + seahub on FreeBSD

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

Configure Vim for basic coding in Java

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

&quot;syntastic&quot;                                                                                                             
let g:syntastic_java_checker = 'javac'
let g:syntastic_java_javac_classpath = &quot;./lib/*.jar\n./src&quot;

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

&quot;nerdtree&quot;                                                                                                                 
map &lt;C-n&gt; :NERDTreeToggle&lt;CR&gt;

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.

Falsche Bestellung gerettet: Ethernetkabel Rj45/SubD9 => Rj45/Rj45

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

Image

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.

Image

So jetzt kann man testen ob alles funktioniert hat:

Image

 

Image

ping heise.de sagt ja! 😉

Euklid und Ruby

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.

Markovgenerator

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…

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.

  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.

WadokuJT-Datensätze extrahieren mit Ruby II

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-Datensätze extrahieren mit Ruby

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.