當你的程式之間需要傳遞資料時,你會怎麼做?譬如說設定檔資料,使用一個最簡單的型式:
『id := value』
那如果設定值很複雜又有層次呢?我甚至還看過透過網路把整個二進位資料結構memory複製到網路去,由接受端宣告一個一模一樣的資料結構在memcpy......(當然,看到這種程式要我處理我會有想自殺的感覺...)。
其實XML是一個很好的規範,他的可擴充性讓程式可以自己去處理自己要的資料,也不會因為多了一個欄位讓天下大亂(我的世界就處在混亂中...ㄏㄏ)。但是要你面對XML這類的複雜結構時,我想很多人看到就感到害怕吧。還好這個世界甚麼不多,Open Source的library最多...ㄏㄏ,Expat是一個很不錯的XML parser library,原則上他是一個C library,而且可以在許多平台使用(好啦...有win32版本...不過我沒用過),Expat是一個stream導向的parser library,也就是說他會從data buffer一直讀進資料然後遇到不同的狀態改變做不同的處理。
經過一番google搜尋後發現Expat的文章不多,只有看到一篇在xml.com的介紹文章,當然expat也有不少wrapper library,譬如Python、Perl、Tcl、C++....等等,你可以直接去使用你習慣的語言的wrapper。這篇文章只是抱持著研究的精神的小小介紹,這裡我們使用C++語言來實作一個簡單的xml parser吧。
使用Expat很簡單,首先當然你必須include相關的header file,Expat只會將兩個header file放進你的系統,分別是expat.h與expat_external.h(這裡以Expat2.0.0為例)。其實你只要加入expat.h,而 expat_external.h會自動被加入。所以當然你必須有:
#include
在你要使用Expat library的程式碼中。因為這裡我使用C++語言,因此我定義一個自己的類別"MyParser"
class MyParser
{
public:
MyParser(string fname);
virtual ~MyParser();
}
先不要緊張,這目前是一個do-nothing的類別,我在建構式的地方傳入一個XML檔案名稱的參數,我也將parser的動作在初始階段完成,讓我們看看建構式的內容。Expat需要一個XML_Parser的instance在整個parsing階段,因此我們第一步便要宣告一個 XML_Parser然後建立它:
XML_Parser parser;
parser = XML_ParserCreate(NULL);
if (!parser) {
cout << "create XML_Parser error" << endl;
return 0;
}
Expat使用callback function來處理狀態改變的動作,所以當你需要對某些tag做處理的時候你必須要設定相關的處理函式,這裡我們只對最簡單的型式做處理,因此我只設定start tag、end tag與default handler:
::XML_SetUserData(parser, this);
::XML_SetElementHandler(parser, expatStart, expatEnd);
::XML_SetDefaultHandler(parser, expatHandler);
你可以使用this指標將自己傳給expat當成UserData;再來我們必須在類別內宣告相關的member function,因此我們的類別便宣告成:
class MyParser
{
public:
MyParser(string fname);
virtual ~MyParser();
static void expatStart(void *data, const XML_Char *el, const XML_Char **attr);
static void expatEnd(void *data, const XML_Char *el);
static void expatHandler(void *data, const XML_Char *s, int len);
}
你可以看到為了讓Expat的C library當成callback function我們將其宣告成static的member function,而當你要用類別內的成員變數或函式時,你不能直接呼叫或使用(沒忘記static吧),因此這個時候我們在 XML_SetUserData(parser, this)做的動作便派上用場,你可以看到expatStart、expatEnd與expatHandler中第一個不定指標參數void *data便會是你在XML_SetUserData時傳入的第二個參數。因此假設你在這3個callback function中要使用一個類別函式"do_something()"時,你可以這樣做:
((MyParser *)data)->do_something();
這樣一來,我們基本的宣告便完成,可以開始餵資料給XML_Parser了
file = fopen(fname.c_str(), "r");
do {
len = fread(buf, 1, sizeof(buf), file);
done = len < sizeof(buf);
if (!XML_Parse(parser, buf, len, done))
done = 1;
} while (!done);
::XML_ParserFree(parser);
fclose(file);
當然最後不要忘了釋放掉我們宣告的parser與關閉檔案。一個最簡單的Expat動作流程就是這麼簡單......
讓我們看看這3個callback function裏面的作法;首先我們先看一個簡單的XML檔案如左:
這個簡單的XML範例為最基本的XML,它沒有DTD宣告也沒有CDATA之類的,只單純的將資料結構化罷了。那Expat的callback function怎麼運作呢?原則上它幫你分好資料與tag的分別,至於你要如何處理資料或是tag屬性你要自己在callback function完成。舉例來說,我們的expatStart函式便會在每次Expat遇到起始tag時去呼叫,而expatEnd就會在遇到結束的 tag時被呼叫,中間的資料便會呼叫我們的expatHandler。
因為XML是一個巢狀結構化的資料,因此必須記住你還未處理完的tag,這裡我們可以用一個簡單的STL vector來記住tag,簡單的push_back與pop_back就可以幫你記住tag。
因此我們宣告一個
vector
這樣便可以在expatStart的地方將tag給push_back而在expatEnd的地方給它pop_back出來
void MyParser::expatStart(void *data, const XML_Char *el, const XML_Char **attr)
{
string tmp = e1;
((MyParser *)data)->working_tag.push_back(tmp);
}
void MyParser::expatEnd(void *data, const XML_Char *el)
{
((MyParser *)data)->working_tag.pop_back();
}
這樣我們便可以知道目前處理的tag是那一個。你可以發現expatStart的函式多了一個參數,那個便是在起始tag裏面的參數
for (int i=0; attr[i]; i+=2)
{
string attr_name = attr[i];
string attr_value = attr[i+1];
/* 對你的參數做你希望的處理 */
}
最後是我們的預設處理函式ExpatHandler的部份,因為我們已經將目前處理的tag紀錄起來,因此我們可以由vector得到作用中的tag
void MyParser::expatHandler(void *data, const XML_Char *s, int len)
{
if (((MyParser *)data)->working_tag.size() > 0)
{
vector
iter--;
/* 對你每一個tag值(*iter)去作你希望的動作 */
}
}
這裡我們對於不是在start tag與end tag裏面的值不去處理,不過真的完善的XML parser必須考慮多一點點,例如此例中的會被當成不在起始與結束tag之間的值而被我們忽略。處理函式的第二個參數是要處理的值,不過注意的是它不會是以null結束的字串給你,但是第三個參數將告訴你他的長度,因此你可以很簡單的處理它
string str = s;
str = str.substr(0, len);
另一個要注意的是Expat會以每一行去處理一次,也就是說如果XML資料為了讓人容易閱讀而使用了換行字元與空白字元,Expat都會將其視為資料。這邊我們可以用最粗糙的方式處理它
int i;
for (i=0; i
break;
str = str.substr(i, str.length()-i);
這樣我們把特殊自元處理掉後就可以得到真正的值去處理了。一個XML parser用Expat處理起來是不是非常簡單呢,下次你希望用到XML的資料時,不妨試試Expat吧。"
没有评论:
发表评论