まえがき
ここ数年ぼちぼちと英単語の暗記に取り組んでいるのですが、暗記カード(紙)やTOEIC対策アプリでの学習に主に効率・自由度などの面で限界を感じたので、よりオープンな暗記用ソフトウェア(SRS; Spaced Repetition learning Systems)を使用することにし、Windows, Mac, Linux, Android, iOSなど主要なプラットフォームに対応し、また定番のソフトウェアでもある Anki を新しい学習ツールとして選択しました。
Ankiはインストールした状態では空っぽで、学習するデータは自分で用意する必要があります。僕が覚えるのは英語で、英語学習に使えるデータは豊富にあります。例えば
単語
語義
などを組み合わせて「表が単語、裏が語義の暗記カード」を(論理的には)作ることが可能ですが、しかし実際にこれを簡単に行うためのツールが見つからなかったため、時間を作って作成することにしました。
要件
当初考えていたのは
- AnkiがサポートするCSV, TSVなどを扱えること。
- 外部のコマンドを叩いて、データを取得できること。
といったふんわりとした要件で、Ankiのデッキを作るツール(概案) のようなものを考えていたのですが、どうもイマイチな気がしていました。そこでさらに調査を進めるうちに驚くべきツールを発見しました。AWKです。非常に有名なツールで、僕もそれまで幾度となく触ったことがあったのにも関わらず、実際には全くこのツールの真の価値を知らなかったわけです。
AWKは正に上記の要件を満たす素晴らしいツールに思えましたが、いくつかの問題があり、結局使用することはできませんでした。しかしAWKのコンセプトは素晴らしいもので、これをそのまま取り入れ、多少の変更を加えたものを実装することにしました。
WOK
AWKはオークと呼ぶらしいです。2014年現在、これと対になるような単語は一つしか思い浮かびません。WOman Knight、すなわち女騎士です。
WOKはAWKにPython互換の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")
サンプルスクリプト
指定した列を元に、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操作に慣れていなければ難しくもあります。このあたりは今後の課題とします。