解釋文本文件是日常編程中太平常的一件事情了,一般來說,土鱉點的做法可以直接手寫 parser 用循環暴力地去 map 文本上的關鍵字從而提取相關信息,想省力一點則可以使用 tokenizer 或正則表達式之類的工具,無論怎樣,總的來說,手寫 parser 去解釋文本基本是件苦力活:寫出的代碼比較難重用,可讀性可維護性也差,要是設計的差點,哪天文本格式一變,以前辛苦寫的代碼馬上推倒重來,未嘗是新鮮事。
解救的方法是通過工具來生成 parser,這方面有好多比較出名的工具,比如 Lex, Yacc, ANTLR 等,它們的功能大同小異,都基於文法(用 EBNF 進行描述) 轉換成相應的 parser. 但這些工具使用起來通常比較麻煩:首先是生成的代碼可讀性不大好,再者如果要把這些代碼加入到工程中,通常都需要對生成的代碼加以修改,因此文法一旦發生變動,parser 需要再次生成,這些修改基本又都得再次重來,因此維護困難。
那么除了上述這些工具還有沒有別的解決方案呢?也許你可以嘗試一下 boost spirit。
以下內容基於 boost 1.37.0,spirit 的版本是 1806,比較老的一個版本,與最新版本相比,大概原理雖是差不多,但庫的目錄結構已有很大不同,知悉。
Spirit 是什么
簡單來說,Spirit 是一個 parser generator,功能與 Yacc,ANTLR 類似,且也是基於 EBNF 來描述文法,再基於文法生成 parser,但與前面這些工具相比,它最大的不同點在於它使用了 C++ 代碼來對文法進行描述,通過非常殘暴的模板編程技巧,在編譯階段就生成了相應的 parser。從使用者的角度來看,文法是用代碼進行描述的,因此它天生就能直接加入到你當前的工程中與現成代碼揉合在一起。
當然,Spirit 的文法在形式上 EBNF 還是有一點點的出入,比如說,用 ">>" 來連接不同表達式,表示重復的符號放在了表達式的前面等,這些都是受 c++ 語法的限制所做出的折衷,文法的語義其實未變。
初體驗
一個經典的整數四則運算如用 EBNF 來描述的話,可以寫成如下的形式:
    group       ::= '(' expression ')'
    factor      ::= integer | group
    term        ::= factor (('*' factor) | ('/' factor))*
    expression  ::= term (('+' term) | ('-' term))*
 
        其中 group, factor, term, expression 分別稱為一個 rule,用於表示怎么去匹配一條相應的文本,上述 EBNF 文法在 Spirit 中可以寫成如下形式:
    group       = '(' >> expression >> ')';
    factor      = integer | group;
    term        = factor >> *(('*' >> factor) | ('/' >> factor));
    expression  = term >> *(('+' >> term) | ('-' >> term));
 
        看起來語法好像差不多,只是運算符有些不同,需要指明的是,在 Spirit 中一個 rule 就是一個 parser 對象,一個 parser 對象包含了相應的語法規則使得該 parser 只能 parse 符合這些規則的文本,比如說,我們現在想 parse 出一組用逗號隔開的整數(CSV),則我們可以定義如下一個 rule:
boost::spirit::rule<> csv_int = int_p >> *(',' >> int_p);
 
        上述代碼中,int_p 是 Spirit 內建的一個 rule 或者說 parser,該 parser 專門用於 parse 一個整型(除了 int_p,Spirit 還內建了一系列用於 parse 其它基本數據類型的 parser, 具體列表參考這里)。parser 與 parser 通過 ">>" 運算符連接在一起后就組成了一個新的 parser,那么怎么來使用 csv_int 這個新生成的 parser 呢? Spirit 內建定義了一個函數,原型大概如下:
boost::spirit::parse_info<> parse(const char* text, parser, separator);
 
        通過調用 parse() 函數傳入需要 parse 的文本與相應的 parser 就能對該文本進行相應的解釋,返回結果會指明 parse 的過程是否成功了,及如果出錯,在哪個位置出錯了。
Semantic actions
前面定義的 csv_int 這個 parser 雖然定義了文法,但它基本沒做什么事情,只能用來檢查一下某段文本是不是一組逗號隔開的整型,功能顯然太弱了,因為通常來說,我們是需要從文本中提取出數據來的,因此 parse 的過程需要支持某些動作,我們需要 parser 在 parse 到某些內容時,能夠執行用戶指定的行為動作,在 Spirit 中,這個些動作就叫作 semantic action.
Semantic action 是屬於一個 parser 的,它的意義在於指明當該 parser 執行成功了之后,要執行哪些操作,而這些操作是由用戶指定的。我們可以通過如下方式將一個 semantic action 與一個 parser 聯系起來:
parser[func];
 
        至於 func 的原型,當然是有要求的,而且這個要看具體的 parser, 比如說 int_p 這樣的 parser,它就只能接受 void func(const int val); 這樣的函數,很簡潔的語法!現在我們來在將前面用於解釋一組整型的代碼中加入一個新功能,在 parse 完每一個整型后,我們將得到的數據保存下來。
#include <vector>
#include <boost/spirit.hpp>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix_core.hpp>
#include <boost/spirit/include/phoenix_object.hpp>
std::vector<int> g_output;
int on_parse_int(const int val)
{
   g_output.push_back(val);
}
int main()
{
   boost::spirit::rule<> int_csv_rule = int_p[on_parse_int] >> *(',' >> int_p[on_parse_int]);
   boost::spirit::parse("2,3,4", int_csv_rule);
   return 0;
}
 
        semantic action 的實現依賴於 boost 里另一個名聲顯赫的函數式模板庫:phoenix, 上面的例子只是一個簡單示范,未及冰山一角,spirit 其實還支持用 lambda 來寫回調函數,以及用閉包來在不同的 rule 之間傳遞用戶定義的上下文信息等,功能很強大,有興趣的讀者可以參考下這里相對完整點的一個例子,它實現了一個簡單的四則運算及基本的函數調用。
生成的 Parser 的類型
Parser 是整個 Spirit 庫的核心功能所在,那么 Spirit 生成的 parser 是怎么進行工作呢? 結論是,spirit 所生成的 parser 就是所謂的 LL recursive decent parser,因此 parse 的時候是從左往右掃描輸入,而對 parser 中的文法,采取先左后右的順序進行匹配的,因此左遞歸之類的問題需要使用者自己消除,對如下一個例子:
rule<> rule1 = (int_p >> ',' >> int_p) | real_p;
 
        rule1 在 parse 文本時,會優先匹配 (int_p >> ',' >> int_p),如若失敗,則再去匹配 real_p。
優缺點
因為使用該庫的時間還不是很長,初步上手的感覺,優點上個人覺得有如下幾點:
- 使用非常方便,尤其當要 parse 一些不太復雜的文本時,寫代碼的效率很高。
 - 生成的 parser 執行效率也很好。
 
結論就是:很好很強大,但與此同時,缺點也明顯:
- 錯誤提示不夠友好。當一段文本格式上有錯誤時,spirit 直接從出錯的地方返回但卻不提供相應的錯誤信息,上層的代碼只知道在哪里出了錯,卻不知道是因什么出了錯,因此很難生成一個有意義的錯誤提示。
 - 該庫的實現大量使用了模板及符號重載,代碼寫起來很酷炫,但是一旦出錯,錯誤提示基本沒有包含太多有意義的信息,因此調試起來很痛苦,尤其是在使用不夠熟練的情況下,因此個人建議在使用 Spirit 時,最好把任務進行適當分解,每完成一個小的任務就先測試確認它功能正常穩定再進行下一步,不要一下子寫一大堆代碼,先不說功能如不正常難以調試,甚至編譯錯誤時,找到出錯的地方都困難。
 - 該庫在實現上嚴重依賴模板元編程,使用了諸多如 expression template 這樣的 coding idiom,parser 的生成實際上是在編譯階段完成,因此當你寫的 rule 很復雜時,編譯時間會很長,真的很長。
 
【參考】
http://boost-spirit.com/distrib/spirit_1_8_3/libs/spirit/doc/quick_start.html
 http://en.highscore.de/cpp/boost/
