octahedron

LemとSKKとCommon Lispでたたかうプログラマのブログ

Inquisitor --- external-formatラッパー & 文字コード/改行コード自動判定ライブラリ

Inquisitor

 Inquisitorとは、Common Lisp文字コード/改行コードの自動判定ライブラリなのである。

github.com

 これまでに文字コード判定を行うライブラリはあったが、 *1

  • 改行コード判定がない
  • さまざまな処理系(CCL、ECLなど)に対応してない
  • external-formatが処理系依存ってヒドい
  • Quicklispに載っていてほしい

というもの足りなさがあったため、勉強がてらつくってみることにした。 実際には、文字コード判定部はzqwell氏のものをそのまま流用したうえで

  • 各処理系対応
  • external-formatのラップ
  • 改行コード判定

を追加した。

 Quicklispへはまだ登録申請を行なっていない。CircleCIでbuild failedしたままだとカッコがつかないと考えているためで、ただし原因はよくわかっていない(どうしよう……)。

使用方法

 以下、特に記さない限り処理系はClozure CLとする*2

Case1: 文字コード/改行コードを知りたい

 関数inquisitor:detect-encodingにバイトストリームと言語圏(後述)を指定すると、文字コードを知ることができる。

(with-open-file (in "/path/to/utf8-lf.ja"
                 :direction :input
                 :element-type '(unsigned-byte 8))
  (inquisitor:detect-encoding in :jp))
=>:UTF-8

 改行コードについてはinquisitor:detect-end-of-lineにバイトストリームを渡せばいい。

(with-open-file (in "/path/to/utf8-lf.ja"
                 :direction :input
                 :element-type '(unsigned-byte 8))
  (inquisitor:detect-end-of-line in))
=>:LF

もし処理系が改行コードを扱えない場合、二つ目の返り値として:CANNOT-TREATが返ってくる。 これは文字コードについても同じで、その処理系で扱えない文字コードと判断されたら:CANNOT-TREATが返ってくる。

処理系が改行コードを扱えるかを知るにはinquisitor.eol:eol-available-pを呼びだせばいい。

inquisitor:detect-encodingに指定する言語圏について

 inquisitor:detect-encodingには、どの言語圏内で文字コード判定を行うか指定する必要がある。地域によって使われている書記体系はさまざまであって、それぞれに対応した文字コードもたくさんあり大変なので、書記体系を絞ってから判定しましょうということか。あるいは、符号化方式によっては異なる書記体系間で同じ符号があてがわれることがあるからか。なんにせよ、文字コード判定とは大変なものなのだなあと思った次第である。

 Inquisitorでは以下の11の言語圏を指定できる。

Case2: external-formatを(処理系に依存することなく)つくりたい

 Common Lispの仕様について各人思うところはあるだろうが、ぼくはひとつ、external-formatの仕様が処理系依存すぎるところが気にくわない。SBCLでは改行コードが扱えなかったりして、クロスプラットフォームを目指したコードでテキスト処理をするときに怖気がするのである(Windows向けSBCLLinuxで作成したUTF8-LFを食わせるとどうなるのだろう、とか)。

 そこでinquisitorでは、処理系に依存せずにexternal-formatをつくれるようにした。

(inquisitor:make-external-format
  (inquisitor.keyword:utf8-keyword)
  (inquisitor.keyword:lf-keyword))
=>:UTF-8 ; SBCLの場合
=>#<EXTERNAL-FORMAT :CP932/:DOS #xxxxxxxxxxx> ; CCLの場合

 そしてinquisitorでは、ベクタやストリームやpathnameを渡すだけでexternal-formatを返す関数を用意した。

ベクタの場合

(let ((str (encode-string-to-octets "公的な捜索係、調査官がいる。わたしは彼らが任務を遂行しているところを見た。")))
  (inquisitor:detect-external-format str :jp))
=>#<EXTERNAL-FORMAT :UTF-8/:UNIX #xxxxxxxxxx>

ストリームの場合

(with-open-file (in "/path/to/utf8-lf.ja"
                 :direction :input
                 :element-type '(unsigned-byte 8))
   (inquisitor:detect-external-format in :jp)
=>#<EXTERNAL-FORMAT :UTF-8/:UNIX #xxxxxxxxxx>

pathnameの場合

(inquisitor:detect-external-format #P"/path/to/utf8-lf.ja" :jp)
=>#<EXTERNAL-FORMAT :UTF-8/:UNIX #xxxxxxxxxx>

 これでテキストファイルを開くのがいくぶん楽になるはずである。こんなふうに。

(let* ((p #P"/path/to/textfile.ja")
       (ef (inquisitor:detect-external-format p :jp)))
  (with-open-file (in p :external-format ef)
    (read-line in)))
=>"一行目の文字列"

現状と今後

 とりあえず、各言語圏のテストデータを用意して、一応テストとしての体裁は整えることができた。テストで失敗している箇所がいくつかあってどう対処すべきかを考える必要がある。

  • UTF16のサロゲートペアを含むテストデータがなぜかUCS2(サロゲートペアは含まないはず)と判定される
  • 各言語圏の誤判定
    • 妥当な結果か否か=テストデータが悪い? or 文字コード判定部のバグ?
  • 文字コードの境界テストデータがない
    • 誤判定との関係(誤判定データには全ての文字コードに含まれる文字しか使われていない、など)

 各言語圏の詳しい人に見てもらうのが一番だと思うけど、どうしたものか。  あとは、冒頭でも書いたとおり、CircleCIに登録してバッジ出したはいいがbuild failedのままなので、quicklispに登録したものか悩ましい、などなど。

 どうしよう。


文章ふざけすぎかなあ。

*1:onjo氏のものzqwell氏による左記の多言語対応版

*2:external-formatで改行コードが扱えるので