pyparsingで独自スクリプトを読み込んで、構文にあわせた処理をする
この記事はMac OS X High SierraにインストールしたPython 2.7とpyparsing 2.2.0を使っています。
概要
例題として、2種類の命令variable
とdump
からなる独自言語(DSLとかミニ言語とかスクリプトとか)を考えます。それぞれ次のような構文だとします。
variable name style args
dump id group-id style timestep file args
このvariable
とdump
は命令コマンドで、その後に続く引数はそれぞれあるルールをもつ(例えばnameは文字、styleはあらかじめ定義されたキーワードのどれか、idとtimestepは数値、argsは1つ以上の配列)とします。
このルールに従って記述された独自言語コードを読み込んで、構文が正しいかチェックするとともに、もし正しい構文が読み込まれた場合、それぞれの命令コマンドvariable
とdump
に対して、別々の処理を行うパーサを作ります。
pyparsingを使います。ifは使わないでやります。複数の命令についてif分岐をしないで処理するための考え方は
、http://www.ptmcg.com/geo/python/confs/pyCon2006_pres2.html に書いてあります。
コードと解説
コードと出力結果と説明を順番にしていきます。
pythonコード
import pyparsing as pp
import sys
import io
from termcolor import colored, cprint
data = '''\
variable INT equal 100
variable DEC equal 10.0
variable EXP equal 100e5
variable ARY index 1 2 3
dump 1 group atom 1000 output arg1 arg2 arg3
variable ERR equal hoge
'''
class Command(object):
# common parser elements
number = pp.Regex(r"[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?")
def __init__(self, *args):
self._action(*args)
def _action(self, *args):
cprint ('Do actions in the superclass of {1} with args: {0}'.format(str(args), type(self).__name__), 'green', attrs=['bold'])
@staticmethod
def syntax():
pass
class VariableCommand(Command):
def __init__(self, quals):
super(VariableCommand,self).__init__(quals[1], quals[2], quals[3:])
def _action(self, name, style, args):
super(VariableCommand,self)._action(name, style, args)
cprint ('Do actions in {1} with args: {0}'.format(str(args), type(self).__name__), 'green')
return
@staticmethod
def syntax():
# parsers for command matching
cmd = pp.Keyword("variable").setResultsName("cmd")
name = pp.Word(pp.printables).setResultsName("name")
style = pp.oneOf("equal index").setResultsName("style")
args = pp.OneOrMore(VariableCommand.number).setResultsName("args")
return (cmd + name + style + args)
class DumpCommand(Command):
def __init__(self, quals):
super(DumpCommand,self).__init__(quals[1], quals[2], quals[3], quals[4], quals[5], quals[6:])
def _action(self, id, groupid, style, timestep, file, args):
super(DumpCommand,self)._action(args)
cprint ('Do actions in {1} with args: {0}'.format(str(args), type(self).__name__), 'green')
return
@staticmethod
def syntax():
# parsers for command matching
cmd = pp.Keyword("dump").setResultsName("cmd")
id = (DumpCommand.number).setResultsName("ID")
groupid = pp.Word(pp.printables).setResultsName("group-ID")
style = pp.oneOf("atom atom/vtk image local custom mesh/stl mesh/vtk").setResultsName("style")
timestep = (DumpCommand.number).setResultsName("timestep")
file = pp.Word(pp.printables).setResultsName("file")
args = pp.OneOrMore(pp.Word(pp.printables)).setResultsName("args")
return (cmd + id + groupid + style + timestep + file + args)
def makeCommandParseAction(cls):
def cmdParseAction(original_string, location, tokens):
cls(tokens)
return tokens
#$return cls(t)
return cmdParseAction
def setCommandSyntax():
variableCommand = VariableCommand.syntax()
variableCommand.setParseAction(makeCommandParseAction(VariableCommand))
dumpCommand = DumpCommand.syntax()
dumpCommand.setParseAction(makeCommandParseAction(DumpCommand))
return (dumpCommand | variableCommand)
'''
main
'''
syntax = setCommandSyntax()
for i, line in enumerate(data.splitlines(True)):
line_no_comment = line.partition('#')[0].rstrip()
print "Parsing line " + str(i) + ": " + repr(line_no_comment)
try:
result = syntax.parseString(line_no_comment)
cprint(result.items(),'cyan')
except pp.ParseException as e:
cprint("matching failed", 'red')
#cprint("no match with error: " + str(e), 'red')
sys.exit()
出力結果
Parsing line 0: 'variable INT equal 100'
Do actions in the superclass of VariableCommand with args: ('INT', 'equal', ['100'])
Do actions in VariableCommand with args: ['100']
[('style', 'equal'), ('cmd', 'variable'), ('name', 'INT'), ('args', (['100'], {}))]
Parsing line 1: 'variable DEC equal 10.0'
Do actions in the superclass of VariableCommand with args: ('DEC', 'equal', ['10.0'])
Do actions in VariableCommand with args: ['10.0']
[('style', 'equal'), ('cmd', 'variable'), ('name', 'DEC'), ('args', (['10.0'], {}))]
Parsing line 2: 'variable EXP equal 100e5'
Do actions in the superclass of VariableCommand with args: ('EXP', 'equal', ['100e5'])
Do actions in VariableCommand with args: ['100e5']
[('style', 'equal'), ('cmd', 'variable'), ('name', 'EXP'), ('args', (['100e5'], {}))]
Parsing line 3: 'variable ARY index 1 2 3'
Do actions in the superclass of VariableCommand with args: ('ARY', 'index', ['1', '2', '3'])
Do actions in VariableCommand with args: ['1', '2', '3']
[('style', 'index'), ('cmd', 'variable'), ('name', 'ARY'), ('args', (['1', '2', '3'], {}))]
Parsing line 4: 'dump 1 group atom 1000 output arg1 arg2 arg3'
Do actions in the superclass of DumpCommand with args: (['arg1', 'arg2', 'arg3'],)
Do actions in DumpCommand with args: ['arg1', 'arg2', 'arg3']
[('style', 'atom'), ('args', (['arg1', 'arg2', 'arg3'], {})), ('group-ID', 'group'), ('cmd', 'dump'), ('N', '1000'), ('file', 'output'), ('ID', '1')]
Parsing line 5: 'variable ERR equal hoge'
matching failed
解説
まず、Command
クラスを親にもつVariableCommand
クラスとDumpCommand
クラスを作ります。この中に構文の定義を返すsyntax()
とコードと、マッチした時に実行させる_action()
というメソッドを作っておきます。
関数setCommandSyntax()
のなかの
variableCommand = VariableCommand.syntax()
でsyntax()
をクラスメソッドとして呼び出して、構文解析器であるpyparsingオブジェクトを作ります。この構文にマッチした時に実行したいことはpyparsingのsetParseAction
を使って決めていきます。このソースでは、
variableCommand.setParseAction(makeCommandParseAction(VariableCommand))
としています。つまり、variableCommand=VariableCommand.syntax()
の構文にマッチした場合、makeCommandParseAction(VariableCommand))
を実行せよ、ということです。setParseAction
のmanualを読むと
Parse action fn is a callable method with 0-3 arguments, called as C{fn(s,loc,toks)},
C{fn(loc,toks)}, C{fn(toks)}, or just C{fn()}, where:
- s = the original string being parsed (see note below)
- loc = the location of the matching substring
- toks = a list of the matched tokens, packaged as a C{L{ParseResults}} object
と書いてあるように、3つ以下の引数?を処理の中で渡してあげることができます。
makeCommandParseAction
にまずVariableCommand
が渡されると、ネストされたメソッドであるcmdParseAction
が返されます。cmdParseAction
は3つの引数をとるメソッドになっているので、setParseAction
のs
, loc
, toks
がそれぞれ渡されます。cmdParseAction
では、まずcls
に入ったVariableCommand
がtokens
で初期化されますので、__init__
にある_action()
が実行されます。
つまり、VariableCommand.syntax()
の構文にマッチしたときに実行しておきたいことを_action()
のなかに書いておけばよい、ということになります。最後はtokens
を返すようにします。
以上のことは、もう一つのコマンドdump
についても同じです。
こんな感じで構文の定義とマッチした時の実行内容をsetCommandSyntax()
にまとめて書いていき、それぞれの構文定義をandしたものを戻すようにしておきます。
return (dumpCommand | variableCommand)
で、mainのところ
syntax = setCommandSyntax()
として、構文定義をsyntax
という変数に放り込んでおきます。
そのあと、ソースコードを一行ずつ読み込んだ変数line_no_comment
に対してpyparsingのparseString
メソッドを実行すると、構文が解析させるとともに、もしマッチした時は先ほどのsetParseAction
が実行されることになります。
result = syntax.parseString(line_no_comment)
もし、例えばcompute
という新しい命令も解析したくなったら、
-
Command
の子クラスとしてcomputeCommand
を作る。 -
computeCommand.syntax()
に構文定義をする。 -
computeCommand._action()
に実行したいことを書いとく。 -
setCommandSyntax()
に付け足す。
をすればよくて、main側は何も変更する必要がありません。
おまけ
pyparsingでは、Token
を組み合わせて構文を作っていく。基本的なToken
は、決まり文句を表すLiteral
と、あるルール(アルファベットalphas
とか数字nums
とかエスケープを除くASCII文字printables
とか便利なやつがいくつか用意されている)に従うWord
、正規表現にマッチする字句Regex
の3種類。他は、
Token | 使い方 |
---|---|
Literal | 決まり文句 |
Empty | 何にでもマッチする |
Keyword | 完全に一致する決まり文句 |
CharsNotIn | ここで指定した文句を取り除いた字句群 |
CloseMatch | n個のミスマッチを許容するLiteral |
NoMatch | 絶対マッチしない |
White | pyparsingでは普通空白を無視するが、これを使うと、ある特定の位置に現れる空白にマッチ |
Regex | 正規表現 |
Word | あるルールに従う文句 |
これらの組み合わせていくために
-
MatchFirst
和なんだけど順序つき -
OneOf
Literalの和 -
OneOrMore
1回以上の繰り返し - などなど
が用意されている。
cmd = pp.Literal("def")
some_word = pp.Word(pp.printables)
number = pp.Regex(r"[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?").setParseAction(lambda t: float(t[0]))
参考
http://www.ptmcg.com/geo/python/confs/pyCon2006_pres2.html
http://infohost.nmt.edu/~shipman/soft/pyparsing/web/index.html