UECジャーナル

開眼☆シェルスクリプト 文章の表記揺れ/綴りをチェックする―コマンドを自作する時は単機能で

文章を扱うコマンドをシェルスクリプトで作成する。作るコマンドは語尾のチェックコマンドとスペルチェックのコマンドだ。いずれのコマンドも既存のコマンドをうまく組み合わせて短いものを作る。

今回の内容ではGancarzのUNIX哲学の中でも特に「各プログラムが1つのことをうまくやるようにせよ。全てのプログラムはフィルタとして振る舞うようにせよ」を意識しておいてほしい。

環境

ここでの作業環境はMac OS Xとし、GNU sed (gsed)とGNU awk (gawk)がインストールされているものとする。リスト1に環境を示す。

リスト1: 環境

$ uname -a
Darwin uedamac.local 12.4.0 Darwin Kernel Version 12.4.0: Wed May  1 17:57:12 PDT 2013; root:xnu-2050.24.15~1/RELEASE_X86_64 x86_64
$ gsed --version
gsed (GNU sed) 4.2.2
 (略) 
$ gawk --version
GNU Awk 4.1.0, API: 1.0
Copyright (C) 1989, 1991-2013 Free Software Foundation.
$ 

加工する文章はreStructuredTextという形式で記述されている原稿とする。reStructuredText形式のファイルの拡張子は.rstとなる。

リスト2: 原稿のファイル

$ ls *.rst
201201.rst        201210.rst        201306.rst
201202.rst        201211.rst        201306SPECIAL.rst
201203.rst        201212.rst        201307.rst
 (略) 
$ 

原稿にはリスト3のようにだいたい30字くらいで改行を入れてある。

リスト3: 原稿

$ tail -n 5 201302.rst 
lsとwcを使えば事足る。captiveでないので、なんとかなる。

 今回は正直言いまして、
かなりエクストリームなプログラミングになってしまったので、
次回からはもうちょっとマイルドな話題を扱いたいと思う。
$ 

作成したコマンドなどはディレクトリSD_GENKOUの下にbinというディレクトリを作ってそこに置く事にする。

敬体と常体のチェック

表記揺れの基本となる敬体(ですます調)と常体(である調)のチェックを行うシェルスクリプトを作る。

リスト4: 語尾を数えるコマンド

$ cat desumasu1 
#!/bin/sh

tmp=/tmp/$$

cat > $tmp-text

desu="(です。|ます。|でした。|ました。|でしょう。|ません。)"
da="(だ。|である。|ない。|か。)"

grep -E "$desu" $tmp-text       |
wc -l                           |
tr -d ' ' > $tmp-desu

grep -E "$da" $tmp-text         |
wc -l                           |
tr -d ' ' > $tmp-da

echo "ですます" $(cat $tmp-desu)
echo "だである" $(cat $tmp-da)

rm -f $tmp-*
exit 0
$ 

6行目で標準入力を$tmp-textに一度溜めている。8、9行目で正規表現を作る。自分の困らない範囲で列挙しておけばよいだろう。

リスト5: 語尾を抽出

$ cat ../*.rst |
gsed 's/....。/\n&\n/g' | grep 。|
gsed 's/。.*/。/' | sort -u
 (略) 
 (縦) 軸。
 (?) を。
) を作れ。
) を知る。
:私です。
	
$ 

実行するとリスト6にようになる。原稿は敬体で書かれているが1.5%程度常体で記載されている部分があるように見える。

リスト6: desumasu1 を使う

$ cat *.rst | ./bin/desumasu1
ですます 2252
だである 32
$ 

コマンドを書き直す

常体がちょっと混ざっているようなのだが、次はどこを修正しなければならないのか知りたくなってくる。ここでは次のように新しいコマンドを作る。

リスト7: desumasu2

$ cat ./bin/desumasu2 
#!/bin/sh

desu="です。|ます。|でしょう。|ません。"
da="だ。|である。|ない。|か。"

gawk '{print FILENAME ":" FNR ":" ,$0}' "$@"    |
gawk -v desu=$desu -v da=$da \
     '$0~desu{print "+",$0}$0~da{print "-",$0}'
$ 

7行目の"$@"はdesumasu2がもらったオプションをそのままgawkに渡すための方法だ。"$*"だと複数のファイル名がオプションに入っている場合にすべての引数が単一も文字列として扱われるためうまくいかない。たちえばリスト8の例では"201311.rst 201211.rst"が1つのファイル名だと解釈されるためcat がエラーを出す。

次に検索で引っ掛ける文字列は4、5行目でシェル変数として定義している。これを12行目でgawkに引き渡している。正規表現を変数に渡している。7行目のFNRは行番号が格納された変数。NR と違って読み込んだファイルごとの行番号が格納されている。この例のように「あるファイルの何行目」を出力するときに使用する。

リスト8: $* でうまくいかない場合

$ cat ./bin/fail_sample 
#!/bin/sh
cat "$*"
$ chmod +x ./bin/fail_sample 
$ ./bin/fail_sample 201311.rst 201211.rst
cat: 201311.rst 201211.rst: No such file or directory
$ 

実際に使う場合はdesumasu2の出力からgrep "^-" で常体の行を抜き出し目で検査することになる。もしこれで分からなければファイル名と行番号が書いてあるので当該のファイルを開いて前後の文脈を見ればよい。

$ cat *.rst | ./bin/desumasu2 | tail -n 3
+ -:13184:      //--dont-suggestを指定すると、候補が出てきません。
+ -:13195: エディタを開かなくてもどこに疑わしい単語があるかチェックできます。
+ -:13197: エディタから独立させておくと、思わぬところで助けられることがあります。
$ ./bin/deathmath2 *.rst | tail -n 3
+ 201311.rst:338:       //--dont-suggestを指定すると、候補が出てきません。
+ 201311.rst:349: エディタを開かなくてもどこに疑わしい単語があるかチェックできます。
+ 201311.rst:351: エディタから独立させておくと、思わぬところで助けられることがあります。
$ cat *.rst | ./bin/deathmath2 | awk '{print $1}' | sort | uniq -c
2268 +
  33 -
$ ./bin/deathmath2 *.rst | grep "^-" | head -n 3
- 201202.rst:562: 寒さに負けず端末を叩いておられますでしょうか。
- 201202.rst:565: ドア用の close コマンドがないものか。
- 201202.rst:607: * プログラマの時間は貴重である。(略)
$ 

リスト8のようにdesumasu2の出力をawk、sort、uniqで加工するとdesumasu1のような答えが得られる。このようにすることで1つのコマンドに1つの機能だけを持たせることができ、使い手がコマンドの機能を覚えるのが簡単、そしてほかのコマンドと組み合わせて処理をさせるといったことも効率よくできるようになる。

英単語をチェックする

次に英単語のスペルチェックを行うスクリプトを作る。スペルチェッカーは通常エディタから読み出して使うが、ここではコマンド仕立てにする。

まずスペルチェッカGNU Aspellをインストールする。MacだとHomebrewでリスト9のようにインストールできる。

リスト9: Aspell のインストール

$ brew install aspell
$ 

シェルスクリプトからAspellを使いたいので、対話形式ではなくフィルタとして使用する。-aを指定することでフィルタとして機能させることができる。

リスト10: man でオプションを調査

$ man aspell
 (略) 
pipe, -a
	Run Aspell in ispell -a compatibility mode.
$ 

試しに使ってみよう。リスト11のように環境変数LANGをCなどに設定してから動作させる。

リスト11: Aspell をフィルタモードで使う

$ echo "All your base are berong to us." | LANG=C aspell -a
@(#) International Ispell Version 3.1.20 (but really Aspell 0.60.6.1)
 (略) 
*
& berong 25 18: Bering, bronc, belong, Behring, bearing,  (略) 
//--dont-suggestを指定すると候補が出てこない。
$ echo "All your base are berong to us." | LANG=C aspell -a --dont-suggest
@(#) International Ispell Version 3.1.20 (but really Aspell 0.60.6.1)
 (略) 
*
# berong 18
 (略) 
$ 

まず補助的なコマンドとして、疑わしいスペルのリストを表示するコマンドをリスト12のようなスクリプトを作る。aspellはバッククォートなどの記号類にも反応する事があり、また、日本語が入ると何が起こるかわからないので4行目のgsedで単語に使う文字だけ残してあとは空白に変換している。

リスト12: 疑わしいスペル抽出コマンド

$ cat ./bin/henspell-list 
#!/bin/sh

gsed "s/[^a-zA-Z0-9']/ /g" "$@"	|
LANG=C aspell -a --dont-suggest	|
gawk '/^#/{print $2}'		|
sort -u	
$ 

リスト13のようにまともな単語も引っかかるが、これはAspellの辞書にこれらの単語を登録することで出なくなる。

リスト13: henspell-listを使う

$ ./bin/henspell-list 201311.rst 
FILENAME
FNR
(略)
berong
(略)
$ 

辞書ファイルにはいくつかの種類があるが、今回のケースではリスト14のように1行目に「personal_ws-1.1 en 0」と書いて、あとは引っかかった正しい単語をひたすら書いていくと作れる。

リスト14: Aspell の辞書ファイル

$ head -n 5 ./bin/dict 
personal_ws-1.1 en 0
FILENAME
FNR
GENKOU
Gancarz
 (略) 
$ 

これをhenspell-listに読み込ませるとよいということになる。

リスト15: henspell-listを改良して使う

$ cat ./bin/henspell-list 
#!/bin/sh

dict=$(dirname $0)/dict

gsed "s/[^a-zA-Z0-9']/ /g" "$@"			|
LANG=C aspell -p "$dict" -a --dont-suggest	|
gawk '/^#/{print $2}'				|
sort -u	
$ ./bin/henspell-list 201311.rst 
berong
da
desumeth
dirname
zA
$ 

次にこのコードを利用してもとの原稿のどこに変なスペルがありそうなのかを表示する。リスト16に作成したコマンドを示す。grepの-wオプションは単語の検索を行う。-nは出力に行番号を表示する指定、-f <FILE>で検索対象の文字列をFILEから読み込むとなっている。

リスト16: henspell

$ cat ./bin/henspell
#!/bin/sh

tmp=/tmp/$$

if [ "$#" -eq 0 ] ; then 
	cat 				|
	tee $tmp-stdin			|
	$(dirname $0)/henspell-list > $tmp-list
	grep -w -n -f $tmp-list < $tmp-stdin
else
	$(dirname $0)/henspell-list "$@" > $tmp-list
	grep -w -n -f $tmp-list "$@"
fi

rm $tmp-*
exit 0
$ 

利用するときはリスト17のようにページャで受けて本当にスペルミスがないか探す事になるだろう。

リスト17: henspellを使う

	
$ ./bin/henspell 201311.rst | less
14:Macには、GNU sed( ``gdes`` )がインストールされているものとします。
87:     da="(だ。|である。|ない。|か。)"
...
217:``deathmarch2`` がもらったオプションをそのまま ``awk``
...
$ 

おわりに

今回はシェルスクリプトで文章チェックのためのコマンドを作った。このようにシェルスクリプトでコマンドを作ることを覚えると、1日かけていた作業が数秒で終わるという幸運なことに何回か巡り会うことができる。シェルスクリプトでコマンドを作ると他のコマンドも呼び出せるから、この方法はオススメだ。

Software Design 2013年11月号 上田隆一著、「テキストデータならお手のもの 開眼シェルスクリプト 【23】文章の表記揺れ/綴りをチェックする―コマンドを自作する時は単機能で」より加筆修正後転載

last modified: 2014-03-17 21:03:17