夏休みの自由研究 「女騎士」 提出しました

まえがき

ここ数年ぼちぼちと英単語の暗記に取り組んでいるのですが、暗記カード(紙)やTOEIC対策アプリでの学習に主に効率・自由度などの面で限界を感じたので、よりオープンな暗記用ソフトウェア(SRS; Spaced Repetition learning Systems)を使用することにし、Windows, Mac, Linux, Android, iOSなど主要なプラットフォームに対応し、また定番のソフトウェアでもある Anki を新しい学習ツールとして選択しました。

Ankiはインストールした状態では空っぽで、学習するデータは自分で用意する必要があります。僕が覚えるのは英語で、英語学習に使えるデータは豊富にあります。例えば

単語

語義

  • E-DIC2
  • システムソフト電子辞典(Epwing辞書に変換可能なもの)
  • Epwing辞書

などを組み合わせて「表が単語、裏が語義の暗記カード」を(論理的には)作ることが可能ですが、しかし実際にこれを簡単に行うためのツールが見つからなかったため、時間を作って作成することにしました。

要件

当初考えていたのは

  • AnkiがサポートするCSV, TSVなどを扱えること。
  • 外部のコマンドを叩いて、データを取得できること。

といったふんわりとした要件で、Ankiのデッキを作るツール(概案) のようなものを考えていたのですが、どうもイマイチな気がしていました。そこでさらに調査を進めるうちに驚くべきツールを発見しました。AWKです。非常に有名なツールで、僕もそれまで幾度となく触ったことがあったのにも関わらず、実際には全くこのツールの真の価値を知らなかったわけです。

AWKは正に上記の要件を満たす素晴らしいツールに思えましたが、いくつかの問題があり、結局使用することはできませんでした。しかしAWKのコンセプトは素晴らしいもので、これをそのまま取り入れ、多少の変更を加えたものを実装することにしました。

WOK

AWKはオークと呼ぶらしいです。2014年現在、これと対になるような単語は一つしか思い浮かびません。WOman Knight、すなわち女騎士です。

rubyu/wok

WOKはAWKPython互換のCSVモジュールと、JavaとそしてScalaの豊富なライブラリを追加したものになります。

女騎士 vs オーク

Running a script

$ wok 'print("hello!")'
$ awk 'BEGIN { print "hello!" }'

Running a program file

$ wok -f program-file input
$ awk -f program-file input

Variables

$ wok -v name=value
$ awk -v name=value

Operating fields

In { _ foreach { row => 
  println(row(0)) 
}}
{ print $1 }

Filtering data

In { _ foreach {
  case row if row exists ("pattern".r.findFirstIn(_).isDefined) => 
    println(row: _*)
  case _ =>
}}
/pattern/ { print $0 }

Printing to a file

val path = "list" !<
In { _ foreach { _ =>
  path.println(NF)
}}
{ print NF > "list" }

Executing a system command

In { _ foreach { row =>
  val res = Seq("echo", row(0)) #| Seq("grep", "angel") !>
  println(res.string)
  println(res.code)
}}

女騎士の拡張された部分

Typed variables

# The following command is equivalent to 
# var name = `value`
$ wok -v@char name=value

# The following command is equivalent to 
# var name = "value"
$ wok -v@str name=value

# The following command is equivalent to 
# var name = """value"""
$ wok -v@rawstr name=value

Quoting

// Setting Quote(mode=Min, quote='"') to Reader
FQ = Quote Min 

// Setting Quote(mode=All, quote='"', escape='\\') to Writer
OFQ = Quote All Q('"') E('\\')

Encoding

// Setting Codec to Reader
CD = Codec("UTF-8")

// Setting Codec to Writer
OCD = Codec("UTF-16")

サンプルスクリプト

rubyu/wok-scripts より

指定した列を元に、Epwingから辞書引きした列を追加する

eb-html.wok

#!/usr/bin/wok -f

/** eb-html.wok
  * 
  * Usage:
  *   java -jar wok-0.1.0.jar -f eb-html.wok -v src=0 -v dst=1 -v@rawstr dic=path_to_epwing -v@rawstr ebmap=path_to_ebmap < input.tsv > output.tsv
  *
  * Option:
  *   -v src:           the index number of the source field
  *   -v dst:           the index number of the destination field
  *   -v@rawstr dic:    path to a directory containing Epwing's CATALOGS file
  *   -v@rawstr ebmap:  path to a EBWin3/4 map file corresponds to a EPWING dictionary specified by option `dic`
  */

FS = '\t'
FQ = Quote.Min
OFS = '\t'
OFQ = Quote.All

val eb = "ebquery-0.3.1.jar"

if (eb nonExistent) 
  eb write Resource.fromURL("https://bitbucket.org/rubyu/ebquery/downloads/ebquery-0.3.1.jar").bytes

def query(s: String) = Seq("java", "-Dfile.encoding=UTF-8", "-jar", eb, "-d", dic, "-f", "html", "-m", "tx,sb,sp,ec,ls", "--ebmap", ebmap, s).!>.string

In { _ 
  .filter (_ isDefinedAt src)
  .map (_.padTo(dst+1, ""))
  .map (row => row.updated(dst, query(row(src))))
  .foreach (row => println(row: _*))
}

list1.txt

a
abacus
abalone

ここでlist1.txtを単語のリストとして、以下のコマンドを実行できます。

> java -jar wok-0.1.0.jar -f eb-html.wok -v src=0 -v dst=1 -v@rawstr dic=C:\dic\KENE7J5 -v@rawstr ebmap=C:\dic\KENE7J5.MAP list1.txt > list2.txt

list2.txt

word definition
a a1, A1 /éı/→音声 ( as, a's, As, A's ...
abucus ab・a・cus /ǽbəkəs/ ( 〜・es, ―ci...
abalone ab・a・lo・ne /æ̀bəlóʊni/ 〔貝〕 ...

指定した列に、外部の頻度順データ等でのランキング列を追加する

rank.wok

#!/usr/bin/wok -f

/** rank.wok
  * 
  * Usage:
  *   java -jar wok-0.1.0.jar -f rank.wok -v src=0 -v dst=1 -v grp=1 -v@rawstr ranking=list.txt < input.tsv > output.tsv
  *
  * Option:
  *   -v src:               the index number of the source field
  *   -v dst:               the index number of the destination field
  *   -v grp:               the unit size of grouping the rank value; this value must be larger than zero
  *   -v@rawstr ranking:    path to a ranking file
  */

FS = '\t'
FQ = Quote.Min
OFS = '\t'
OFQ = Quote.All

val rank = In.from(ranking) { _
  .zipWithIndex
  .map { case (row, i) => row(0) -> i }
  .toMap
}

In { _
  .filter (_ isDefinedAt src)
  .map (_.padTo(dst+1, ""))
  .map { row =>
    val k = row(src)
    rank.get(k) match {
      case Some(v) => row.updated(dst, s"${v / grp + 1}")
      case None => row
    }
  }
  .foreach (row => println(row: _*))
}

ranking.txt

a
aa
ab
aah
aardvark
abucus
abaca
abalone

ここでlist2.txtを単語のリストとして、以下のコマンドを実行できます。

> java -jar wok-0.1.0.jar -f rank.wok -v src=0 -v dst=2 -v grp=1 -v@rawstr ranking=ranking.txt list2.txt > list3.txt

list3.txt

word definition rank
a a1, A1 /éı/→音声 ( as, a's, As, A's ... 1
abucus ab・a・cus /ǽbəkəs/ ( 〜・es, ―ci... 6
abalone ab・a・lo・ne /æ̀bəlóʊni/ 〔貝〕 ... 8

あとがき

我々には夏休みに何かを作ろうとする習性が刷り込まれているのではないか、そしてそれは義務教育が我々にもたらす最も素晴らしいものの一つではないか、などと思ったりします。

さて、夏休みどころか、さらに数ヶ月をまるまる投じて制作した本ソフトウェアですが、起動時にScalaファイルをコンパイルするためメモリを大量に消費する、同じくコンパイルのために数秒の待ち時間が発生する、記述が冗長になるなどの様々な短所はありますが、一方で、クォートが扱える、コンパイル時に型エラーがないことが保証される、ScalaのCollectionフレームワークに乗っかれるなどの長所もあります。

英語学習用デッキを生成するためのツールとして十分な機能を備えてはいますが、CUI操作に慣れていなければ難しくもあります。このあたりは今後の課題とします。