這是本人(liigo)獨立實現的SGF格式圍棋棋譜文件解析器,本文介紹其實現細節。網絡上肯定可以找到完善的開源的SGF解析器,這是毋庸置疑的,我不直接使用它們,也不參考它們的實現代碼,而是自己獨立編碼實現,是有原因的,因為我想自己重復發明輪子,並且認為這樣更有助於提高我的編碼能力。(關於我的“一定要學會重復發明輪子”的不成熟的論調,今後我將會專門撰文表述。)
我(liigo)開發的這個SGF解析器,采用基於事件的簡單API,類似於XML解析器中的SAX(Simple API for XML)。這種解析器的核心是:由用戶事先提供一系列回調函數,解析器在解析的過程中,依次調用相關的回調函數並傳入相應參數,用戶程序在回調函數中做出相應的處理。此類解析器屬於輕量級的解析器,解析速度快,占用內存少,結構清晰易於實現,只是相對來說不如基於DOM的解析器方便使用。
SGF格式,Smart Game Format,被設計用來記錄多種游戲類棋譜的通用格式,在圍棋領域被發揚光大,是用於描述圍棋棋譜的最重要也最通用的形式。它是純文本的、基於樹(TREE)的結構,便於識別、存儲和傳輸。其格式簡潔實用,也非常易於編程解析。SGF格式官方規范網址為:http://www.red-bean.com/sgf/。(說到圍棋棋譜,不得不贊歎一下,它只需用一幅圖就可以完整還原一盤棋從始至終的風雲變幻;作為對比,象棋一幅圖只能描述對弈中某一時刻的場景。)
SGF的主要結構由樹(GameTree)、節點序列(Sequence)、節點(Node)、屬性(Property)等組成。其中“屬性”為最重要的基本單位,它由屬性標識(PropIdent)和屬性值(PropValue)組成。由分號“;”分隔的多個屬性,稱為節點。多個節點順序排列稱為節點序列。由括號“(”“)”括起來的節點序列,稱為樹,樹中可包含子樹。SGF的EBNF定義如下(參見html#ebnf-def">http://www.red-bean.com/sgf/sgf4.html#ebnf-def):
view plaincopy to clipboardprint?
Collection = GameTree { GameTree }
GameTree = "(" Sequence { GameTree } ")"
Sequence = Node { Node }
Node = ";" { Property }
Property = PropIdent PropValue { PropValue }
PropIdent = UcLetter { UcLetter }
PropValue = "[" CValueType "]"
CValueType = (ValueType | Compose)
ValueType = (None | Number | Real | Double | Color | SimpleText | Text | Point | Move | Stone)
Collection = GameTree { GameTree }
GameTree = "(" Sequence { GameTree } ")"
Sequence = Node { Node }
Node = ";" { Property }
Property = PropIdent PropValue { PropValue }
PropIdent = UcLetter { UcLetter }
PropValue = "[" CValueType "]"
CValueType = (ValueType | Compose)
ValueType = (None | Number | Real | Double | Color | SimpleText | Text | Point | Move | Stone)
以下是一個簡單的有一定代表性的SGF文本,先讓大家有一個感性認識:
+ expand sourceview plaincopy to clipboardprint?
(;FF[4]GM[1]SZ[19]FG[257:Figure 1]PM[1]
PB[Takemiya Masaki]BR[9 dan]PW[Cho Chikun]
WR[9 dan]RE[W+Resign]KM[5.5]TM[28800]DT[1996-10-18,19]
EV[21st Meijin]RO[2 (final)]SO[Go World #78]US[Arno Hollosi]
;B[pd];W[dp];B[pp];W[dd];B[pj];W[nc];B[oe];W[qc];B[pc];W[qd]
(;B[qf];W[rf];B[rg];W[re];B[qg];W[pb];B[ob];W[qb]
(;B[mp];W[fq];B[ci];W[cg];B[dl];W[cn];B[qo];W[ec];B[jp];W[jd]
;B[ei];W[eg];B[kk]LB[qq:a][dj:b][ck:c][qp:d]N[Figure 1]
;W[me]FG[257:Figure 2];B[kf];W[ke];B[lf];W[jf];B[jg]
(;W[mf];B[if];W[je];B[ig];W[mg];B[mj];W[mq];B[lq];W[nq]
(;B[lr];W[qq];B[pq];W[pr];B[rq];W[rr];B[rp];W[oq];B[mr];W[oo];B[mn]
(;W[nr];B[qp]LB[kd:a][kh:b]N[Figure 2]
;W[pk]FG[257:Figure 3];B[pm];W[oj];B[ok];W[qr];B[os];W[ol];B[nk];W[qj]
;B[pi];W[pl];B[qm];W[ns];B[sr];W[om];B[op];W[qi];B[oi]
(;W[rl];B[qh];W[rm];B[rn];W[ri];B[ql];W[qk];B[sm];W[sk];B[sh];W[og]
;B[oh];W[np];B[no];W[mm];B[nn];W[lp];B[kp];W[lo];B[ln];W[ko];B[mo]
;W[jo];B[km]N[Figure 3])
(;W[ql]VW[ja:ss]FG[257:Dia. 6]MN[1];B[rm];W[ph];B[oh];W[pg];B[og];W[pf]
;B[qh];W[qe];B[sh];W[of];B[sj]TR[oe][pd][pc][ob]LB[pe:a][sg:b][si:c]
N[Diagram 6]))
(;W[no]VW[jj:ss]FG[257:Dia. 5]MN[1];B[pn]N[Diagram 5]))
(;B[pr]FG[257:Dia. 4]MN[1];W[kq];B[lp];W[lr];B[jq];W[jr];B[kp];W[kr];B[ir]
;W[hr]LB[is:a][js:b][or:c]N[Diagram 4]))
(;W[if]FG[257:Dia. 3]MN[1];B[mf];W[ig];B[jh]LB[ki:a]N[Diagram 3]))
(;W[oc]VW[aa:sk]FG[257:Dia. 2]MN[1];B[md];W[mc];B[ld]N[Diagram 2]))
(;B[qe]VW[aa:sj]FG[257:Dia. 1]MN[1];W[re];B[qf];W[rf];B[qg];W[pb];B[ob]
;W[qb]LB[rg:a]N[Diagram 1]))
(;FF[4]GM[1]SZ[19]FG[257:Figure 1]PM[1]
PB[Takemiya Masaki]BR[9 dan]PW[Cho Chikun]
WR[9 dan]RE[W+Resign]KM[5.5]TM[28800]DT[1996-10-18,19]
EV[21st Meijin]RO[2 (final)]SO[Go World #78]US[Arno Hollosi]
;B[pd];W[dp];B[pp];W[dd];B[pj];W[nc];B[oe];W[qc];B[pc];W[qd]
(;B[qf];W[rf];B[rg];W[re];B[qg];W[pb];B[ob];W[qb]
(;B[mp];W[fq];B[ci];W[cg];B[dl];W[cn];B[qo];W[ec];B[jp];W[jd]
;B[ei];W[eg];B[kk]LB[qq:a][dj:b][ck:c][qp:d]N[Figure 1]
;W[me]FG[257:Figure 2];B[kf];W[ke];B[lf];W[jf];B[jg]
(;W[mf];B[if];W[je];B[ig];W[mg];B[mj];W[mq];B[lq];W[nq]
(;B[lr];W[qq];B[pq];W[pr];B[rq];W[rr];B[rp];W[oq];B[mr];W[oo];B[mn]
(;W[nr];B[qp]LB[kd:a][kh:b]N[Figure 2]
;W[pk]FG[257:Figure 3];B[pm];W[oj];B[ok];W[qr];B[os];W[ol];B[nk];W[qj]
;B[pi];W[pl];B[qm];W[ns];B[sr];W[om];B[op];W[qi];B[oi]
(;W[rl];B[qh];W[rm];B[rn];W[ri];B[ql];W[qk];B[sm];W[sk];B[sh];W[og]
;B[oh];W[np];B[no];W[mm];B[nn];W[lp];B[kp];W[lo];B[ln];W[ko];B[mo]
;W[jo];B[km]N[Figure 3])
(;W[ql]VW[ja:ss]FG[257:Dia. 6]MN[1];B[rm];W[ph];B[oh];W[pg];B[og];W[pf]
;B[qh];W[qe];B[sh];W[of];B[sj]TR[oe][pd][pc][ob]LB[pe:a][sg:b][si:c]
N[Diagram 6]))
(;W[no]VW[jj:ss]FG[257:Dia. 5]MN[1];B[pn]N[Diagram 5]))
(;B[pr]FG[257:Dia. 4]MN[1];W[kq];B[lp];W[lr];B[jq];W[jr];B[kp];W[kr];B[ir]
;W[hr]LB[is:a][js:b][or:c]N[Diagram 4]))
(;W[if]FG[257:Dia. 3]MN[1];B[mf];W[ig];B[jh]LB[ki:a]N[Diagram 3]))
(;W[oc]VW[aa:sk]FG[257:Dia. 2]MN[1];B[md];W[mc];B[ld]N[Diagram 2]))
(;B[qe]VW[aa:sj]FG[257:Dia. 1]MN[1];W[re];B[qf];W[rf];B[qg];W[pb];B[ob]
;W[qb]LB[rg:a]N[Diagram 1]))
熟悉編寫文本解析器的程序員朋友應該都清楚,根據EBNF定義,編寫對應的解析器,是相當簡單和直觀的,貌似只是一項翻譯性的工作。本人實現SGF解析器,再次印證了這個觀點,大部分情況下,我只是按部就班地將EBNF翻譯為C語言代碼而已,呵呵。
我首先設計了“SGFParseContext”結構,用於保存解析器工作期間的相關數據:
view plaincopy to clipboardprint?
typedef struct _tagSGFParseContext
{
void* pUserData;
int treeIndex;
PFN_ON_TREE pfnOnTree;
PFN_ON_TREE_END pfnOnTreeEnd;
PFN_ON_NODE pfnOnNode;
PFN_ON_NODE_END pfnOnNodeEnd;
PFN_ON_PROPERTY pfnOnProperty;
char idBuffer[16];
char* valueBuffer;
int valueBufferSize;
}
SGFParseContext;
typedef struct _tagSGFParseContext
{
void* pUserData;
int treeIndex;