前のエントリーで簡易S式パーサをre.Scannerで作ったのですが、まぁ個人的にまとめておいたほうが後々使えるだろう、ということでライブラリにまとめました。ダンパもついているので、S式の読み込みの他、PythonオブジェクトをS式で出力することができます。
実装には引き続きre.Scannerを活用しています。おかげで短い行数でキレイにかけているのではないかと。
ダウンロード
ソースはこんな感じ(テストのぞく)。
import re, sysfrom unicodedata import east_asian_widthtry:from re import Scannerexcept ImportError:from sre import Scannerclass ParseError(StandardError): passclass Ident(unicode):def __repr__(self):return "Ident(%s)"%unicode.__repr__(self)class Symbol(unicode):def __repr__(self):return "Symbol(%s)"%unicode.__repr__(self)class Pair(list):def __repr__(self):return "Pair(%s)"%list.__repr__(self)class Token(object):def __init__(self, value, pos):self.value = valueself.pos = posdef __repr__(self):return repr(self.value)class Binding(object):def __init__(self, dct):self.dct = dict(((k, k.__class__), v) for k,v in dct.iteritems())__contains__ = lambda self, key: (key, key.__class__) in self.dct__getitem__ = lambda self,key: self.dct[(key, key.__class__)]default_binding = {"#t":True, "true":True, "#f":False, "false":False, "nil":None, "dict":Ident(u'alist->hash-table')}class Reader(object):PAREN = {"]":"[", ")":"("}def __init__(self, binding=None, symbol_marker="'", use_dict=True):self.binding = binding or default_bindingself.symbol_marker = symbol_markerself.use_dict = use_dictdef read(self, value):self.result = []self.paren_stack = []self.source = valueself.pos = 0self.scanner = Scanner([(r"\s+", self("skip")),(r";[^\n]*\n", self("skip")),(r""""(((?<=\\)")|[^"])*((?<!\\)")""", self("str")),(r"(\(|\[)", self("open")),(r"(\)|\])", self("close")),(r"(([\d]+|(((\d+)?\.[\d]+)|([\d]+\.)))e[\+\-]?[\d]+)|(((\d+)?\.[\d]+)|([\d]+\.))", self("number")),(r"\-?((0x[\da-f]+)|(0[0-7]+)|([1-9][\d]*)|0)[l]?", self("number")),(r"""%s([^\(\[\)\]\s"]+)"""%self.symbol_marker, self("symbol")),(r"""([^\(\[\)\]\s"]+)""", self("ident")),(r"""".*""", self("unterm_str")),(r".*", self("unknown_token"))], re.M|re.S|re.I)self.scanner.scan(self.source)if self.paren_stack:self.raise_error("missing closing parenthesis.")return self.parse(self.result)def append(self, v):self.last().append(Token(v, self.pos))def __call__(self, name):def _(scanner, s):self.pos += len(s)return getattr(self, name)(s)return _def unknown_token(self,s): self.raise_error("unknown token: %s"%s)def skip(self, _): passdef open(self, s):new_lst = []self.last().append(new_lst)self.paren_stack.append([s, new_lst])def close(self, s):if not self.paren_stack:self.raise_error("missing opening parenthesis.")if self.PAREN[s] != self.paren_stack.pop()[0]:self.raise_error("missing closing parenthesis.")def str(self, s): self.append(eval('u""'+s+'""'))def unterm_str(self, s): self.raise_error("unterminated string literal.")def number(self, s): self.append(eval(s))def symbol(self, s): self.append(Symbol(s[1:]))def ident(self, s):if s in self.binding:self.append(self.binding[s])else:self.append(Ident(s))def last(self):if self.paren_stack:return self.paren_stack[-1][1]else:return self.resultdef parse(self, rs):def is_ident(value, expected):return getattr(value,"value", None) == Ident(expected)def is_pair(rs):return getattr(rs, "__len__", lambda :0)()==3 and is_ident(rs[1], u".")if isinstance(rs, list):if not len(rs):return []elif self.use_dict and is_ident(rs[0], u"alist->hash-table"):if len(rs) != 2:self.raise_error("alist->hash-table: expected 1 arguments, got %d."%(len(rs)-1), rs[0].pos)if not all(is_pair(a) for a in rs[1]):self.raise_error("alist->hash-table: aruguments must be alist", rs[0].pos)return dict((self.parse(i[0]), self.parse(i[2])) for i in rs[1])elif len(rs)!=3 and any(is_ident(t, u".") for t in rs):self.raise_error('illegal use of "."', rs[0].pos)elif is_pair(rs):parsed = self.parse(rs[2])if not isinstance(rs[2], list):return Pair([rs[0].value, parsed])if isinstance(parsed, Pair):return Pair([rs[0].value, parsed])elif isinstance(parsed, list):return [rs[0].value]+parsedelse:return [rs[0].value, parsed]else:return map(self.parse, rs)else:return rs.valuedef raise_error(self, msg="parse error", pos=None, range=3):pos = pos or self.poslines = self.source.split("\n")curline = self.source[:pos].count("\n")linepos = pos - len("\n".join(lines[:curline]))buf = ["\n"]for i in xrange(max(0, curline-range), curline+1):buf.append("% 5d: %s"%(i+1, lines[i]))width = 7 + sum(east_asian_width(c) == 'W' and 2 or 1 for c in unicode(lines[i]))buf.append("%s~"%(" "*width))buf.append("line %d, %d: %s"%(curline+1,linepos, msg))raise ParseError(("\n".join(buf)).encode(sys.stderr.encoding))class Dumper(object):def __init__(self, binding=None ,symbol_marker="'"):binding = binding or default_bindingself.binding = Binding(dict(zip(binding.values(), binding)))self.symbol_marker = symbol_markerdef dump(self, obj):result = self.to_sexp(obj, [])if isinstance(result, list) and len(result) and result[0]=="(":result = result[1:-1]return u" ".join(result)def to_sexp(self, obj, result):ap = result.appendtos = lambda v: self.to_sexp(v, result)if isinstance(obj, Pair):ap("(")tos(obj[0])ap(" . ")tos(obj[1])ap(")")elif isinstance(obj, (tuple, list)):ap("(")map(tos, obj)ap(")")else:if isinstance(obj, dict):ap("( alist->hash-table ")tos([(k, Ident(u"."), v) for k,v in obj.items()])ap(" ) ")elif obj in self.binding:ap(unicode(Ident(self.binding[obj])))elif isinstance(obj, Symbol):ap(u"'%s"%unicode(obj))elif isinstance(obj, (Ident,int, float, long)):ap(unicode(obj))else:s = unicode(repr(obj)).decode("unicode_escape")m = re.match(r"""^[u|r]?["|'](.*)["|']$""", s, re.M|re.S)if m:s = m.group(1)ap("\"%s\""%s.replace('"','\\"').replace("\\'","'"))return resultdumper = Dumper()read = Reader().readdump = dumper.dump
概要
特徴は
- 辞書を定義できる
- ドット対に対応
- 識別子に対して、任意のバインディングを指定できる
- わりとちゃんとエラー表示される
- シンボル表記、数値表記(python表記)に対応
といった当たりでしょうか。具体的にはテストコードを見てもらうと分かるかと。
(あああ hoge->fuga123 (1 . (2 . 3)) "hoge\\"hoge" ;comment2foo "aaa" #t <= 'foo"hogehogefoo" (5 . (6 .())))(dict (("いいい" .(alist->hash-table (("a-1" . "vvv")("a-2" . (hoge foo bar)))))))(10 1L -45 010 0x10 -10 -0x10 3.14 10. .001 1e100 3.14e-10 0e0); comment3 ()(""")
という感じのS式が
[[Ident(u'あああ'), Ident(u'hoge->fuga123'), Pair([1, Pair([2, 3])]), u'hoge"hoge',Ident(u'foo'), u'aaa', True, Ident(u'<='), Symbol(u'foo'),u'hogehoge\nfoo\n', [5,6]],{u'いいい':{u'a-1': u'vvv',u'a-2': [Ident(u'hoge'), Ident(u'foo'), Ident(u'bar')]}},[10, 1L, -45, 010, 0x10, -10, -0x10, 3.14, 10., .001, 1e100, 3.14e-10, 0e0]]
となります。IdentやSymbolはunicodeのサブクラス、Pairはリストのサブクラスになっているので、違和感なく使えると思います。
また、alist->hash-tableで辞書が作れます。デフォルトでdictにalist->hash-tableをバインドしてますので、dictでも辞書が作れます。この機能はオンオフ切り替えも可能です。
その他、#tをTrueなどSchemeっぽくデフォルトバインディングが用意してあります。もちろん、バインディングは変更可能ですのでCLっぽくもできます。
と、こんな感じです。一番便利なのはやっぱり辞書ですかねえ。なので、YAML,JSONで書いてる設定ファイルをS式で置き換え・・・なんてことができるかもしれません。
No comments yet
trackback uriLeave a Comment