はじめに
与えられたテキストのスペルチェックがしたい。
Ruby ではどうやるのがいいのか。
Hunspell
昔,そういうことを調べた人(私を含む)は Ispell とか GNU Aspell の名を知っているかもしれない。
これらの後継として,Hunspell が今は有力であるらしい。
Hunspell はブラウザーやメールソフト,ワープロ,組版ソフトなど,テキストを扱う多くのアプリケーションに組み込まれている。
また,コマンドラインツールもあり,単独で使うこともできる。
少なくとも百数十の言語の Hunspell 用辞書が配布されているようなので,対応言語の数もなかなかのものだ1。
Ruby では ffi-hunspell という gem があるので,本記事ではこれを使うことにする。
インストール
Hunspell のインストール
ffi-hunspell gem には Hunspell そのものは入っていないので,別途インストールする必要がある。
Hunspell は macOS でも Windows でも Linux でも簡単にインストールできる。
Hunspell のインストール方法はここでは網羅的に書けないが,たとえば macOS で Homebrew を使っているなら
brew install hunspell
でインストールできる。
辞書のインストール
Hunspell をインストールしただけでは辞書が付いてこないので,スペルチェックはできない。
スペルチェックしたい言語の辞書のファイルをインストールする必要がある。
いま「辞書のファイル」という言い方をしたが,どうやら
-
*.dic
辞書ファイル(dictionary file) -
*.aff
接辞ファイル(affix file)
の二つがペアになっていて,言語ごとに二つ揃えて用意する必要があるようだ。
「接辞ファイル」は私が仮に直訳してみただけで,正しい訳語かどうかは分からない。
例として,米国英語用辞書は
- en_US.dic
- en_US.aff
というファイル名になっている。
おそらくファイル名が被らなければ一つの言語にいくつでも辞書が置けるので,「ぼくがかんがえたさいきょうの米国英語辞書」を作って,my_awesome_en_us.dic, my_awesome_en_us.aff みたいなファイル名で使用・配布してもいいんだと思う(知らんけど)。
辞書の一覧
試しにコマンドラインで
hunspell -D
と打ってみよう。これは「今使える辞書の一覧」を表示するものだ。
結果は,まず冒頭で
SEARCH PATH:
として,辞書ファイルの検索パスがドバーッと表示されるはずだ。
逆に言えば,この検索パスに出てくるディレクトリーのどこかに辞書のファイルを入れれば認識してくれる,ということでもある。
次に
AVAILABLE DICTIONARIES (path is not mandatory for -d option):
などと表示されているはずだ。辞書が存在すれば,この行に続いて辞書の一覧が出るはずだ。
私の環境では,これに続いて,
Can't open affix or dictionary files for dictionary named "ja_JP".
が表示された。この行が言わんとすることは,
「ja_JP」の接辞ファイルも辞書ファイルも開けないんだけど?
というようなことだと思う(超訳)。
そんなファイルは存在しないので開けなくて当然。
「ja_JP」はたぶん環境のロケールを拾っているだけじゃないかな。ここではこれについて追及しない。
表示は以上だ。
辞書を探す
Hunspell 用の辞書は実にさまざまな場所で配布されている。
まずは,The Document Foundation のサイトの「Language/Support」のページ
を見てみよう。この団体はオフィススイートである LibreOffice を提供しているところ。
ちょっとページがゴチャゴチャしているが,表形式になっている箇所の左上に「Language」という検索窓がある。
ここに「english」と入力してみよう。
インクリメンタルサーチになっていて,「engl」くらいまで入力すれば英語だけに絞られる。
いろんなことが書かれているが,我々の目的はスペルチェック辞書なので,いちばん右の列の「Spell check dictionaries」のところを見よう。
ここに「English Dictionaries」というリンクがある。これをクリック。
すると以下のページに飛ぶ。
何も考えずに最新版をダウンロードするなら,右上の「Download latest」ボタンを押せばいいし,リリースの履歴を確認したければ「Release List」の表を見る。
「Release List」の表のいちばん右の列の「Download」はボタンにもリンクにも見えないが,これを押せばそのバージョンのファイルがダウンロードできる。
最新版ダウンロードファイルは,今やってみたら dict-en-20231001_lo.oxt という名前だった。
この .oxt
という拡張子は,LibreOffice やそのフォーク元である OpenOffice.org の拡張機能のファイル形式らしいがよくは知らない。
.oxt をバラす
このファイルは,ただの ZIP ファイルなので,拡張子を .oxt
から .zip
に変えてやると解凍できる。
解凍するとたくさんのファイルが出てくるが,この中で拡張子が .dic
のものと .aff
のものを探す。
ただし,hyph_en_GB.dic とか hyph_en_US.dic のように,「hyph_」で始まるものはスペルチェック辞書ではなくハイフネーション辞書なので,今回の目的には使わない。
dict-en-20231001_lo.oxt の場合,拡張子の前の部分が en_XX
という形のファイル計 10 個が見つかった。
XX
の部分は AU
, CA
, GB
, US
, ZA
で,それぞれオーストラリア,カナダ,イギリス,アメリカ,南アフリカであるようだ。
これらのうち,使いたいものの .dic
と .aff
を然るべきところ(後述)に置けばよい。
その他の辞書
英語の場合,実は特殊用途の辞書もある。もう一度 Language/Support のページの英語のエントリーを見てみよう。
「Spell check dictionaries」のほかに「Specialized spell check dictionaries」というものもある。
この一覧には医学用語や化学用語といったものも挙げられている。
専門分野の仕事をするならこういう辞書も役立つだろう(というか必須だろう)。
私自身は試していないので,これ以上は言及できない。
次に,フランス語の辞書も探してみよう。
「Language」の検索窓に英語で「french」と入力するか,フランス語で「français」と入力すればいい。
すると出てくるフランス語の行の右端の列の「Spell check dictionaries」のところに Dictionnaires Francais というリンクがあるので,これをクリック。
おいおい,フランス語で書いてあるのに「Français」じゃなくて「Francais」なのはなんでやねん。べつに ASCII 縛りとか無いと思うが。
こちらも英語でやったのと同じ要領で中身を見ると,辞書のファイルは以下の四点があった。
- fr-classique
- fr-moderne
- fr-reforme1990
- fr-toutesvariantes
それぞれの辞書の特徴は README_dict_fr.txt に書いてあるのだが,フランス語なので読めん。
ただ,Classique が 「recommandé」(推奨)となっているので,試すだけならとりあえず fr-classique でいいのだと思う。
その他の配布元
Hunspell 用辞書の配布元としては,ほかにも例えばメールクライアント Thunderbird のアドオン配布サイトがある:
ここで言語名と「dictionary」などで AND 検索してみると,スペルチェック辞書のアドオンが見つかる。
アドオンをダウンロードすると,拡張子 .xpi
のファイルになっているが,これもまた ZIP ファイルなので,拡張子を .zip
に変えて解凍し,中から *.dic
と *.aff
のファイルを取り出すことができる。
辞書の配置
辞書のファイルをどこに置くか。
すでに述べたように,hunspell -D
で出てくる「SEARCH PATH」のどこかに置けばよい。
たぶん
- 全ユーザーが使えるようにするか,特定のユーザー専用にするか
- 特定のアプリ用にするか否か
- Hunspell だけで使うか,同じフォーマットの辞書を利用する他のスペルチェックツールでも使えるようにするか
で違ってくるのだと思う。
私の場合(macOS),あまり何も考えずに /Library/Spelling
に置くことにした。
そういうディレクトリーは存在してなかったので作った。
置く場所を決めたらそこに *.dic
と *.aff
をペアで置くだけ。
もちろん,ディレクトリーやファイルの読み取りパーミションは適切に設定する必要がある。
辞書のファイルを配置したら再び
hunspell -D
してみよう。今度は
/Library/Spelling/en_US
みたいな感じで表示されるはずだ。
なお,これはファイルパスではない。これらに拡張子を付けたものがファイルパスになる。
ちなみに,macOS の持つスペルチェック機能はこの場所に置かれた辞書を認識するようだ。
たとえば,私は CotEditor というテキストエディターを使っているが,メニューの[編集]→[スペルと文法]で出てくるスペルチェック機能(これは OS の機能を呼び出しているらしい)で,新しく追加した辞書が言語の選択肢に出てきた。
ffi-hunspell のインストール
最後に gem のインストール。単に
gem install ffi-hunspell
で OK。
私の経験の範囲ではとくに困ったことは起きなかった。
試用
ではさっそく試してみよう。
en_US
の辞書は入っているとする。
require "ffi/hunspell"
dict = FFI::Hunspell.dict("en_US")
puts dict.check?("Ruby") # => true
puts dict.check?("Rubi") # => false
dict.close
コードを上から順に説明する。
gem 名は ffi-hunspell
なのだが,Bundler を使わずに require
する場合,ffi/hunspell
を指定することに注意。
Bundler を使う場合は,もちろん Gemfile に gem "ffi-hunspell"
と書く。
まず最初に FFI::Hunspell.dict
で辞書オブジェクトを生成する必要がある。
引数に辞書名を指定する。これは辞書のファイル名の拡張子を除いた部分だ。
辞書オブジェクトに対し,単語の文字列を引数にして check?
メソッドを呼び出すと,スペルが OK なら true
が,NG なら false
が返る。それだけ。
辞書オブジェクトは使い終わったら close
する必要がある。File
オブジェクトなんかと同じ。
たぶんスクリプトが終わる時に勝手に close
してくれるのだと思うが,それに任せるのはよい流儀ではないだろう。
実は File.open
なんかと同じように,ブロックを与える用法もある。以下のように書く。
require "ffi/hunspell"
FFI::Hunspell.dict("en_US") do |dict|
puts dict.check?("Ruby") # => true
puts dict.check?("Rubi") # => false
end
これだとブロックの評価が終わったところで自動的に close
してくれるはずだ。
こちらの書き方が良い。
なお,上記の dict
は FFI::Hunspell::Dictionary
というクラスのインスタンス。
機能
FFI::Hunspell::Dictionary の持つメソッドは,よく調べていないがたぶん以下の三つ。
-
check?
単語の綴りが合っているかを判定 -
stem
単語の語幹の候補を挙げる -
suggest
単語が誤っている場合に正しい語形の候補を挙げる
このうち check?
は既に見たので,残りの二つを確認しよう。
以下の例では,ローカル変数 dict
に en_US
の FFI::Hunspell::Dictionary オブジェクトが入っているとする。
stem
例えば,notes という単語は,語幹 note に複数形語尾 s が付いた形をしている。
stem
メソッドを使えば以下のように「notes」から「note」を得ることができる。
p dict.stem("notes") # => ["note"]
メソッド名の「stem」は日常語では「幹」という意味だが,文法用語では「語幹」のこと。
dictionaries という単語も dictionary に複数形語尾を付加したものだが,この場合,語尾は s でなく es だし,語幹の y は i に変化している。
このような単語についても,期待する結果が得られる:
p dict.stem("dictionaries") # => ["dictionary"]
素晴らしい!
名詞の変化だけではない。動詞 start の三人称単数現在形,現在進行形,過去形についてもやってみよう:
p dict.stem("starts") # => ["start"]
p dict.stem("starting") # => ["start"]
p dict.stem("started") # => ["start"]
すべて期待どおり!
(starts は名詞 start の複数形とも解釈できるが結果は同じ)
ちなみに,変化語尾が付いていない語で試すと,同じものが返る:
p dict.stem("make") # => ["make"]
ところで,ここまでの例を見て分かるとおり,返り値は文字列ではなく文字列の配列になっている。
これがなぜなのかは,以下の例を見れば分かる。
p dict.stem("making") # => ["making", "make"]
making という単語は,making という名詞とも解釈できるし,make という動詞の現在進行形とも解釈できるのだ。「語幹」の候補が一般には複数あるので,配列が返る仕様になっているというわけだ。
単語の辞書形が得られる?
ここまで試してみて,「おお! これで単語の辞書形(動詞なら原形,形容詞なら原級,など)が分かるぞ!」と期待したのだが,そういうわけではなかった。
以下の例を見て少々がっかりした:
p dict.stem("wrote") # => ["wrote"]
p dict.stem("better") # => ["better"]
wrote から write が得られるわけではないのだ。
better から good が得られるわけでもない。
wrote は write に語尾を付けたものではないし,better も同様2。
おそらく stem
メソッドはあくまで「語幹に語尾を付加して出来た語から元の語幹を得る」だけのものなのだろう。
dictionaries から dictionary が得られる例を見ると,語幹の若干の変化には対応していそうだが,以下の例はやはりがっかりする:
p dict.stem("stopping") # => ["stopping"]
p dict.stem("stopped") # => ["stopped"]
動詞 stop に -ing や -ed を付けるにあたって,語幹の最後の子音 p を重ねているだけなのだがこういうものには対応していないようだ。
このあたりは辞書によるのかもしれない。
本当に語幹なのか
英語以外の言語でいろいろ実験してみたところ,どうやら Hunspell でいうところの「語幹(stem)」は言語学的な語幹とは違うということが分かった。
エスペラントで「学生」は studento なのだが,「学生たちを」は studentojn となる。
-o は名詞の単数主格語尾で,-ojn は複数対格語尾だ。
語幹は student である。この student に -o や -ojn を付ければ上記の単語が出来上がる(語尾無しの student という単語は無い)。
ところが,エスペラントの辞書(eo-EO)で stem
を使ってみると
p dict.stem("studento") # => ["studento"]
p dict.stem("studentojn") # => ["studento"]
のように,語幹ではなく,単数主格形が出てきた。
単数主格形は名詞のいわば基本形であり,辞書にもこの形で載っている。
また別の実験では,接辞の付いた語の接辞が取れて別の語尾が付いた語が返ってきた。
というわけで,Hunspell のいうところの stem は,語幹だったり,(何らかの意味の)基本形だったり,接辞まで剥ぎ取ったりしたものだったり,さまざまだということが分かった。
単純に語幹と思ってはいけない。
suggest
suggest
は,誤った語形から正しい語形を推定して,その候補を挙げるメソッドだ。
候補はやはり複数ありうるので,返り値は文字列の配列になっている。
ほな,定番の誤り recieve(正しくは receive)行きまひょか:
p dict.suggest("recieve") # => ["receive", "relieve", "reverie"]
三つも候補が出てきた。
reverie はあんまり似てない気もするけど,どういう基準で似ていると判断しているのだろう?3
suggest
に正しい語形を与えたらどうなるのか?
p dict.suggest("receive")
# => ["receive", "receives", "receiver", "received", "deceive"]
空配列が返るわけではない。存在している単語であっても,他の語のスペルミスの可能性はあるわけだし。
さいごに
Ruby で Hunspell を使ったスペルチェックをやってみた。
かなりお手軽にできるということが分かった。
応用としては,たとえば
- Rails などウェブアプリで,ユーザー入力に対してスペルチェック
- テキスト処理プログラムに付加機能としてスペルチェックをプラス
- 静的ウェブサイトの全 HTML を一括でスペルチェック
といったことが考えられるだろうか。
しかし,そのためにはまだまだ考えなければならないことがある。
ffi-hunspell は(たぶん)単語単位でしかチェックができない。だからテキスト中から単語を切り出す処理が必要になるが,これが意外と難しいんである。
また,実用的には辞書に単語を追加して使いたいこともあるだろう。ffi-hunspell にはそういう機能もある(らしい)。
それから,いまシステムにどんな Hunspell 辞書が入っているかを Ruby 側から知りたいこともあるはずだ。これはどう書くのか。
もし本記事に反響があるなら,そういったことも続編として書いてみたい。