plex / plex3 (Python の GNU Flex もどき) のへろわるど

ドキュメントのちら見で箸か棒にかかるなら入り口くらいは遊んでみるかモード。

一つ前、二つ前同様 Python Parsing Tools より。

リアルワールドなご用では parser はいらないけど lexer はちゃんとしたのが欲しい、というケースが結構多く、ワタシは学生時代は「GNU flex のみで」書くフィルタープログラムを書くのに凝ってた時期があった。C プログラムを「綺麗に印刷するためのフォーマッタ」なんて書いてたわ。今にして思えば、ワタシの Sphinx 好きは、この頃から芽はあったのかもな。

てなわけで plex:

Plex is a Python module for constructing lexical analysers, or scanners. Plex scanners have almost all the capabilities of the scanners generated by GNU Flex, and are specified in a very similar way. Tokens are defined by regular expressions, and each token has an associated action, which may be to return a literal value, or to call an arbitrary function.

元祖 lex と GNU flex との、目に見える一番大きな機能拡張が「状態遷移」を管理できること、だったのよね。そして ply とか rply でこれがないのが微妙にツラいかったりしてるわけ。そうなんだろ? と思ってすぐにこの例が目に留まる:

 1 #
 2 #   Example 5
 3 #
 4 
 5 lex = Lexicon([
 6     (name,            'ident'),
 7     (number,          'int'),
 8     (space,           IGNORE),
 9     (Str("(*"),       Begin('comment')),
10     State('comment', [
11         (Str("*)"), Begin('')),
12         (AnyChar,   IGNORE)
13     ])
14 ])

そうなんだよなぁ、これがあるなしで結構作りやすさ、違うんだわ。と、まずは好印象から始まってみる。

次に考えたのが「rply は lexer と parser が完全に分離してるので、plex の Scanner でリプレイスしちゃえんかなぁ?」だった。結論からはこれは「食べれない」、おいしくない:

Scanning is performed by a Scanner object. When you create a Scanner, you specify the Lexicon that is to be used, and an input stream, which should be a file-like object.

To read tokens from the input stream, you call the read() method of the Scanner. Each time read() is called, it returns a tuple

1 (value, text)

where value is the value returned by the token’s action, and text is the text from the input stream that matched the token.

We can test out the Lexicon we defined earlier like this:

 1 #
 2 #   Example 2
 3 #
 4 
 5 filename = "my_file.txt"
 6 f = open(filename, "r")
 7 scanner = Scanner(lexicon, f, filename)
 8 while 1:
 9     token = scanner.read()
10     print(token)
11     if token[0] is None:
12         break

ファイルライクオブジェクト前提なのね。rply 向けアダプタを作れなくないけどちとノリが違うのよね。あんまし嬉しかない。

と、ここまでは、インストールしてみることも動かしてみることもなく、のファーストインプレッション。棒にはかかっておるよさ。

そもそもオリジナル plex が v 2.0 12/2009 ととても古く、plex3 (Python3 port of Plex) (No official release) が存在してる、ってだけで臆してしまうんだけれど、まぁやろうとだけはしてみよう、と…:

Example 5 にコードを補って…
 1 from plex import *
 2 
 3 #
 4 #   Example 6
 5 #
 6 
 7 lex = Lexicon([
 8     (name,            'ident'),
 9     (number,          'int'),
10     (space,           IGNORE),
11     (Str("(*"),       Begin('comment1')),
12     (Str("{"),        Begin('comment2')),
13     State('comment1', [
14         (Str("*)"), Begin('')),
15         (AnyChar,   IGNORE)
16     ]),
17     State('comment2', [
18         (Str("}"),  Begin('')),
19         (AnyChar,   IGNORE)
20     ])
21 ])
22 import sys
23 scanner = Scanner(lex, sys.stdin)
24 while True:
25     token = scanner.read()
26     print(token)
27     if token[0] is None:
28         break
1 [me@host: ~]$ echo 'a (* this is comment *)' | py -2 plex_exam5.py
2 Traceback (most recent call last):
3   File "plex1.py", line 8, in <module>
4     (name,            'ident'),
5 NameError: name 'name' is not defined

うーん、ドキュメントを前から読んでいかないとダメなパターンか…:

Example 5 にコードをもっと清く正しく補って…
 1 from plex import *
 2 
 3 #
 4 #   Example 6
 5 #
 6 
 7 letter = Range("AZaz")
 8 digit = Range("09")
 9 name = letter + Rep(letter | digit)
10 number = Rep1(digit)
11 space = Any(" \t\n")
12 
13 lex = Lexicon([
14     (name,            'ident'),
15     (number,          'int'),
16     (space,           IGNORE),
17     (Str("(*"),       Begin('comment1')),
18     (Str("{"),        Begin('comment2')),
19     State('comment1', [
20         (Str("*)"), Begin('')),
21         (AnyChar,   IGNORE)
22     ]),
23     State('comment2', [
24         (Str("}"),  Begin('')),
25         (AnyChar,   IGNORE)
26     ])
27 ])
28 import sys
29 scanner = Scanner(lex, sys.stdin)
30 while True:
31     token = scanner.read()
32     print(token)
33     if token[0] is None:
34         break
1 [me@host: ~]$ echo 'a (* this is comment *)' | py -2 plex_exam5.py
2 ('ident', 'a')
3 (None, '')

うん、期待通り。が…

 1 [me@host: ~]$ echo 'a (* this is comment *)' | py -3 plex_exam5.py
 2 Traceback (most recent call last):
 3   File "plex1.py", line 1, in <module>
 4     from plex import *
 5   File "C:\Python35\lib\site-packages\plex\__init__.py", line 33, in <module>
 6     from plex.lexicons import Lexicon, State
 7   File "C:\Python35\lib\site-packages\plex\lexicons.py", line 117
 8     if type(specifications) <> types.ListType:
 9                              ^
10 SyntaxError: invalid syntax

<>」なんていつの Python だよっ、て思うが、それ以前に、あらら、2.7 ってまだ「<>」使えるんだ、て事実にも驚く。

やな予感、の通り、plex3 (Python3 port of Plex) が必要で、まぁこっち入れとけば Python 2.x でも使えるわけだが、PyPI に未登録なわけなので、自力でインストールな。しかるに:

 1 # original plex:
 2 #   doesn't support python 3 (never work)
 3 #
 4 # plex3 (for python 3 fork):
 5 #   No official release, so visit https://github.com/uogbuji/plex3,
 6 #   download (or git clone), and do this:
 7 #       python setup.py install
 8 #from plex import *
 9 from plex3 import *
10 
11 letter = Range("AZaz")
12 digit = Range("09")
13 name = letter + Rep(letter | digit)
14 number = Rep1(digit)
15 space = Any(" \t\n")
16 
17 lexicon = Lexicon([
18     (name,            'ident'),
19     (number,          'int'),
20     (space,           IGNORE),
21     (Str("(*"),       Begin('comment1')),
22     (Str("{"),        Begin('comment2')),
23     State('comment1', [
24         (Str("*)"), Begin('')),
25         (AnyChar,   IGNORE)
26     ]),
27     State('comment2', [
28         (Str("}"),  Begin('')),
29         (AnyChar,   IGNORE)
30     ])
31 ])
32 
33 import sys
34 scanner = Scanner(lexicon, sys.stdin)
35 while True:
36     token = scanner.read()
37     print(token)
38     if token[0] is None:
39         break

これで 2/3 両方動く。

いかんせん完全にストップしちゃってて今後の進化もあまり期待出来なさそうなので、少なくとも人にオススメは出来ない。結構好みではあるんだけどね。