textlintで日本語テキストの文字校正を試してみた

はじめに

textlintについては少し前から時々名前を聞くなと思っていましたが、自分に関わりがありそうなものとして意識したきっかけは、@t_wada さんによる以下のツイートだったと思います。

これを見た時点では、まあそういうこともあるだろうな、という程度の軽い感想でしたが、その後じわじわと「気になる」感じが増してきて、ようやく週末に少し時間を取れそうだったので、とりあえず1〜2日トライしてみたのが以下の話です。

先にぼくの背景について簡単に記しておくと、ぼくは3年前のちょうど今頃からプログラミングに入門し、最初はRubyJavaScriptを触っていたのだけど、同年8月頃にたまたまPerlを教えてくれる人たちに出会って、以後このブログに書いてきたようなことをしている非エンジニアの編集者です。

※その他の履歴についてはこちらをどうぞ。 → Profile

プログラミング歴3年とは言っても、普段の仕事ではとくにプログラミングを活用したり学習したりする機会もなく、ようやくチョット使えるようになってきたのもPerlVimぐらいで、今回のテーマであるtextlintまわりで使われているJavaScriptなどの知識はほとんど無いので、いろいろ勘違いしているところもあるかと思いますが、その際にはコメント欄やTwitterにて適宜ご指摘ください。

導入

さて、上記のような興味とともに最初にチェックしたのは、textlint作者である @azu さんの以下のブログ記事でした。
efcl.info

レポジトリにも導入のチュートリアルがあるので、どちらを見ながらスタートするか少し迷いましたが、
github.com

日本語で書かれている前者の記事をぼんやり目で追いながら、順にコマンドを打ち込んでいったらいつの間にか最初の環境づくりはほぼ完了していました。

※具体的には、textlint本体とmax-ten, spellcheck-tech-word, no-mix-dearu-desumasuというルール群をインストールして、試しに動かしてみるまで。

ちなみに、とりあえずtextlintを使ってどのような校正が成されるのか体験してみたい、という目的であれば、その記事でも紹介されているように Chrome拡張を使うという選択肢もあります。

ぼくの場合は、以前にVimJSONシンタックスチェックを設定した際、少しだけnpmを触ったことがあって、

note103.hateblo.jp

かつ、このときにはけっこうハマったので、その復習もかねてコマンドラインからのインストール&実行を試すことにしました。

一瞬話がそれますが、僕がこういうことをやる一番の目的は、「文字校正さえできればいい」といった最終形を求めてのことではなく、それができるまでの過程を体験しながら、それが置かれているシステムの裏側(仕組み)を知りたいということ、またそれを身につけて自分一人でも手元でイチから再現できるようにしたい、といった点にあるようで、それがこういうトライの原動力になっているようにも思えます。

textlintrcを設置

本題に戻ると、上記の導入記事で紹介されている最初のルール群を入れた後は、そのまま案内に沿って textlintrc というvimrc的なものを作りました。

書式はJSONYAML、JSモジュール(って何?)のいずれでも良いようですが、ここは素直に @azu さんが例示していた以下の形で。

{
  "rules": {
    "max-ten": {
      "max": 3
    },
    "spellcheck-tech-word": true,
    "no-mix-dearu-desumasu": true
  }
}

これはJSON形式だと思いますが(いちいち自信がない)、JavaScriptで使うようなコメントアウトが使用可能なので後々大変助かりました。コメントアウト便利。

で、これを書いたら「.textlintrc」としてホームディレクトリに設置します。
といっても、実際にはvimrcやbashrcと同様、ファイル本体はドットファイルを集めるディレクトリに入れて、そこからシンボリックリンクでホームディレクトリへリンクしています。

$ ln -s /path/to/dotfiles/textlintrc ~/.textlintrc

このtextlintrcを設定して何が嬉しいのかというと、たとえばこれを設定せずに上記のルールを使おうとしたら、

$ textlint --rule no-mix-dearu-desumasu --rule max-ten --rule spellcheck-tech-word README.md

というふうに、けっこう壮大な引数をタイプする必要がありますが、textlintrcを設定しておけば、

$ textlint README.md

だけで済みます。(上記二つのサンプルコードは @azu さんのブログより)

これは使わない手はないですね。

感覚的には、vimrcに各種Vimプラグインやそれぞれの設定を記述していくことに近いと感じました。

最初のつまずき

環境設定はそんなところで、ここからは実際のテキストを使ってツールを動かしていきます。

サンプルテキストには、ぼくらの夏目漱石&ぼくらの青空文庫から「草枕」をお借りしました。

山路やまみちを登りながら、こう考えた。
 智ちに働けば角かどが立つ。情じょうに棹さおさせば流される。意地を通とおせば窮屈きゅうくつだ。とかくに人の世は住みにくい。
 住みにくさが高こうじると、安い所へ引き越したくなる。どこへ越しても住みにくいと悟さとった時、詩が生れて、画えが出来る。
(略)

時間の都合上、ブラウザでそのままコピペしただけなのでルビもそのまま残っていますが、サンプルなのでご容赦ください。

全文だとさすがに長すぎるので、冒頭の一部を「kusamakura.txt」というファイルに保存して、さっそく実行。

f:id:note103:20160612202105g:plain

ん……? なんとなく良いようにも見えるけど、最後のほうがなんか変?

これってそもそもこういう表示をするツールだということなのか、それともまだ鋭意開発中のものだから環境によってはちゃんと動かない、ということなのか……? といろいろ考えてしまいました。
これが今回最初のつまずき。

これに対しては、その後「とりあえずlessで見てみるか……」と思いついたのが幸いして。

f:id:note103:20160612202146g:plain

今度は最後まで表示されました。さっきのはやはり途中で切れていたようです。

見たところ、漱石先生は一文あたりの読点がけっこう多いようですね……。

prh を使ってみる | 2度目のつまずき

基本的な動き方は確認できました。ここからは、これをどう応用できるのか、どんなバリエーションを付けていけるのか、といったことを考えていくわけですが、このツールではあまりにもいろんなことが出来そうで、また実際「こんなこともできるよ!」という説明も膨大にあるようで、もはやワクワクするのを通り越してウツになりそう、できれば最小限のことだけをとりあえずできるようになりたい……などと思いつつ、それでもとりあえずこれだけはやってみたい! と思ったのがこちら。

efcl.info

ここで紹介されている @vvakameさんは技術(書)界隈では有名な方で、ぼくも以前にRe:VIEWの開発者カンファレンスに参加した際に発表を拝見しましたが、

※参考: 書籍執筆支援システム「ReVIEW」に触ってみた話(&リンク集) - the code to rock

このtextlint-rule-prhというのは、その@vvakameさんによるproofread-helper(以下「prh」)というツールをtextlintから使うというもの。
github.com

ということで、さっそく案内にあるとおりにあれこれ設定して実行してみます。

f:id:note103:20160612035210g:plain

おぉっと、盛大なエラー……。
メッセージをコピペすると、こんな感じ。

Unhandled rejection Error: Error while loading rule 'prh': ENOENT: no such file or directory, open '/Users/kadomatsuhiroaki/sample/textlint/~/prh.yml'
    at Error (native)
    at Object.fs.openSync (fs.js:634:18)
    at Object.fs.readFileSync (fs.js:502:33)
    at Object.fromYAMLFilePath (/usr/local/lib/node_modules/textlint-rule-prh/node_modules/prh/lib/index.js:9:22)
    at createPrhEngine (/usr/local/lib/node_modules/textlint-rule-prh/lib/prh-rule.js:30:35)
    at reporter (/usr/local/lib/node_modules/textlint-rule-prh/lib/prh-rule.js:79:26)
    (略)

本当はもう少し長いエラーですが、注目すべきはたぶん1行目のこれで。

no such file or directory, open '/Users/kadomatsuhiroaki/sample/textlint/~/prh.yml'

「/Users/kadomatsuhiroaki」というのはぼくのMacのホームディレクトリ。そして「/sample/textlint」というのは今作業をしているディレクトリです。

そのつなぎ目がなんか変ですね。

/textlint/~/prh.yml

これは何かというと、おそらくtextlint-rule-prhの説明を見ながら書いた、textlintrc内の以下の記述が影響しているようです。

    "prh": {
      "rulePaths": [
        "~/prh.yml"
      ]
    },

ぼくとしては、「~/prh.yml」と書くことによって、「ホームディレクトリ直下にprh.ymlというファイルを置いたよ」とコンピューターに指示したつもりでしたが、上のエラーから察するに、そういう意味では受け取られておらず、コードを実行しているカレントディレクトリからの相対パスとして「~/prh.yml」が受け取られているようです。

しかしこの仕様だと、ファイルを実行するディレクトリごとに異なるprh.ymlを設置することになるので、本当にそうなのかなあ? としばらくハマりました。(使い方や理解の仕方が間違っているのかなと)

僕の感覚では、こういう多種多様なファイルを処理するツールの場合、作業場所もコロコロ変わると思うので、そのつどこういう必須データを生成するという発想はなかったのですが、ただあらためて考えてみると、校正ルールというのは文書の内容や方針によって変わるほうが自然とも言えて、毎回まったく同じというのも確かに不便なのかもしれません。

ということで、これについてはtextlintrcを以下のように直し、

    "prh": {
      "rulePaths": [
        "./prh.yml"  //<= 「~」を「.」に
      ]
    },

基本的にはつねに作業するディレクトリにprh.ymlを生成・設置することにしました。(生成方法については後出のtx.shをご参照)

追記: 絶対パスにも対応して頂きました

上記の「~/prh.yml と記述してもホームディレクトリだと認識してもらえない」という件に関して、作者の @azu さんが早々に対応してくださいました。

textlint-rule-prhの v3.1.0以降とのこと。

これにより、自分は基本的にprhの辞書はいつも同じでよい、という場合は絶対パスで以下のようにしておいて、

    "prh": {
      "rulePaths": [
        "~/path/to/prh.yml"
      ]
    },

プロジェクトごとに辞書を管理したい場合はこれまで通り、たとえば以下のようにしておけば大丈夫です。

    "prh": {
      "rulePaths": [
        "./prh.yml"
      ]
    },

なお、後出のコード「tx.sh」では、後者の設定を前提としていますのでご留意ください。

@azu さん、ありがとうございました!!

(追記ここまで)

prh を使ってみる(2) | 辞書を選ぶ

さて、そのprhですが、初期状態では以下のような辞書が用意されています。

prh.yml
review-ignore.yml
target.yml
techbooster.yml
WEB+DB_PRESS.yml

といっても、このすべてが同等に作られているということではなくて、とくに実用的なのは以下の二つのようです。

techbooster.yml
WEB+DB_PRESS.yml

前者の「techbooster」というのは、Androidを初めとする技術系のサークル「TechBooster(テックブースター)」の方針に沿って作られた辞書とのこと。

同サークルの名前は電子書籍の分野でもよく聞きますが、@vvakameさんとも関わりが深いようで、prh的にもこの辞書が一押しだそうです。

WEB+DB PRESS」は言わずと知れた技術誌の雄ですが、同誌編集者の稲尾さんが公開された用語統一ルールなどをもとに作られているようです。

複数の辞書をマージして使うこともできるようですが、素直なぼくとしては(そしてYAML力のないぼくとしては)、ひとまず一押しの「techbooster.yml」を使ってみます。

先の作業ディレクトリに prh.yml という名前でコピーして*1、実行。

$ textlint kusamakura.txt | less

結果。

/Users/kadomatsuhiroaki/sample/textlint/kusamakura.txt
3:57 ✓ error 出来る => できる prh
5:4 ✓ error 事 => こと prh
5:77 ✓ error 出来て => できて prh
6:293 ✓ error 故に => ゆえに prh
6:498 ✓ error ―― => ── spellcheck-tech-word
7:89 ✓ error ―― => ── spellcheck-tech-word
9:141 ✓ error 一つ => ひとつ prh
(略)

ふむ。いい感じにprh名義のエラー(というか指摘)が出てきましたね。だんだん使い方がわかってきた気がします。

結果をテキストファイルに書き出す

ここまでの作業を一旦ふり返ると、まずチェックしたいファイルと、準拠したいルールを用意して、そのチェックを通した「結果」を受け取る、ということまで出来ました。

しかしながら、この段階ではまだ毎回ターミナル上のlessを目視しては「ふむふむ」と確認しているだけなので、あまり実用的ではないですね。

ということで、次はこの出力されたエラーをテキストファイルに書き出してみようと考えました。

ただ、ちょっと冷静に考えてみると、上のような出力結果を受け取ったところで、結局それをまた元の文書とひとつひとつ突き合わせて、どこがどう間違っているのか確認していくという修行のような作業が待っているわけで、それはそれで大変そうです。

一方、textlintのオプション機能として、出力フォーマットを指定することができて、たとえば以下のようにすると……

$ textlint -f pretty-error kusamakura.txt | less

こんな感じに出てきます。

f:id:note103:20160612025026g:plain

ええと、例が悪くて何がいいんだかわかりづらいですが、行の中のどの辺がエラーなのか、というのが(本当は)見やすくなっています。

じゃあ、このフォーマットでテキストファイルに書き出してみようと、素朴なぼくとしては以下のコマンドを叩くわけですが、

$ textlint -f pretty-error kusamakura.txt > pretty-error.txt

こんな感じで出てきます。

[4m/Users/kadomatsuhiroaki/sample/textlint/kusamakura.txt[24m
[90m...[39m
[31m-  住みにくさが高こうじると、安い所へ引き越したくなる。どこへ越しても住みにくいと悟さとった時、詩が生れて、画えが出来る。
[39m[32m+  住みにくさが高こうじると、安い所へ引き越したくなる。どこへ越しても住みにくいと悟さとった時、詩が生れて、画えができる。
[39m[90m 人の世を作ったものは神でもなければ鬼でもない。やはり向う三軒両隣りょうどなりにちらちらするただの人である。ただの人が作った人の世が住みにくいからとて、越す国はあるまい。(略)

おぉぅ……。「[39m[32m」とか「[39m[31m」とかいう文字化け的な何かが出てきました。

ということで、これについてはPerl正規表現を使って整形していくことにしましょう。

追記: [39m[32m などを消す方法

こちらについても、その後 @azu さんから対応方法を教えて頂きました。

なるほど……じつは本記事には書かなかったのですが、この表示についてはけっこう試行錯誤しまして。一生懸命 less の設定をいじったりしていたのですが、まったく直らず諦めたのでした。
textlint コマンドのほうにこういったオプションを付ければ良かったのですね……。

ANSI escape code というものもこの機会に知ることができて大変勉強になりました。
これまたいろいろ検索したのですが、まったく引っかからなかったので……。

上記の方法を併用すれば、以下に出てくるコードもよりシンプルに書けそうですが、記事としてはこのオプションを知らない前提で話を進めていきますので、その旨ご留意ください。

あらためまして @azu さん、ありがとうございました!!

(追記ここまで)

コードを書く

あらためて、この段階で実現したいことを整理すると、

  1. ターミナルでコマンド+ファイル名を打ち込むと、
  2. 通常の結果と、pretty-errorフォーマットで出力された結果がそれぞれテキストファイルに書き出される

という状態を求めています。

そして、それを実現するために書いたシェルスクリプトPerlスクリプトが以下です。

tx.sh
#!/bin/sh

if [ ! -f "$1" ] ; then  # 引数で指定したファイルがカレントディレクトリになければ実行しない
    echo "No $1."
else
    if [ ! -f "prh.yml" ] ; then  # カレントディレクトリにprh.ymlがなければ元の置場からコピーしてくる
        cp /path/to/techbooster.yml ./prh.yml;
    fi

    textlint "$1" > textlint_error.txt  # 通常のtextlintをして結果をtextlint_error.txtに書き出し
    cat textlint_error.txt  # 結果をターミナルにも出力

    textlint -f pretty-error "$1" > textlint_pretty_error.txt  # pretty-errorフォーマットで出力し、一時ファイルに書き出し
    perl /path/to/textlint_pretty_error_tidy.pl textlint_pretty_error.txt > textlint_pretty_error_tidied.txt  # 後出のPerlスクリプトで整形し別ファイルへ書き出し
    rm -f textlint_pretty_error.txt  # 一時ファイルを削除
fi
textlint_pretty_error_tidy.pl
#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';
use File::Slurp;

my $data = read_file($ARGV[0]);  # 一時ファイルを読み込み
my @data = split/\n/, $data;

# 不要な文字列をカット
for (@data) {
    chomp $_;
    $_ =~ s/\\[90m//g;
    $_ =~ s/\\[31m//g;
    $_ =~ s/\\[0m//g;
    $_ =~ s/.*?([^\/]+\.[^\/]+)$/$1/g;  # ファイル名が絶対パスで出てくるのでbasenameのみにする
}

say for @data;

そしてすかさず、上記のシェルスクリプトを「tx」というコマンドで、マシンのどこからでも使えるようにしてみます。

$ chmod a+x tx.sh
$ ln -s /path/to/tx.sh /usr/local/bin/tx

シェルスクリプトでは引数から対象ファイルを受け取るようにしているので、以下のように入力すれば、

$ tx kusamakura.txt

あとはシェルスクリプトPerlと結託してあれこれやった挙句、作業ディレクトリ内に以下のファイルを生成してくれます。

textlint_error.txt
textlint_pretty_error_tidied.txt

Vimから使えるようにする

長くなりましたが、この勢いでもうひと山のぼります。

目当てのファイルを生成するところまで来ましたが、これでも今ひとつ不便です。
というのも、この方法だとtextlintを実行するたびにターミナルに行く必要がありますし、実行後には書き出したファイルをわざわざ開く手間が生じるからです。

では逆に考えろ、ということで、どうなったら満足なのか想像力を働かせてみると、

  1. Vimで文書を編集しているときにコマンド(マッピング)を叩くと、
  2. 上記の2ファイル(エラー結果を書き出したもの)が生成されて、
  3. 元バッファに加えて計3つのバッファが分割画面で表示される。

みたいな状況が良さそうなので、上のシェルスクリプトを活用して以下の関数をvimrcに書いてみました。

function! s:TextLintErrors()
  execute ":! tx %"
  execute ":sp textlint_error.txt"
  execute ":sp textlint_pretty_error_tidied.txt"
endfunction
nnoremap <silent> <Leader>tl :<C-u>call <SID>TextLintErrors()<CR><CR>

実行。

f:id:note103:20160612171920g:plain

できました。

動画だけだとわかりづらいので簡単に説明しますと、

  1. 最初に1画面まるまる「草枕」の状態
  2. 次に実行中の状況が流れていく様子(カクカク動きながら例の文字化けテキストも出てくる)
  3. 最後に画面が3分割された状態(下2つの出力結果をざっとスクロールしながら覗いていく)

という順に進んでいます。

展望とまとめ

ということで、かなり駆け足&欲張り&行き当たりばったりでしたが、自分なりにtextlintに触れた経過を書いてみました。

実質ほぼ1日半ぐらいの間に試したことなので、見落としや根本的な勘違いなどもあるかもしれませんし、実用性の面では、初めのほうで触れたブラウザ拡張などを用いたほうがずっと便利かもしれません。
io-monad.hatenablog.com

それでも、この機会にいろいろと学べたことは多く(とくにほとんど忘れていたシェルスクリプトの書き方とか)、個人的には良い勉強になりました。

今後の目標としては、上で紹介した設定やルールの使い方ではまだ不足が多いので、textlintには他にどんな機能があるのか、そしてそれをどう利用していけるのか、そのチューニングの方法なども含めてもう少し研究してみたいと思っています。

ちなみに、以下を見るとすでに少なからぬルールが揃いつつあるようなので、

この辺の状況も、自分なりに整理・把握していきたいところです。

ざっと触ってみた感触として、textlintは非常にプラガブルな思想にもとづくツールで、使い手が自由にカスタマイズしながら使えることが大きな魅力であると感じました。

こういうツールに対しては、「自然言語ってのはコードの正誤ほど簡単に白黒つけられないんだよな〜」といった否定的な見解が避けがたく出てくるものだと思いますが、そうであるからこそ、「唯一の正解」を設けない、誰もがそれぞれの用途に応じて一から設定していけるこのあり方には共感します。

職業としてこういう道具を必要とする人々(執筆家・編集者・校正者など)に届くまではもうしばらく時間が必要かもしれませんが、様々な専門領域の文書作成者たちが、それぞれのルールを一定の方式に沿って共有空間へ提供し、誰もがそれを使い、また自分なりにカスタマイズし……などという未来がくれば、それは大変喜ばしいことだと思います。

ぼくがその中でどのような役割を果たせるかはわかりませんが、そのような希望を抱いた数日でした。

*1:textlintrcのほうのファイル(辞書)名を書き換えてもいいと思います。