Python:re.ScannerでS式パーサ

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オブジェクトとして返る

python code
  1. import re, sys
  2. from unicodedata import east_asian_width
  3.  
  4. try:
  5.   from re import Scanner
  6. except ImportError:
  7.   from sre import Scanner
  8.  
  9. class ParseError(StandardError): pass
  10.  
  11. class Symbol(unicode):
  12.   def __repr__(self):
  13.     return "Symbol(%s)"%unicode.__repr__(self)
  14.  
  15. class 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.  
  72. def 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]
  85.  

こんな感じ。非常にシンプルな気がします。

python code
  1. print read_sexp(u"""("ほげほげ"
  2. ;comment
  3. ;comment
  4. (hogehoge 123) ;aaaaaaa
  5. "hoge\\"aaaa"
  6. ;comment
  7. ;comment
  8.  
  9. aaaa b)""")
  10.  

output:

 code
  1. [u'\u307b\u3052\u307b\u3052', [Symbol(u'hogehoge'), 123], u'hoge"aaaa', Symbol(u'aaaa'), Symbol(u'b')]
  2.  

エラーも一応。

python code
  1. print read_sexp(u"""(
  2. aaaa
  3. bbbb (ccc ddd) )
  4. (eee
  5. ああああああ""")
  6.  

output:

 code
  1. __main__.ParseError:
  2.  
  3.     2: aaaa
  4.     3: bbbb (ccc ddd) )
  5.     4: (eee
  6.     5: ああああああ
  7.                   ~
  8. line 5, 7: missing closing parenthesis.
  9.  

エラー表示もいい感じ。フォントにもよりますが(等幅なら大丈夫)、一応文字幅を考慮して~をエラー箇所に出すようにしています。HTML上だと日本語はずれちゃうかもだけど。

というわけで

Pythonでトークナイズするときにはかなり便利なんじゃないかと思いました。

Related posts:

08.22.08/07pm

No comments yet

trackback uri
  • ajax-loading
  • ajax-loading
  • ajax-loading

Leave a Comment

You can use these tags: <code>, <i>, <em>, <strong>, <a>

About

Author:yuin(http://inforno.net/)

文学部文化学科卒という生粋の文系趣味プログラマ。

主にRuby、Javascript、PHP、JAVA,Python,C,Scala,Schemeなどを使っています。今はPythonな感じかもしれない。今後作曲活動なども復活するかもしれない。

Pages