Emacs + sdicから英辞郎v135を使えるようにするまでの顛末

前から買おう買おうと思ってましたが、仕事も辞めることですし、1980円で買えたので(データのみ)、
こいつをsdic経由でEmacsから利用できるようにしてみたいと思います。

なお、同じようなことは昔から様々な方が書いており、

などなど、探せば色々出てきます。

ただ、私のようにデータだけ購入した人間はそういないらしいので、出来るようになるまでの顛末と共に記録しておきたいと思います。

今回私が購入したのはEDPのVersion.135 というものです。
英辞郎の書籍はCD-ROMだそうですが、こいつは二つの形式が入っています。

  • PDIC/Unicode形式
  • CD-ROMでの検索に利用するための形式・・・だそうです。ぶっちゃけ次のテキスト形式が大事です。
  • テキスト形式
  • 上のPDIC/Unicode形式をPDIC一行形式?に変換したものです。ちなみにVer.135の時点では全部合わせて502MBありました。

Webの情報を見ると、ほとんどの方がまずPDICを変換するところからスタートしているようですが、今回はすでに手元にテキストデータがあるので
これをsdic形式に変換していくことにします。

ただし、このテキストデータは他で変換しているような、PDIC1行テキスト形式とかいうものではなく、次のようなフォーマット
になっています。この形式は、sdicに添付されているeijirou.perlの中身から読み取らせて頂きました。

全体は、一行につきひとつの語と訳語のセットが入れられた形式。
1. 先頭に全角記号■
2. 単語/例文と訳語は:(コロン)で区切られる
3. 単語と:の間で{}で区切られているのは品詞
4. 訳語側の{}は漢字のルビ
5. 訳語側の()は、日本語に対応する英語
6. 訳語側の◆は、その前の文字列に対する補足情報(参考、簡単な解説など)
7. 複数の訳語がある場合は●で区切る
8. ;についてがよくわかりませんでした(ぉぃ

こんな形式になっているようです。実際のデータを眺めてみても、この形式であるのはほぼ間違いないようでした。

さて、普通はじゃあ変換するかー、ということになるのでしょうが、ここで回り道(笑)をすることにしました。せっかく今現在OCaml
勉強しているのですから、それで書こうではないか、となりました。せっかく仕様も(大体)わかったことですし。

とりあえず↓のようなOCamlを書いてみました。いや、Rubyとかの方が色々と楽なのはわかっているのですが・・・。

let waei = ref false

(* regexps *)
let reg_newline = ref (Str.regexp "\\(\n\\|\r\n\\|\r\\)$")
let reg_amp = ref (Str.regexp "&")
let reg_gt = ref (Str.regexp ">")
let reg_lt = ref (Str.regexp "<")
let reg_rectangle = ref (Str.regexp "^■")
let reg_split = ref (Str.regexp " +: +")
let reg_translation = ref (Str.regexp " +{[^}]+}")
let reg_ruby = ref (Str.regexp " +([a-zA-Z0-9]+)")
let reg_truncate = ref (Str.regexp "[\t ]+")
let reg_diamond = ref (Str.regexp "^\\(.*?\\)◆.*$")
let reg_multi = ref (Str.regexp "^\\(.*?\\)●")

(* helper functions *)
let replace_newline = Str.global_replace !reg_newline ""
let replace_amp = Str.global_replace !reg_amp "&amp;"
let replace_gt = Str.global_replace !reg_gt "&gt;"
let replace_lt = Str.global_replace !reg_lt "&lt;"
let replace_rectangle = Str.replace_first !reg_rectangle ""
let split_colon = Str.split !reg_split
let delete_translation = Str.global_replace !reg_translation ""
let delete_ruby = Str.global_replace !reg_ruby ""
let space_truncate = Str.global_replace !reg_truncate " "
let delete_diamond = Str.global_replace !reg_diamond "\1"
let replace_multi = Str.replace_first !reg_multi "\1 / "
let has_multi s = Str.string_match !reg_multi s 0

(* convert eijiro format into sdic format per line *)
let convert_line line =
  let rec all_replace_multi content =
    if has_multi content then
      all_replace_multi (replace_multi content)
    else
      content
  in
  let struct_content head key content =
    if key = head then
      "<K>" ^ key ^ "</K>" ^ content
    else
      "<H>" ^ head ^ "</H>" ^ "<K>" ^ key ^ "</K>" ^ content
  in
  let line = replace_newline line in
  let line = replace_amp line in
  let line = replace_gt line in
  let line = replace_lt line in
  let line = replace_rectangle line in
  let splitted = split_colon line in
  let (head, content) = (List.nth splitted 0, List.nth splitted 1) in
  let key = String.copy head in
  let key = delete_translation key in
  let key = delete_ruby key in
  let key = space_truncate key in
  if !waei then
    let content = all_replace_multi content in
    let key = delete_diamond key in
    struct_content head key content
  else
    struct_content head key content

let convert_eijiro_to_sdic _ =
  let rec inner_convert_line _ =
    begin
      let unconvert_line = input_line stdin in
      let converted_line = convert_line unconvert_line in
      (* converted_line don't have newline *)
      print_endline converted_line;
      inner_convert_line ();
    end
  in
  try
    inner_convert_line ()
  with End_of_file -> ()

(* this tool only accept stdin and utf-8 string,
   and newline as Unix(\n).
*)
let _ =
  let argv = List.tl (Array.to_list Sys.argv) in
  if List.exists (fun s -> s = "--waei") argv then
    waei := true;
  convert_eijiro_to_sdic ()

・・・長すぎたので反省します。正規表現リテラルがある言語とそうでない言語の差だと思いますが。
ちなみに、これを154MBの英和テキストに対して、以下のようにして実行してみると、大体以下の時間で変換が終わりました。
nkfかましているのは、テキストファイルがおそらくShift-JISでOCamlUTF-8なので、それを合わせるためです。

% ocamlfind ocamlopt -package str -c eijiro2sdic.ml
% ocamlfind ocamlopt -package str -o eijiro2sdic eijiro2sdic.cmx
% time cat EIJI-135.TXT | nkf -w80 | eijiro2sdic > eijiro.sdic
cat EIJI-135.TXT  0.01s user 0.36s system 2% cpu 18.319 total
nkf -w80  16.89s user 0.11s system 92% cpu 18.328 total
eijiro2sdic > eijiro.sdic  12.00s user 3.70s system 85% cpu 18.335 total
% time cat EIJI-135.TXT | nkf -w80 | eijiro2sdic --waei > eijiro.sdic

ということで、30秒もかからずに変換できました。全部変換しても5分とかかりません。これは予想以上に変換が速かったのでいい意味で驚きました。いや、Rubyとかと比較していないからあれですが。
ちなみに一番サイズがでかいのは和英テキストで、大体230Mくらいあります。ただしこれは単体の話で、例示とかを含めると英和の方が大きくなります。今回は英和と和英のみ変換してます。

さて、これでsdic形式に変換することができましたが、他の方も書いていますが、サイズがサイズなので、とてもじゃありませんが普通に使うと遅いです。
ですので、suffix array形式に変換してやりましょう。

以前はsufaryというのを利用するようでしたが、これはどうやらすでに開発が止まっている?ようなので、これの後継であるsaryを使うことにします。Gentooにこっちの方しかパッケージなかったし。

saryへの変換については、OSSはアルミニウムの翼で飛ぶ Emacs 英辞郎 + sdic + sary の方で紹介されています。
saryの導入からの流れは以下の通りです。変換したものをそれぞれ eijiro.sdic, waeijiro.sdicとしてあるとします。

% ls -l
-rw-r--r-- 1 derui derui 161184928  95 08:04 EIJI-135.TXT
-rw-r--r-- 1 derui derui 117719636  96 16:45 REIJI135.TXT
-rw-r--r-- 1 derui derui   3824193  95 08:04 RYAKU135.TXT
-rw-r--r-- 1 derui derui 242712112  98 08:58 WAEI-135.TXT
-rw-r--r-- 1 derui derui 217177218  923 19:14 eijiro.sdic
-rw-r--r-- 1 derui derui 308097921  923 19:13 waeijiro.sdic
% sudo emerge sary
% mksary -c UTF-8 eijiro.sdic
% mksary -c UTF-8 waeijiro.sdic
% ls -l
-rw-r--r-- 1 derui derui 161184928  95 08:04 EIJI-135.TXT
-rw-r--r-- 1 derui derui 117719636  96 16:45 REIJI135.TXT
-rw-r--r-- 1 derui derui   3824193  95 08:04 RYAKU135.TXT
-rw-r--r-- 1 derui derui 242712112  98 08:58 WAEI-135.TXT
-rw-r--r-- 1 derui derui 217177218  923 19:14 eijiro.sdic
-rw-r--r-- 1 derui derui 508658844  923 19:18 eijiro.sdic.ary
-rw-r--r-- 1 derui derui 308097921  923 19:13 waeijiro.sdic
-rw-r--r-- 1 derui derui 800311576  923 19:25 waeijiro.sdic.ary
%

変換にはそれなりの時間がかかります。それと、見てわかるように、サイズがかなりの勢いで増えます。まぁ、今時のTB単位なら屁の河童と思います。ただまぁサイズ的にもそれ以外の意味でも、Dropboxで共有するとかはやめておいた方が無難でしょう。

なにはともあれ、これで無事に変換できましたので、Emacsから使えるようにしていきます。

といっても、すでに先人が達成されていることではありますので、先程のサイトを参考に以下のようなものをinit.elでもinit.dのファイルでもいいので追記します。
キー設定はあえて書きませんので、C-c wでも何でも使いやすいようにしてください。

;; ---------------------------------------------------
;; sdic
;; ---------------------------------------------------
(eval-after-load "sdic"
  '(progn
     (setq sdicf-array-command "/usr/bin/sary") ; コマンドパス
     (setq sdic-eiwa-dictionary-list
           '((sdicf-client "path/to/eijiro.sdic" (strategy array)))
           sdic-waei-dictionary-list
           '((sdicf-client "path/to/waeijiro.sdic" (strategy array))))

     ;; saryを直接使用できるように sdicf.el 内に定義されているarrayコマンド用関数を強制的に置換
     (fset 'sdicf-array-init 'sdicf-common-init)
     (fset 'sdicf-array-quit 'sdicf-common-quit)
     (fset 'sdicf-array-search
           (lambda (sdic pattern &optional case regexp)
             (sdicf-array-init sdic)
             (if regexp
                 (signal 'sdicf-invalid-method '(regexp))
               (save-excursion
                 (set-buffer (sdicf-get-buffer sdic))
                 (delete-region (point-min) (point-max))
                 (apply 'sdicf-call-process
                        sdicf-array-command
                        (sdicf-get-coding-system sdic)
                        nil t nil
                        (if case
                            (list "-i" pattern (sdicf-get-filename sdic))
                          (list pattern (sdicf-get-filename sdic))))
                 (goto-char (point-min))
                 (let (entries)
                   (while (not (eobp)) (sdicf-search-internal))
                   (nreverse entries))))))

     (defadvice sdic-forward-item (after sdic-forward-item-always-top activate)
       (recenter 0))
     (defadvice sdic-backward-item (after sdic-backward-item-always-top activate)
       (recenter 0))))

(setq sdic-default-coding-system 'utf-8-unix)

これで使えるようになります。saryを使っているおかげで検索速度は非常に速く、私の環境ではほとんど待ち時間はありませんでした。

一番時間がかかったのが、OCamlを書く時間だったというのは反省すべき点ですが、これで一々Firefox英辞郎を引かなくてよくなりました。結局メインが英辞郎の紹介よりもそっちの方がメインのような。まぁいいか。