2021年4月24日 星期六

使用 libexpat 來解析XML文件

expat 是使用C語言所寫的XML解析器,採用Stream-oriented的方式解析XML 文件,在我使用的Ubunto環境下,可以用一下方式安裝套件:

# sudo apt-get install expat

expat 大約有二十多個供使用的Library Call,在此只介紹要用的幾個基本函數。

程式編譯後,連結的程式館是 -lexpat ,程式一開始要加入以下的 include file:

#include "expat.h"

之後在調用之前,要做以下宣告與初始化動作:

XML_Parser XML_ParserCreate(const XML_Char *encoding)

  XML_ParserCreate()會建立一個 XML_Parser 的資料結構然後回傳給一個變數,之後所有的操作都要帶上這個變數。他的參數 encoding 是一個字串說明這個XML要處理的編碼方式。目前已經知道的有以下四種方式:
  - US-ASCII
  - UTF-8
  - UTF-16
  - ISO-8859-1
  
  但是因為XML通常會指定文件的編碼方式,所以就不用雞婆,直接用 NULL 即可。所以我們最常見的作法就是採用如下宣告:

  XML_Parser xml = XML_ParserCreate(NULL);

  由於XML處理程式使用 Stream 架構,所以要先掛上一個Callback,也就是說我們要處理的XML是哪一段。

    XML_SetElementHandler(XML_Parser p,
                      XML_StartElementHandler start,
                      XML_EndElementHandler end);

    typedef void    (*XML_StartElementHandler)(void *userData,
                            const XML_Char *name,
                            const XML_Char **atts);

    typedef void    (*XML_EndElementHandler)(void *userData,
                             const XML_Char *name);

   XML_SetElementHandler() 就是用來掛上兩個 Callback,一個程式叫做 start() 是開始找出XML要處理的字串,一個 end() 用來找出結束的字串。 srat() 與 end() 都是Function pointer,指到兩個獨立的處理程式(Function)。但是在呼叫 XML_SetElementHandler() 設定Callback之前,我們還要為這兩個 Callback程式中的 userData 進行設定。
   還有一個變數叫做 Depth,這是指XML的深度。就是第一層、第二層的意思。

   設定 userData 的程式是  
   
   XML_SetUserData(XML_Parser parser, void *p) 
   
   這個呼叫會自動將 *p 所指到的記憶體,自動變成 *userData,而成為 start() 與 end() 的 data stream input。
   start() callback 的功能就是找出 XML 自串的起始,然後將字串當中的字放到 **atts 之中回傳。  end() 的功能就是找出結尾字,也放到 *name 之中回傳。
     
   也許用例子來說明比較容易,看下面看這個例子:
   <server>
        <client ip="114.36.9.181" lat="25.0478" lon="121.5318" isp="Chunghwa Telecom" isprating="3.7" rating="0" ispdlavg="0" ispulavg="0" loggedin="0" country="TW"/>
   </server>
   
   我們想要讀出這串XML裏面的值。
   先將我們要處理的資料結構餵到 XML expat 當中。

   XML_SetUserData(xml, p_client);

   這個 p_client 是如此宣告的
   
    struct client_info *p_client ;

    struct client_info
    {
        char   ip[MAX_IPADDRESS_STRLEN];
        double lat;
        double lon;
        char   isp[MAX_ISP_NAME];
    };   

    對應到上面的XML,可以知道我們要出 ip 的值,填到上面的資料結構中,程式就可以繼續處理了。這時候要先寫 start 與 end 這兩個Callback。

Start_Callback()因為是Call-back,所以要使用預定的帶入的三個參數:
- void          *userData   : 就是上面的的 XML_SetUserData 設定的資料結構(就是 p_client)。
- const char    *tagname    : tagname 是一個要處理XML的標頭字串,以上面的例子來說,就是"client"。
- const char    **atts      : 這個一個Array。Call-back程式把字串中的內容Parsing出來之後,將結果放到Array當中。

注意這邊 tagname 與 atts 都是 Const char,說明這是一個Global Variable,必須在外圍先做宣告。

static void XMLCALL 
Start_Callback(void *userData, const char *tagname, const char **atts)
{
    int i;

    if (strcmp(tagname, "client") == 0) {

        struct client_info *p_client = (struct client_info *)userData;

        for (i = 0; atts[i]; i += 2) {
            if (strcmp(atts[i], "ip") == 0)
                strcpy(p_client->ip, atts[i + 1]);
            if (strcmp(atts[i], "isp") == 0)
                strcpy(p_client->isp, atts[i + 1]);
            if (strcmp(atts[i], "lat") == 0)
                p_client->lat = atof(atts[i + 1]);
            if (strcmp(atts[i], "lon") == 0)
                p_client->lon = atof(atts[i + 1]);
        }
    }
}

End_Callback() 與 Start_Callback() 一樣,用來處理XML的結尾,通常這個結尾都很簡單,所以幾乎沒有甚麼好寫的。
這Call Back 有兩個參數: 
userData    : 就是之前設定好的XML Stream ;
name        : 只需要做的XML Tag,例如前面的例子,就是傳入 "client" 這個字串。

static void XMLCALL 
end_callback(void *userData, const char *tagname)
{
    depth--;  / *將處理這張XML的位置還原 */

    if (strcmp(tagname, "clinet") == 0) {

        /* 下面就是完成Pasing XML之後,做一堆處理這些XML的工作 */

        int     i;
        struct server_info *p_server = (struct server_info *)userData;

        p_server->distance = get_distance(client.lat, client.lon, p_server->lat, p_server->lon);

        for (i = 0; i < MAX_CLOSEST_SERVER_NUM; i++) {

            if ( servers[i].url[0] == 0 ) {

                break;
            }
        }

        if ( i == MAX_CLOSEST_SERVER_NUM ) {

            for (i = 0; i <MAX_CLOSEST_SERVER_NUM; i++) {

                if (servers[i].distance >  p_server->distance) {
                    break;
                }
            }
        }
        if (i != MAX_CLOSEST_SERVER_NUM)
            memcpy(&servers[i], p_server, sizeof(struct server_info));
        memset(p_server, 0, sizeof(struct server_info));
    }

}

沒有留言:

張貼留言