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));
    }

}

Libcurl 的使用範例

/* 
 * 這個例子說明如何寫Libcurl中的Callback 程式,這Callback將把 URL回傳的資料,放到記憶體當中。
 * Shows how the write callback function can be used to download data into a
 * chunk of memory instead of storing it in a file.
 * 
 */ 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#include <curl/curl.h>
 
struct MemoryStruct {
  char *memory;
  size_t size;
};
/*
  這個例子的就是存取一個URL,傳回內容與CONTENT SIZE。
    
  size_t callback_function( void *ptr, size_t size, size_t nmemb, void *stream);
  這函數將在libcurl接收到數據後被調用,用來儲存http送回的資料。
  程式在處理回傳資料最難的就是不知道回傳的資料量有多少? 所以記憶體也不知道要需告多大。所以就設計了這個Callback。
  *ptr 是一快記憶體空間,裡面放著 http 傳回的資訊。
  size :說明 Contents 所指到的記憶體有多大?
  memb :這是一個有趣的參數,因為網路上每個字的單位有多大? 所以利用這個變數來做通知 Callback。實際的記憶體SIZE = size * memb
    userp:這個參數主要回傳一個應用程式可以使用的資料結構,
            struct MemoryStruct {
            char *memory;
            size_t size;
            };
    *memory 之中放著由 ptr 送進來的 HTTP 回傳的資料。
    size 是 memory 的 SIZER。
    CURLOPT_WRITEDATA用於表明CURLOPT_WRITEFUNCTION函數中的stream指針的來源。
    如果你沒有通過CURLOPT_WRITEFUNCTION屬性给easy handle設置回調函數,libcurl會提供一個預設的回調函數,它只是簡單的將接收到的數據印到標准輸出。
    你也可以通過 CURLOPT_WRITEDATA屬性之中的Callback,將資訊寫到記憶體之中,傳給Libcurl,然後透過 HTTP傳送出去。
*/
static size_t
WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp)
{
    size_t realsize = size * nmemb;
    struct MemoryStruct *mem = (struct MemoryStruct *)userp;
    
    char *ptr = realloc(mem->memory, mem->size + realsize + 1);
    if(ptr == NULL) {
        /* 
        記憶體不足所以拿不到記憶體,out of memory! 
        */
        printf("not enough memory (realloc returned NULL)\n");
        return 0;
    }
    
    mem->memory = ptr;
    memcpy(&(mem->memory[mem->size]), contents, realsize);
    mem->size += realsize;
    mem->memory[mem->size] = 0;
    
    return realsize;
}
 
int main(void)
{
    CURL *curl_handle;
    CURLcode res;
    
    struct MemoryStruct chunk;
    
    chunk.memory = malloc(1);  /* will be grown as needed by the realloc above */ 
    chunk.size = 0;    /* no data at this point */ 
    
    curl_global_init(CURL_GLOBAL_ALL);
 
    /* 
        將curl Library進行初始化,init the curl session 
    */
    curl_handle = curl_easy_init();
 
    /* 
        設定要存取的URL 
    */
    curl_easy_setopt(curl_handle, CURLOPT_URL, "https://www.example.com/");
 
    /* send all data to this function  */ 
    
    curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
 
    /* 
       we pass our 'chunk' struct to the callback function
       chunk 就會傳給 WriteMemoryCallback 之中的第四個參數 userp 
    */
    
    curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)&chunk);
 
    /* 
       這一行如果不寫,有時有問題,有的SERVER會有問題,所以務必要寫
    */ 
    curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "libcurl-agent/1.0");
 
    /* get it down ! */ 
    res = curl_easy_perform(curl_handle);
 
    /* check for errors */ 
    if(res != CURLE_OK) {
        fprintf(stderr, "curl_easy_perform() failed: %s\n",
        curl_easy_strerror(res));
    }
    else {
        /*
         * Now, our chunk.memory points to a memory block that is chunk.size
         * bytes big and contains the remote file.
         *
         * Do something nice with it!
        */ 
        printf("%lu bytes retrieved\n", (unsigned long)chunk.size);
    }
    /* cleanup curl stuff */ 
    curl_easy_cleanup(curl_handle);
    free(chunk.memory);
    /* we're done with libcurl, so clean it up */ 
    curl_global_cleanup();
    return 0;
}