程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 基本的HTML文本解析器的設計和實現(C/C++源碼)

基本的HTML文本解析器的設計和實現(C/C++源碼)

編輯:C++入門知識

作者:莊曉立 (liigo)

日期:2011-1-19

原創鏈接:http://blog.csdn.net/liigo/archive/2011/01/19/6153829.aspx

轉載請保持本文完整性,並注明出處:http://blog.csdn.net/liigo

關鍵字:HTML,解析器(Parser),節點(Node),標簽(Tag)

這是進入2011年以來,本人(liigo)“重復發明輪子”系列博文中的最新一篇。本文主要探討如何設計和實現一個基本的HTML文本解析器。

眾所周知,HTML是結構化文檔(Structured Document),由諸多標簽(<p>等)嵌套形成的著名的文檔對象模型(DOM, Document Object Model),是顯而易見的樹形多層次結構。如果帶著這種思路看待HTML、編寫HTML解析器,無疑將導致問題復雜化。不妨從另一視角俯視HTML文本,視其為一維線狀結構:諸多單一節點的順序排列。仔細審視任何一段HTML文本,以左右尖括號(<和>)為邊界,會發現HTML文本被天然地分割為:一個標簽(Tag),接一段普通文字,再一個標簽,再一段普通文字…… 如下圖所示:

HTML文本的單維結構圖

標簽有兩種,開始標簽(如<p>)和結束標簽(</p>),它們和普通文字一起,順序排列,共同構成了HTML文本的全部。

為了再次簡化編程模型,我(liigo)繼續將“開始標簽”“結束標簽”“普通文字”三者統一抽象歸納為“節點”(HtmlNode),相應的,“節點”有三種類型,要麼是開始標簽,要麼是結束標簽,要麼是普通文字。現在,HTML在我們眼裡更加單純了,它就是“節點”的線性順序組合,是一維的“節點”數組。如下圖所示:HTML文本 = 節點1 + 節點2 + 節點3 + ……

HTML是“節點”的線性順序組合

在正式編碼之前,先確定好“節點”的數據結構。作為“普通文字”節點,需要記錄一個文本(text);作為“標簽”節點,需要記錄標簽名稱(tagName)、標簽類型(tagType)、所有屬性值(props);另外還要有個類型(type)以便區分該節點是普通文字、開始標簽還是結束標簽。這其中固然有些冗余信息,比如對標簽來說不需要記錄文本,對普通文字來說又不需要記錄標簽名稱、屬性值等,不過無傷大雅,簡潔的編程模型是最大的誘惑。用C/C++語言語法表示如下:

view plaincopy to clipboardprint?
enum HtmlNodeType  
{  
    NODE_UNKNOWN = 0,  
    NODE_START_TAG,  
    NODE_CLOSE_TAG,  
    NODE_CONTENT,  
};  
enum HtmlTagType  
{  
    TAG_UNKNOWN = 0,  
    TAG_A, TAG_DIV, TAG_FONT, TAG_IMG, TAG_P, TAG_SPAN, TAG_BR, TAG_B, TAG_I, TAG_HR,   
};  
struct HtmlNodeProp  
{  
    WCHAR* szName;  
    WCHAR* szValue;  
};  
#define MAX_HTML_TAG_LENGTH (15)  
struct HtmlNode  
{  
    HtmlNodeType type;  
    HtmlTagType  tagType;  
    WCHAR tagName[MAX_HTML_TAG_LENGTH+1];  
    WCHAR* text;  
    int propCount;  
    HtmlNodeProp* props;  
}; 
enum HtmlNodeType
{
 NODE_UNKNOWN = 0,
 NODE_START_TAG,
 NODE_CLOSE_TAG,
 NODE_CONTENT,
};
enum HtmlTagType
{
 TAG_UNKNOWN = 0,
 TAG_A, TAG_DIV, TAG_FONT, TAG_IMG, TAG_P, TAG_SPAN, TAG_BR, TAG_B, TAG_I, TAG_HR,
};
struct HtmlNodeProp
{
 WCHAR* szName;
 WCHAR* szValue;
};
#define MAX_HTML_TAG_LENGTH (15)
struct HtmlNode
{
 HtmlNodeType type;
 HtmlTagType  tagType;
 WCHAR tagName[MAX_HTML_TAG_LENGTH+1];
 WCHAR* text;
 int propCount;
 HtmlNodeProp* props;
};

具體到編寫程序代碼,要比想象中容易的多。編碼的核心要點是,以左右尖括號(<和>)為邊界自然分割標簽和普通文字。左右尖括號之間的當然是標簽節點(開始標簽或結束標簽),左尖括號(<)之前(直到前一個右尖括號或開頭)、右尖括號(>)之後(直到後一個左尖括號或結尾)的顯然是普通文字節點。區分開始標簽或結束標簽的關鍵點是,看左尖括號(<)後面第一個非空白字符是否為/。對於開始標簽,在標簽名稱後面,間隔至少一個空白字符,可能會有形式為“key1=value1 key2=value2 key3”的屬性表,關於屬性表,後文有專門的函數負責解析。此外有一點要注意,屬性值一般有引號括住,引號內出現的左右尖括號應該不被視為邊界分隔符。

下面就是負責把HTML文本解析為一個個節點(HtmlNode)的核心代碼(不足百行,夠精簡吧):

view plaincopy to clipboardprint?
void HtmlParser::ParseHtml(const WCHAR* szHtml)  
{  
    m_html = szHtml ? szHtml : L"";  
    freeHtmlNodes();  
    if(szHtml == NULL || *szHtml == L) return;  
    WCHAR* p = (WCHAR*) szHtml;  
    WCHAR* s = (WCHAR*) szHtml;  
    HtmlNode* pNode = NULL;  
    WCHAR c;  
    bool bInQuotes = false;  
    while( c = *p )  
    {  
        if(c == L")  
        {  
            bInQuotes = !bInQuotes;  
            p++; continue;  
        }  
        if(bInQuotes)  
        {  
            p++; continue;  
        }  
        if(c == L<)  
        {  
            if(p > s)  
            {  
                //Add Text Node  
                pNode = NewHtmlNode();  
                pNode->type = NODE_CONTENT;  
                pNode->text = duplicateStrUtill(s, L<, true);  
            }  
            s = p + 1;  
        }  
        else if(c == L>)  
        {  
            if(p > s)  
            {  
                //Add HtmlTag Node  
                pNode = NewHtmlNode();  
                while(isspace(*s)) s++;  
                pNode->type = (*s != L/

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved