用C语言实现SGF格式围棋棋谱解析器
這是本人(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定義如下(參見http://www.red-bean.com/sgf/sgf4.html#ebnf-def):
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文本,先讓大家有一個感性認識:
(;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”結構,用于保存解析器工作期間的相關數據:
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;
相應的還有初始化和清理SGFParseContext結構的函數,initSGFParseContext, cleanupSGFParseContext,皆不是本解析器的關鍵,略過不提。
接著我(liigo)設計了五個回調函數的函數原形:
typedef void (*PFN_ON_TREE) (SGFParseContext* pContext, const char* szTreeHeader, int treeIndex); typedef void (*PFN_ON_TREE_END) (SGFParseContext* pContext, int treeIndex); typedef void (*PFN_ON_NODE) (SGFParseContext* pContext, const char* szNodeHeader); typedef void (*PFN_ON_NODE_END) (SGFParseContext* pContext); typedef void (*PFN_ON_PROPERTY) (SGFParseContext* pContext, const char* szID, const char* szValue);
這五個回調函數,將分別在解析器解析到“樹開始”“樹結束”“節點開始”“節點結束”“遇到屬性”時,由解析器調用。解析器調用每個回調函數時,都會傳入必需的參數,供回調函數即時取用。
下面正式開始解析工作。整個解析器被分為 parseProperty, parseNode, parseNodeSequence, parseGameTree, parseSGF 幾大部分順序解析,屬于至底向上的分析實現模式。這幾大部分,也分別對應著SGF的EBNF定義中的某一項。所有解析函數都接收參數 const char* szCollection, int fromPos,之前的解析函數將決定后續解析函數的起始解析位置。
第一步,解析屬性(parseProperty)。此處關鍵的是要定位到屬性值(szValue)開始和結束符號“[”和“],兩者之間的是屬性值,“[”之前的則是屬性標識(szID)。由于[和]之間可能存在轉義字符“\”,不能簡單地搜索字符“]”,必須花相當篇幅的代碼處理轉義字符(我用局部變量in_escape記錄轉義狀態并進行分別處理)。此外要為提取出的屬性標識和屬性值分配足夠的存儲空間,以便傳遞到用戶回調函數,前者不會太長使用靜態分配,后者變長則使用動態分配(同時自動預分配存儲空間,緩存,避免頻繁申請內存)。代碼如下:
//Property: id[value] int parseProperty(SGFParseContext* pContext, const char* szCollection, int fromPos) { const char* szFromPos; int lindex; int nIDBufferSize = sizeof(pContext->idBuffer) - 1; assert(szCollection && fromPos >= 0); szFromPos = szCollection + fromPos; lindex = findchar(szFromPos, -1, '['); assert(lindex > 0 && lindex < nIDBufferSize); if(lindex > 0 && lindex < nIDBufferSize) { memcpy(pContext->idBuffer, szFromPos, lindex); pContext->idBuffer[lindex] = '\0'; if(isTextPropertyID(pContext->idBuffer)) { //parse the text or simple-text value, consider the '\' escape character const char* s = szFromPos + lindex + 1; char c; int in_escape = 0; int valuelen = 0; getEnoughBuffer(pContext, 1024); pContext->valueBuffer[0] = '\0'; while(1) { c = *s; assert(c); if(!in_escape) { if(c == '\\') { in_escape = 1; } else if(c == ']') { break; } else { getEnoughBuffer(pContext, valuelen + 1); pContext->valueBuffer[valuelen++] = c; } } else { //ignore the newline after '\' if(c != '\r' && c != '\n') { getEnoughBuffer(pContext, valuelen + 1); pContext->valueBuffer[valuelen++] = c; } else { char nc = *(s+1); if(nc) { if((c=='\r' && nc=='\n') || (c=='\n' && nc=='\r')) s++; } } in_escape = 0; } s++; } getEnoughBuffer(pContext, valuelen + 1); pContext->valueBuffer[valuelen] = '\0'; if(pContext->pfnOnProperty) pContext->pfnOnProperty(pContext, pContext->idBuffer, pContext->valueBuffer); return (s - szCollection + 1); } else { int rindex = findchar(szFromPos, -1, ']'); int nNeedBufferSize = rindex - lindex - 1; assert(rindex >= 0); getEnoughBuffer(pContext, nNeedBufferSize); memcpy(pContext->valueBuffer, szFromPos + lindex + 1, nNeedBufferSize); pContext->valueBuffer[nNeedBufferSize] = '\0'; if(pContext->pfnOnProperty) pContext->pfnOnProperty(pContext, pContext->idBuffer, pContext->valueBuffer); return (fromPos + rindex + 1); } } return -1; }
第二步,解析節點(parseNode)。分號“;”跟后面N個屬性,一個while循環調用parseProperty()逐個解析屬性即可:
//Node: ; {property} int parseNode(SGFParseContext* pContext, const char* szCollection, int fromPos) { const char* szFromPos = szCollection + fromPos; assert(fromPos >= 0); //assert(szFromPos[0] == ';'); if(pContext->pfnOnNode) pContext->pfnOnNode(pContext, szFromPos); if(szFromPos[0] == ';') { fromPos++; szFromPos++; } while(1) { fromPos += skipSpaceChars(szFromPos, NULL); if(szCollection[fromPos] == '\0' || findchar(";)(", -1, szCollection[fromPos]) >= 0) break; fromPos = parseProperty(pContext, szCollection, fromPos); szFromPos = szCollection + fromPos; } return fromPos; }
第三步,解析節點序列(parseNodeSequence)。節點的順序排列,至少有一個節點,后面可能還有0個或多個節點。仍然是一個while循環搞定:
//NodeSequence: node{node} int parseNodeSequence(SGFParseContext* pContext, const char* szCollection, int fromPos) { const char* szFromPos = szCollection + fromPos; assert(fromPos >= 0); //assert(szFromPos[0] == ';'); while(1) { fromPos = parseNode(pContext, szCollection, fromPos); fromPos += skipSpaceChars(szFromPos, NULL); szFromPos = szCollection + fromPos; if(szFromPos[0] != ';') { if(pContext->pfnOnNodeEnd) pContext->pfnOnNodeEnd(pContext); break; } } return fromPos; }
第四步,解析樹(parseGameTree)。樹是一個嵌套結構,最外層是一對括號“(”“)”,里面是N個節點序列或N個嵌套的子樹。仍然用一個while循環搞定,遇到“(”則遞歸調用parseGameTree()解析樹或其子樹,否則調用parseNodeSequence()解析節點序列。代碼如下:
//GameTree: ( {[NodeSequence]|[GameTree]} ) //old GameTree: ( NodeSequence {GameTree} ) int parseGameTree(SGFParseContext* pContext, const char* szCollection, int fromPos) { char c; const char* szFromPos = szCollection + fromPos; assert(fromPos >= 0); assert(szFromPos[0] == '('); pContext->treeIndex++; if(pContext->pfnOnTree) pContext->pfnOnTree(pContext, szFromPos, pContext->treeIndex); fromPos++; szFromPos++; fromPos += skipSpaceChars(szFromPos, NULL); c = szCollection[fromPos]; while(1) { if(c == '(') fromPos = parseGameTree(pContext, szCollection, fromPos); else fromPos = parseNodeSequence(pContext, szCollection, fromPos); szFromPos = szCollection + fromPos; fromPos += skipSpaceChars(szFromPos, NULL); c = szCollection[fromPos]; if(c == ')') { if(pContext->pfnOnTreeEnd) pContext->pfnOnTreeEnd(pContext, pContext->treeIndex); pContext->treeIndex--; break; } } return (fromPos + 1); }
第五步,最后一步了,解析整個SGF文本內容(parseSGF)。這是對外公開的核心接口。N個樹的順序排列,好辦呀,循環調用parseGameTree()順序解析各個樹不就OK了?代碼如下:
//SGFCollection: GameTree {GameTree} int parseSGF(SGFParseContext* pContext, const char* szCollection, int fromPos) { const char* szFromPos = szCollection + fromPos; assert(fromPos >= 0); assert(szFromPos[0] == '('); pContext->treeIndex = -1; while(1) { fromPos = parseGameTree(pContext, szCollection, fromPos); fromPos += skipSpaceChars(szFromPos, NULL); szFromPos = szCollection + fromPos; if(szFromPos[0] != '(') break; } return fromPos; }
測試代碼:
int main(int argc, char *argv[]) { char* s; int x; SGFParseContext Context; //initSGFParseContext(&Context, onTree, onTreeEnd, onNode, onNodeEnd, onProperty, NULL); initSGFParseContext(&Context, onTree2, onTreeEnd2, onNode2, onNodeEnd2, onProperty2, NULL); //test parse property: { s = "AB[cdef]X[xyz]"; printf("\ntest parse property: ----- \n"); x = parseProperty(&Context, s, 0); x = parseProperty(&Context, s, 8); s = "C[ab\\]cd]"; x = parseProperty(&Context, s, 0); } //test parse node: { s = ";A[a]BB[bb]C[]"; printf("\ntest parse node: ----- \n"); x = parseNode(&Context, s, 0); s = ";A[a];BB[bb]C[]"; x = parseNode(&Context, s, 0); x = parseNodeSequence(&Context, s, 0); } //test parse tree: { printf("\ntest parse tree: ----- \n"); s = "(;A[a](;C[c](X[x])Z[z]);D[d](;E[e](F[ff])))"; x = parseGameTree(&Context, s, 0); } #if 1 //parse real sgf file: { int len = 0; void* data = NULL; FILE* pfile = fopen("d:\\x.txt", "r"); printf("\n---------- test parse real sgf file: -------- \n"); if(pfile) { fseek(pfile, 0, SEEK_END); len = ftell(pfile); assert(len > 0); fseek(pfile, 0, SEEK_SET); data = malloc(len); assert(data); fread(data, 1, len, pfile); parseSGF(&Context, data, 0); fclose(pfile); pfile = NULL; } } #endif { char c; printf("\n----- any key to exit: ----- \n"); fflush(stdout); scanf("%c", &c); } }
總結:整個SGF解析器結構比較清晰,只要按照EBNF定義,按部就班地逐步處理即可,不是特別復雜。但由于牽涉到文本、指針、遞歸,有許多細節需要注意。各位朋友不妨評估一下,自己需要花費多久可以寫出類似這樣一個SGF解析器?如果時間充裕,也不妨真的動手寫一下,看看是否眼高手低呢?所謂的“重復發明輪子”,并非絕對的毫無意義,至少可以鍛煉我的動手能力。
另外,有一個設計上的取舍,不知是較好還是較壞。所有的回調函數,目前都有一個 SGFParseContext* pContext ,而此前相同位置的參數是 void* pUserData。是后來考慮到回調函數可能需要訪問SGFParseContext中的相關數據(如在PFN_ON_NODE中讀取treeIndex),為了方便用戶使用才引入pContext參數(用戶也可以通過pUserData自行傳入pContext,終究是多了一步)。目前的做法,似乎暴露了解析器內部結構(SGFParseContext),又似乎增強了回調函數的穩定性和擴展性(即使不改變函數原形也能通過pContext提供額外參數)。
雖然這個SGF解析器已應用到開源軟件“M8圍棋譜”(http://code.google.com/p/m8weiqipu/)中,并初步達到了實用目的,但并不能保證該解析器已達到工業強度,其實有不少情況尚未測試到,必然會有疏忽錯漏之處,誠請各位朋友批評指正。
另注,考慮到與現有SGF格式文件的兼容性,對SGF規范中的EBNF稍做了一定擴展。
完整源代碼請參見:
http://code.google.com/p/m8weiqipu/source/browse/trunk/sgf.h
http://code.google.com/p/m8weiqipu/source/browse/trunk/sgf.c
轉載于:https://www.cnblogs.com/fortest/archive/2009/09/06/2056928.html
總結
以上是生活随笔為你收集整理的用C语言实现SGF格式围棋棋谱解析器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 生成word_Word生成员工信息表,每
- 下一篇: python 实数如何取整_从面试官角度