Rubyの StringScanner
は個人的にかなり好きなモジュールで、Rubyでちょっとしたパーサなどを書くときに重宝しています。
一方、Pythonにはexperimentalながら re.Scanner
というクラスがあります( >= 2.4
)。experimentalなのでマニュアルにはのっていませんが。この re.Scanner
はかなりシンプルなんですが典型的な StringScanner
の使い方の範疇では、こちらのほうがキレイに書けるような気がします。
re.Scanner
の使い方
使い方は非常に簡単で
-
(regex, action)
のリストを渡してScannerオブジェクトを作成action
は(scanner, string_matched) => stringな関数、Noneを返せば結果は無視される。
-
scanメソッドでスキャン。結果が配列で返ってくる
といった感じ。関数を渡すので、 StringScanner
のようなwhileループを作る必要がなく、キレイにまとまります。
例:S式パーサ
re.Scanner
は簡単、ということでS式パーサでも。トークナイズ+αな処理をするので、 action
をインスタンスメソッドにして状態を保存することにします。
目標は
- 数値(っぽいもの)、文字列、シンボルが使える
- シンボルのみ、新たにクラスを定義して(unicodeのサブクラス)それにマップ。それ以外は組み込み型に。
- パースエラーも分かりやすく
- 結果はPythonのリストorオブジェクトとして返る
で
1import re, sys
2from unicodedata import east_asian_width
3
4try:
5 from re import Scanner
6except ImportError:
7 from sre import Scanner
8
9class ParseError(StandardError): pass
10
11class Symbol(unicode):
12 def __repr__(self):
13 return "Symbol(%s)"%unicode.__repr__(self)
14
15class TokenProcessor(object):
16 PAREN = {"]":"[", ")":"("}
17 def __init__(self, value):
18 self.result = []
19 self.append = self.result.append
20 self.string = value
21 self.paren_stack = []
22 self.pos = 0
23
24 def __call__(self, name):
25 def _(*a):
26 self.before(*a)
27 return getattr(self, name)(*a)
28 return _
29
30 def before(self, scanner, s):
31 self.pos += len(s)
32 self.skip(scanner, s)
33
34 def error(self, scanner, s): self.raise_error("unknown token: %s"%s)
35
36 def skip_whitespaces(self, scanner, s): self.append(",")
37
38 def skip(self, scanner, s):
39 last = "".join(self.result[-2:])
40 if last in ["[,", ",,", ",]"]:
41 self.result[-2:] = sorted(last, key=ord)[1]
42
43 def atom(self, scanner, s):
44 if s in ["(", "["]:
45 self.append("[")
46 self.paren_stack.append(s)
47 elif s in [")", "]"]:
48 if not self.paren_stack:
49 self.raise_error("missing opening parenthesis.")
50 if self.PAREN[s] != self.paren_stack.pop():
51 self.raise_error("missing closing parenthesis.")
52 self.append("]")
53 elif re.match(r"""^(".*)$""", s or ""):
54 self.append("u"+s)
55 elif re.match(r"""^((\-?\d[\de\.]+)|(\s*)|(.*"))$""", s or ""):
56 self.append(s)
57 else:
58 self.append("Symbol(u\"%s\")"%s)
59
60 def raise_error(self, msg="parse error", range=3):
61 lines = self.string.split("\n")
62 curline = self.string[:self.pos].count("\n")
63 linepos = self.pos - len("\n".join(lines[:curline]))
64 buf = ["\n"]
65 for i in xrange(max(0, curline-range), curline+1):
66 buf.append("% 5d: %s"%(i+1, lines[i]))
67 width = 6 + sum(east_asian_width(c) == 'W' and 2 or 1 for c in lines[i])
68 buf.append("%s~"%(" "*width))
69 buf.append("line %d, %d: %s"%(curline+1,linepos, msg))
70 raise ParseError(("\n".join(buf)).encode(sys.stderr.encoding))
71
72def read_sexp(sexp):
73 processor = TokenProcessor(sexp)
74 scanner = Scanner([
75 (r"\s+", processor("skip_whitespaces")),
76 (r";[^\n]*\n", processor("skip")),
77 (r""""(?:[^"])*"|(\]|\[|\)|\(|[^\(\)\s]+)""", processor("atom")),
78 (r".*", processor("error"))
79 ], re.M)
80 scanner.scan(processor.string)
81 if processor.paren_stack:
82 processor.raise_error("missing closing parenthesis.")
83 result = eval("".join(processor.result).lstrip(","))
84 return (isinstance(result, tuple) and (result[0],0) or (result,0))[0]
こんな感じ。非常にシンプルな気がします。
1print read_sexp(u"""("ほげほげ"
2;comment
3 ;comment
4 (hogehoge 123) ;aaaaaaa
5 "hoge\\"aaaa"
6;comment
7;comment
8
9aaaa b)""")
output:
1[u'\u307b\u3052\u307b\u3052', [Symbol(u'hogehoge'), 123], u'hoge"aaaa', Symbol(u'aaaa'), Symbol(u'b')]
エラーも一応。
1print read_sexp(u"""(
2aaaa
3bbbb (ccc ddd) )
4(eee
5ああああああ""")
output:
1__main__.ParseError:
2
3 2: aaaa
4 3: bbbb (ccc ddd) )
5 4: (eee
6 5: ああああああ
7 ~
8line 5, 7: missing closing parenthesis.
エラー表示もいい感じ。フォントにもよりますが(等幅なら大丈夫)、一応文字幅を考慮して ~
をエラー箇所に出すようにしています。HTML上だと日本語はずれちゃうかもだけど。
というわけで
Pythonでトークナイズするときにはかなり便利なんじゃないかと思いました。