本当はパーサジェネレータでも書いた方がいいのかもしれませんが、あいにくそういうことはやっておりません。長いことツリー構造+Visitorパターンをベースに云々やってましたが、なんか意図しない所でメソッドがクロージャっぽい挙動になってて正しくないツリーが構築されちゃったり、仕組みが必要以上に大掛かりで無駄に複雑な感じになってしまったりとあまり良いことがありませんでした。で、今回はできるだけシンプルになるよう努力してみました。
基本的にボトムアップになるような感じで書いています。概要としては以下のような感じです。
継続というのは関数型言語でメジャーな考え方です。parse()の結果を呼び出し側で受け取る代わりに、結果を引数にとるような関数(、もしくはそのようなメソッドを備えたオブジェクト)をあらかじめ渡してやることで、パースの結果をどのように処理するか?という部分がソフトウェアの部品単位として交換できるようになります。開発の初期であれば結果を単にprintするようにしておけば良いし、順序関係のチェックなんかの上位の処理に渡したいときはそのようにすればいい。パース関数が返す「結果」の仕様さえ(後々変更がないよう)しっかり考えておけば、棋譜解析関連のすべての開発はこの関数をベースに出発できるわけです。
まぁ、それ故に戻り値の仕様決めはかなり難しいのですが、今回はあまり深いことは考えずにやってみます。本当はCSAでもKIFでも関係なく処理できるような仕様...というか、実世界の「棋譜」で表現されていること全部をカバーできるような仕様を考えるべきだと思ってます。適切な仕様であれば、任意のフォーマットに対応可能なはずです。
以下にコードを掲載します。main部分は解釈した結果をそのままprintするだけです。Windowsでの動作確認はやってませんが、LinuxでPython2.7なら多分動くと思います。
#!/usr/bin/env python # -*- coding: utf-8 -*- import os,sys,glob import string,re import csaioutil import signal,time,cProfile class DefaultLineParser: """ CSA ver.1.0 [player name] N+, N- [starting condition] PI, P1,P2,...P9, P+, P- +,- [move] [+-](00|[1-9]{2}[1-9]{2}(piece)(<sep>T[1-9][0-9]*)? [special command] %(TORYO|MATTA|CHUDAN|SENNICHITE|JISHOGI|TSUMI|FUZUMI|ERROR|KACHI|HIKIWAKE) """ mpnm= re.compile("^N[+-]") mpi = re.compile("^PI((00|[1-9]{2})(FU|KY|KE|GI|KI|KA|HI|OU)){0,9}") mpn = re.compile("^P[1-9](([^+-].{2}|[+-](FU|KY|KE|GI|KI|KA|HI|TO|NY|NK|NG|UM|RY|OU)))*") mpx = re.compile("^P[+-](00AL|((00(FU|KY|KE|GI|KI|KA|HI))|[1-9]{2}(FU|KY|KE|GI|KI|KA|HI|TO|NY|NK|NG|UM|RY|OU))*)") mtrn= re.compile("^[+-]$") mmov= re.compile("^[+-]((00[1-9]{2})(FU|KY|KE|GI|KI|KA|HI)|[1-9]{4}(FU|KY|KE|GI|KI|KA|HI|TO|NY|NK|NG|UM|RY|OU))") mtim= re.compile("T(0|[1-9][0-9]*)") msp = re.compile("^%(TORYO|MATTA|CHUDAN|SENNICHITE|JISHOGI|TSUMI|FUZUMI|ERROR|KACHI|HIKIWAKE)") mcom= re.compile("^'") memp= re.compile("^$") mksp= re.compile("^/") matches = [mpnm, mpi, mpn, mpx, mtrn, mmov, mtim, msp, mcom, memp, mksp] #groups = { # "playername": [mpnm], # "position": [mpi, mpn, mpx], # "turn": [mtrn], # "move": [mmov,mtim], # "sp" : [msp], # "comment": [mcom], # "empty": [memp], # "kifusep": [mksp] #} @classmethod def parse(cls,line): """ main()部分で動いているのはこれを継承したV2LineParserの方なので、 ここの実装はとりあえずダミー。 デフォルト実装としてCSA Ver.1.0を想定しているので、本来はそれをパースできるように書いとく所。 """ for i,m in enumerate(cls.matches): if(m.match(line)): return line raise Exception, "CSA SYNTAX ERROR: "+line def nsplit(l,n): """ 文字列をn文字ずつに分割する関数 """ return [l[i:i+n] for i in range(0,len(l),n)] def generateTerm(cate,tag,values=[]): """ パース結果の形式を生成する関数 """ return [{u"categoly": cate, u"tag": tag, u"values": values}] class V2LineParser(DefaultLineParser): #mpnm= re.compile("^N[+-]") #mpi = re.compile("^PI((00|[1-9]{2})(FU|KY|KE|GI|KI|KA|HI|OU)){0,9}") #mpn = re.compile("^P[1-9](([^+-].{2}|[+-](FU|KY|KE|GI|KI|KA|HI|TO|NY|NK|NG|UM|RY|OU)))*") #mpx = re.compile("^P[+-](00AL|((00(FU|KY|KE|GI|KI|KA|HI))|[1-9]{2}(FU|KY|KE|GI|KI|KA|HI|TO|NY|NK|NG|UM|RY|OU))*)") #mtrn= re.compile("^[+-]$") #mmov= re.compile("^[+-]((00[1-9]{2})(FU|KY|KE|GI|KI|KA|HI)|[1-9]{4}(FU|KY|KE|GI|KI|KA|HI|TO|NY|NK|NG|UM|RY|OU))") #mtim= re.compile("T(0|[1-9][0-9]*)") #msp = re.compile("^%(TORYO|MATTA|CHUDAN|SENNICHITE|JISHOGI|TSUMI|FUZUMI|ERROR|KACHI|HIKIWAKE)") #mcom= re.compile("^'") #memp= re.compile("^$") #mksp= re.compile("^/") minf= re.compile("^\$.*:.*") mver= re.compile("^V2(.[0-3])?") @classmethod def parse(cls,line): c="" if(len(line)==0): return generateTerm(u"other", u"empty", []) c=line[0] if((not (c=="$" or c=="'")) and len(line.split(","))>1): # multi statement return reduce(lambda a,b: a+b, map(lambda l: cls.parse(l), line.split(","))) if(c=="V"): if(cls.mver.match(line)): return generateTerm(u"version", u"version",[line[1:]]) elif(c=="N"): if(cls.mpnm.match(line)): if(line[1]=="+"): return generateTerm(u"info", u"name+", [line[2:]]) elif(line[1]=="-"): return generateTerm(u"info", u"name-", [line[2:]]) elif(c=="$"): if(cls.minf.match(line)): return generateTerm(u"info", line.split(":")[0], [line.split(":")[1:]]) elif(c=="P"): if(cls.mpi.match(line)): #mytag return generateTerm(u"ini", u"pi", map(lambda l: [int(l[0]), int(l[1]), l[2:4]], nsplit(line[2:],4))) elif(cls.mpn.match(line)): return generateTerm(u"ini", u"pl", [int(line[1]), map(lambda l: [l[0], l[1:3]] if(l[0]=="+" or l[0]=="-") else [u"*",u"*"], nsplit(line[2:],3))]) elif(cls.mpx.match(line)): return generateTerm(u"ini", u"px", [line[1], map(lambda l: [int(l[0]), int(l[1]), l[2:4]], nsplit(line[2:],4))]) elif(c=="+" or c=="-"): if(len(line)==1): return generateTerm(u"ini", u"turn",[c]) else: if(cls.mmov.match(line)): return generateTerm(u"move", c, [int(line[1]), int(line[2]), int(line[3]), int(line[4]), line[5:7]]) elif(c=="T"): if(cls.mtim.match(line)): return generateTerm(u"move", u"time", [int(line[1:])]) elif(c=="%"): if(cls.msp.match(line)): return generateTerm(u"move", u"special", [line[1:]]) elif(c=="'"): if(cls.mcom.match(line)): return generateTerm(u"comment", u"", [line[1:]]) elif(c=="/"): if(cls.mksp.match(line)): return generateTerm(u"kifuseparator", u"", []) raise Exception,line return None def parseline(line,parser=DefaultLineParser): """ parseline(line, parser=DefaultParser) Argument 'line' is must Unicode string 'parser' is a class object that line parser, parser class must have an interface classmethod 'parse(cls, line)' """ return parser.parse(line) class cont: # parse()に対するコールバック用のクラス。関数型言語で言う所の「継続」。 def accept(self,res): print res[u"categoly"]+" : "+res[u"tag"]+" : "+str(res[u"values"]) def parseline2(line, continuous, parser=DefaultLineParser): """ variation of parseline(), using continuous object """ for res in parseline(line, parser): continuous.accept(res) def test(): lines = [ "'csa version 1.0", "N+you", "N-com", "PI-82HI", "-", "-3334FU", "T1", "%TORYO"] lines = map(lambda l: unicode(l), lines) for line in lines: parseline2(line, cont(), DefaultLineParser) def test2(): lines = csaioutil.readlines_wrap(file("sample.csa")) # readlines() + unicode文字列への変換を行うラッパー関数 for line in lines: parseline2(line, cont(),V2LineParser) if __name__=="__main__": cProfile.run("test2()")