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

まえがき

ここ数年ぼちぼちと英単語の暗記に取り組んでいるのですが、暗記カード(紙)や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操作に慣れていなければ難しくもあります。このあたりは今後の課題とします。

WindowsでProcessBuilderの文字コード周りがどうなるか検証した

外部のプログラムにデータを渡して、その結果を得たい。 標準入出力では問題が起こりそうにないが、コマンドライン引数まわりは怪しく思える。 Windows以外の、システムのデフォルトエンコーディングUTF-8な環境なら何も問題なくイケそうだが、Windowsではどうか。ここではコマンドライン引数について検証する。

こんなのを用意して、

object OuterProcess {
  def build(commands: List[List[String]]): Option[ProcessBuilder] = {
    if (commands.nonEmpty) {
      build(commands.tail) match {
        case Some(x) => Some(commands.head #| x)
        case None => Some(commands.head)
      }
    } else {
      None
    }
  }
  def call(commands: List[List[String]]): String = {
    build(commands) match {
      case Some(x) => x !!
      case None => ""
    }
  }
}

-Dfile.encoding=UTF-8な設定で以下のテストを実施した。

class OuterProcessTest extends SpecificationWithJUnit {
  "OuterProcess.call" should {
    "receive UTF-8 encoded output" in {
      OuterProcess.call(List(
        List("cmd", "/c", "type", "text.txt")
      )) mustEqual("éindʒəl") //success
    }
    "pass unicode arguments to a program" in {
      OuterProcess.call(List(
        List("cmd", "/c", "echo", "éindʒəl")
      )) mustEqual("éindʒəl") //fail
    }
    "pass unicode arguments to a program" in {
      OuterProcess.call(List(
        List("cmd", "/c", "chcp", "65001", "&&", "cmd", "/c", "echo", "éindʒəl")
      )) mustEqual("éindʒəl") //success
    }
  }
}

というわけで、引数は正しくUnicodeなまま、対象のプログラムに渡されている。そして、このechoのように、プログラムがUnicodeな引数を正しく扱えるかどうかは、そのプログラム自体(またはその設定など)に依存するようだ。例えば、Windowsで動くgrepを用意して以下のテストを行う場合、Gowgrepでは失敗してGnupackのものでは成功する。これは前者がUnicode(マルチバイト全般?)を与えた時にうまく動作せず、後者では問題ないため。

class OuterProcessTest extends SpecificationWithJUnit {
  "OuterProcess.call" should {
    "connect programs" in {
      OuterProcess.call(List(
        List("cmd", "/c", "chcp", "65001", "&&", "cmd", "/c", "echo", "éindʒəl"),
        List("grep", "éindʒəl")
      )) mustEqual("éindʒəl")
    }
  }
}

210円でiPhone5のケースにストラップを装着する方法

iPhone5を購入して、その足で100円ショップのセリアに寄って液晶保護シートとクリアケースを確保したのが昨日のこと。思いのほかケースの出来がよく、かなり満足なのですが、残念ながらストラップホールがありません。ケースに穴を開けてストラップを通すのもアリなんでしょうが、ケースの強度低下や、ストラップのヒモとiPhone本体との干渉などの不安材料があり、別の方法がないかなぁと考えました。

この改造のためのパーツを集めた100円ショップがちいさいダイソーだったので、他のショップにはもっといいものがあるかもしれませんが、今回はこんな感じです。

材料

  • 結束バンドベース (20mm角)
  • 超強力両面テープ (幅19mm)

方法

  1. 結束バンドベースにあらかじめ張ってある両面テープを剥がし、超強力両面テープ(19mm角)に張り替えます。
  2. クリアケースの好きな位置に貼ります。
  3. 完成!

どうでしょうか? それほど不恰好ではないと思います。つるつるのケースはどうしても手から離れやすいので、ストラップがあれば安心です。

Kobo Touchで目次画面の現在地が後方にズレる問題の対策

Kobo TouchはどうやらEPUBの目次に全てのページが存在していることを前提としているようで、

metadata.opfのspine内のN番目のitemref要素を開いている = toc.ncxのN番目のnavPoint要素が現在地ねっ!

と解釈するようです。目次画面を開いたとき、現在地が後方にズレるのはこれが原因です。楽天Koboから大手出版社の小説のサンプル版をいくつかダウンロードしてみましたが、普通にズレていましたので、あまり気にする人はいないのかもしれません。(というか使ってる人がそもそもいない…?)ですが、今自分がどこを開いているのかがわからないというのは、用途によってはかなり致命的です。

ところで、自炊した画像データやPDFからEPUBを作成するにはChainLPが大変便利で、僕はこのツールでデータをEPUBに変換しています。ChainLPで作成したEPUBは、一枚の画像(を表示するためのページ)と一つのitemrefが1対1になっています。このため、データに目次を設定し、出来上がったEPUBをKobo Touchで閲覧する場合、目次の現在位置が後方に(正確には、開いているページがN番目のページであった場合、N番目の目次に)ズレることになります。悲しいことですが、悲しんでばかりもいられません。少し考えると、Kobo Touchのこの仕様への対策がいくつか思いつきました。

対策A 全てのページを目次に登録する

目次の動作がすごく遅くなり、実用上問題がありました。また、意味のある目次項目が、便宜上加えられた無意味な目次項目に埋もれてしまい、加えてその位置が不規則に分散してしまうため、非常に使いづらいです。これはやめておいたほうがよいでしょう。

対策B 目次ごとにページを統合する

要するに

ページ番号 見出し
1 表紙
2
3 まえがき
4
5 第一章
6

とある場合、

ページ番号 見出し
1, 2 表紙
3, 4 まえがき
5, 6 第一章

のようにセクションごとにページをまとめてしまうわけです。この方法が今のところ最良だと思います。ページ送りも問題なく行え、またセクション内のでページ送りは、単一のページに格納する場合より高速なようです。 ChainLPで作成したEPUBに、この変更を施すスクリプトが以下です。

ところで、Kobo Touchで目次を使って別の位置にジャンプしたあと、”戻る”的な操作がうまく動作しないのはどうしてなんでしょうね…? しかたなく毎回しおりを挟んで、あとでそこに戻っていますが…。不便!

ChainLPで作成したEPUBで目次が使えない問題の対策

Open Packaging Format (OPF) 2.0.1 v1.0(http://www.idpf.org/epub/20/spec/OPF_2.0.1_draft.htm#Section2.4) の

The spine element must include the toc attribute, whose value is the the id attribute value of the required NCX document declared in manifest (see Section 2.4.1.)

あたりが問題のようです。 以下のように単純に書き換えるだけで、Kindle PaperwhiteやKobo Touchなどで目次が使えるようになります。

Kindleを買ってから延長保証を追加しようとして失敗した話

初期不良のKindleを新品交換すると、延長保証に入れなくなる

というのが今回のお話でして、実際に僕が体験したことを書こうと思います。

届いた!しかし…

いろいろ触っていると、画面に輝点というか、ディスプレイ内に金属片のようなものがあり、バックライトが反射して常に眩しく光っている感じだったのです。電話をし、返品をお願いしました。そして交換品が届き…

交換品は良い!延長保証を追加しよう!

再び電話で問い合わせ。調査後、メールで結果を報告してもらうことに。

無慈悲なメール

電話をした次の日にメールが届きました。

以下、メールを抜粋したもの

XXXX様

Amazon.co.jpにご連絡いただき、ありがとうございます。

このたびは、当サイトのご利用に際し、ご不便をおかけいたしましたことをお詫び申し上げます。

今回の延長保証を購入できない件につきまして確認させていただきましたところ、交換させていただきました端末には新たに購入した延長保証を紐付けることができないとの回答がございました。

そのため、お客様にご注文を進めていただきました際にエラーの表示がされたかと存じます。

お客様のご期待に沿う返答とならず、誠に恐れ入りますが、ご容赦いただきますようお願いいたします。

Amazon.co.jpのまたのご利用を心よりお待ちしております。

素早いレスポンスと正直な回答はGoodなのですが、改善策などは提示されず。ええーと思いつつ、仕方なく新しいものを注文しました。

そしてオチです

どうやら一度でも新品交換しちゃうと、その後購入したぶんにも延長保証を追加したりできないみたい…?

Kindle Paperwhite
Kindle Paperwhite
posted with amazlet at 13.05.13
Amazon.co.jp (2012-11-19)
売り上げランキング: 1
Kindle Paperwhite用長期保証 (自然故障・不具合を1年延長)
Techmark Japan (2012-11-19)
売り上げランキング: 247