##概要
Pythonのライブラリpyparsingを使うと、階層的な定義の箇条書きで、読み易く変更し易い文法を定義できる。if文など使わず記述出来る点に目を付け、pyparsingのクラスやメソッド、変数名に日本語を使って文法を書いてみた。同じ内容の英文記述に較べて、日本語によるコードは一目見た時に把握し易いと感じた。クラスやメソッドまで日本語にしたのはやりすぎかもしれないが、Pythonで日本語を使ってここまでできるとは思っていなかった。
Python3.7, pyparsing 2.4.6を使用。(Anacondaディストリビューション)
##記述と実行例
組織の3種類のメンバ名簿を想定して、それを読み込むパーサの文法を日本語で定義してみた。 Pythonの予約語以外の変数名(文法名、式名)、関数名、importしたクラス名や関数名、パース対象とも漢字を使っている。
コードは、上から下に向かって文をボトムアップに積み上げた記述になる。最後に記述された以下の文の右辺式がトップレベルの文法で、組織のメンバに3種類あることがわかる。
協会員 = 賛助会員 | 学生会員 | 個人会員
ボトムアップに、と書いたが、記述の際はこの最後の定義をまず決め、次いで部品に相当する下位の定義を書いたら、この最後の文から細部に分解していく方法をとった。
一番重要な3種類のメンバの分岐を行うために、名簿の各行の最初のトークンに一致する式を定義する。つまりパースする文字列の最初の一致が会社名か学校名かで分岐する。個人会員はそれ以外というわけだ。(以下にコードを抜き出し)
会社名 = 後方一致('会社')
賛助会員 = (会社名 + 代表者 + 会員番号)('賛助会員')
学校名 = 後方一致('大学') | 後方一致('高専') | 後方一致('大学校')
学生会員 = (学校名 + 姓名 + 姓名読み + 会員番号)('学生会員')
尚、〇✖株式会社のように、区切り無の連続した文字列を想定した会社名、学校名の一致は上記のように正規表現の後方一致で行っている。 当初、以下の記述を考えていたのだが先行する式である漢字列(Word)に、例えば '株式会社' まで食われてしまういわゆる過食(greedy)問題でうまくいかなかった。最長一致も試したがうまくいかない。pyparsingではトークンを左から消費していくためで前方一致では問題が起きない。
"""後方一致は過食問題を起こす"""
会社名 = 結合(漢字列 + oneOf('株式会社 合同会社 有限会社'))('会社名')
"""前方一致は過食問題を起こさない"""
会社名前方 = 結合('株式会社' + 漢字列)
正規表現を使って文の羅列という形式を保つために、後方一致のRegexを返す関数(lambda式)を設けている。後方一致関数には、'株式会社 合同会社' のように複数の文字列を渡せる定義とした方が効率は良いが、単一引数にして文法記述で複数記述する形態とした。
pyparsingで文法を定義する際に、先ほどの過食問題を避けるには、正規表現での回避方法の他に、入力の文字種を前後で変える、区切り文字を入れるなどで、消費を止めることが考えられる。対症療法では解決に無駄に時間をかけることになる。(実際2日間をああでもないこうでもないと無駄にした)
以下は全コードで、メイン部分でデータを与えてテストしており簡単な例外処理も入れている。但し、バックトラックによると思われるが、pe.locは正しく最初のエラー箇所を示してくれるとは限らないようだ。
文法記述に最初はとまどうが、いくつか書くとコツがわかってくる。イメージではパース対象の文字列が、文法記述したコードに注入され、どれかの式にマッチしたものだけがフィルタされて抜けてくる、というように捉えている。
#by T.Hayashi
#tested with Python3.7, pyparsing 2.4.6
#don't use full-width space as delimitter in this script.
from pyparsing import (
Combine as 結合,
Word as 列,
nums as 数字,
__version__ as 版数,
Regex ,
pyparsing_unicode as uni,
ParseException)
#以下日本語
def 文法を定義():
後方一致 = lambda s : Regex(r'.*'+s)
整数 = 列(数字)
漢字列 = 列(uni.Japanese.Kanji.alphas)
かな列 = 列(uni.Japanese.Hiragana.alphas)
会員番号 = 整数('会員番号')
姓名 = 漢字列('姓名')
姓名読み = かな列('姓名読み')
会社名前方 = 結合('株式会社' + 漢字列)
会社名 = 会社名前方 | 後方一致('会社')
代表者 = 漢字列('代表者')
賛助会員 = (会社名 + 代表者 + 会員番号)('賛助会員')
学校名 = 後方一致('大学') | 後方一致('高専') | 後方一致('大学校')
学生会員 = (学校名 + 姓名 + 姓名読み + 会員番号)('学生会員')
個人会員 = (姓名 + 姓名読み + 会員番号)('個人会員')
協会員 = 賛助会員 | 学生会員 | 個人会員
return 協会員
def テスト(gram,instr):
try:
r=gram.parseString(instr)
name=r.getName()
print(name,r.get(name))
print()
except ParseException as pe:
print(f'error at {pe.loc} of {instr}')
print(instr)
#loc : char position.
print(' '*(pe.loc-2)+'^')
#print('Explain:\n',ParseException.explain(pe))
print('pyparsing 版数:',版数)
文法=文法を定義()
テスト(文法,'山田太郎 やまだたろう 3456')
テスト(文法,'架空東大学 川崎三郎 かわさきさぶろう 5127')
テスト(文法,'株式会社架空商事 東太郎 0015') #前方一致
テスト(文法,'架空商事株式会社 海山太郎 0010') #後方一致
テスト(文法,'北北西高専 伊藤一郎 いとういちろう 900')
#エラーの確認 高校は定義に無い
テスト(文法,'北北東高校 鈴木三郎 すずきさぶろう 1000')
#エラーの確認 会社が抜け
テスト(文法,'株式架空商事 東太郎 0015')
#エラーの確認 読みに漢字
テスト(文法,'山田一太郎 やまだ一太郎 3456')
以下は実行結果。
pyparsing 版数: 2.4.6
個人会員 ['山田太郎', 'やまだたろう', '3456']
学生会員 ['架空東大学', '川崎三郎', 'かわさきさぶろう', '5127']
賛助会員 ['株式会社架空商事', '東太郎', '0015']
賛助会員 ['架空商事株式会社', '海山太郎', '0010']
学生会員 ['北北西高専', '伊藤一郎', 'いとういちろう', '900']
error at 6 of 北北東高校 鈴木三郎 すずきさぶろう 1000
北北東高校 鈴木三郎 すずきさぶろう 1000
^
error at 7 of 株式架空商事 東太郎 0015
株式架空商事 東太郎 0015
^
error at 9 of 山田一太郎 やまだ一太郎 3456
山田一太郎 やまだ一太郎 3456
^
##終わりに
BNF(Backus-Naur form)風の定義の羅列で、通常のプログラムに比べ理解し易い文法を定義できた。日本語を使ったら、いろいろ予期しないことが起きるのではと思っていたが、それもなく、Pythonでここまでできるとは意外だった。気を付けたのは、過食問題を持ち込まないようにすること、コード入力時見えない全角空白が入らないようにすることであった。
より大きな規模の文法を正しく動作するように定義、デバッグするのはトレースが難しいこともあり、書き方によっては通常のPythonのプログラムより簡単ではないかもしれない。このため、作っては例外や想定外動作を見て修正することを繰り返す小生の場合は、通常のプログラムに較べて、始めからできるだけ正しいコードを書くことを強く心掛ける必要がある、と思う次第..。
注:過食問題と呼んだが一般的な用語ではなくここで仮にそう名付けた。