天天看點

使用xpath實作document.querySelector樣式選擇器進行html解析(一):将html轉成xml使用xpath實作document.querySelector樣式選擇器進行html解析(二):擴充一下xpath以便支援正則

使用xpath實作document.querySelector樣式選擇器進行html解析(一):将html轉成xml

使用xpath實作document.querySelector樣式選擇器進行html解析(二):擴充一下xpath以便支援正則

使用xpath實作document.querySelector樣式選擇器進行html解析(三):實作樣式選擇器

使用xpath實作document.querySelector樣式選擇器進行html解析(四):将選擇結果封裝進行輸出

-----------------------------------------------------------------

文盲做采集工作也做了有些年頭了,一直以來,對采集到的内容都是用正則進行資料提取的,但是使用的時間越長,越覺得使用正則很麻煩。

第一,了解正則的人在行業内真的是少數,而且複雜的業務邏輯寫出來的正則,隔段時間,自己都看不懂了。。。

第二,正則對文檔的格式還是有一定要求的,比如說如何提取一個完整的閉合html标簽,這個正則就很複雜,用到層深計算了,如果一旦html内出現了非法内容,那就是一場災難,正則會整個卡死。。。。。

是以,文盲老顧一直想找一個htmlparser類型的東西來代替正則,恩,比如說Winista.HtmlParser啦、HtmlAgilityPack啦

但是,這裡要說一個但是,這些第三方的東西并不符合咱們的日常使用習慣,什麼是日常使用習慣呢?當然是css選擇器啦!不管是按id找啦,按樣式找啦,還是按标簽找啦,這些方式我相信大部分開發人員都能很快上手。

于是,按照這個目的觸發,那麼文盲老顧找到的第三方工具都需要帕斯掉了,因為他們不支援,或僅支援部分需求,恩。。。。hmmmmmmmm,也許是文盲老顧沒弄明白這些東西到底怎麼來實作這個css選擇器方式的内容查找,總之,文盲決定自己搞一個htmlparser了

廢話說到這裡,下邊開始編寫文盲版的htmlparser

-----------------------------------------------------------------

在開始編寫之前整理一下思路

首先,html是一個格式很随意的文本文檔,不能強求它一定符合xhtml規範

第二,在xml中,可以通過xpath來實作諸如id、樣式、文字包含等css1.0、2.0、3.0各種規範的選擇器(雖然可能比較複雜,但文盲老顧在2014年的确已經實作了很多内容,css僞類沒做實作,有需要的話,各位同學可以在本文後留言共同讨論)

第三,html無法直接轉成xml,是以我們需要對html進行一些處理,使其能正常的轉換到xml格式

最後,定義一個通用方法,來實作css選擇器方式選取節點并得到想提取的資訊

根據這個思路,第一步應該是先把html轉成xml,好了,開始做第一步工作

-----------------------------------------------------------------

首先先定義一個類,用以加載html内容

public class HtmlObject
    {
        private string _html = string.Empty;
        private List<string> _tags = new List<string>();
        private List<string> _self = new List<string>();
        private XmlDocument _xml = null;
        public string Html
        {
            get
            {
                return _html;
            }
        }
        public XmlDocument Xml
        {
            get
            {
                return _xml;
            }
        }
        public HtmlObject()
        {
            InitDefine();
        }
        public HtmlObject(string html)
        {
            _html = html;
            InitDefine();
            InitHtml();
        }
        public void Load(string file)
        {
            LoadHtml(FileHelper.FileToString(file));
        }
        public void LoadHtml(string html)
        {
            _html = html;
            InitHtml();
        }
        public void LoadUrl(string url)
        {
            Ajax ajax = new Ajax();
            ajax.AppendCss = false;
            ajax.AddFullPath = true;
            ajax.AutoSave = false;
            ajax.AutoUpdate = true;
            LoadHtml(ajax.Http(url));
        }
        private void InitDefine()
        {
            // 聲明自閉合标簽
            _self.AddRange(new string[] { "img", "br", "hr", "base", "meta", "link", "area" });
        }
        private void InitHtml()
        {
            _tags = new List<string>();
            XmlDocument xml = new XmlDocument();
            xml.LoadXml("<r />");
            MatchCollection mc = Regex.Matches(_html, @"<!(?!-)(?:[^<>'""]|(['""])[^'""]*\1)*?>|<([%\?])[\s\S]*?\2>|<!--[\s\S]*?-->|<(script|style)(?!\w)[^<>]*?>(?:[^'""]|(['""])[^'""]*\4)*?</\3(?!\w)[^<>]*?>|<(?![!%\?])(?:[^<>'""]|(['""])[^'""]*\5)*?>|[^<]+(?=<|$)", RegexOptions.IgnoreCase);
            XmlNode node = xml.DocumentElement;
            for (int i = 0; i < mc.Count; i++)
            {
                ParseNode(ref node, mc[i].Value);
            }
            _xml = xml;
        }
        private void ParseNode(ref XmlNode node, string value)
        {
            // 如果是标簽
            if (Regex.IsMatch(value, @"^<"))
            {
                XmlNode xn = null;
                string name = string.Empty;
                //如果是樣式或腳本
                if (Regex.IsMatch(value, @"^<(script|style)(?!\w)", RegexOptions.IgnoreCase))
                {
                    xn = XMLExpand.AppendNode(node, Regex.Match(value, @"(?<=^<)(style|script)", RegexOptions.IgnoreCase).Value.ToLower());
                    xn.AppendChild(xn.OwnerDocument.CreateCDataSection(Regex.Match(value, @"(?<=^<(style|script)[^<>]*?>)[\s\S]*?(?=</\1[^<>]*?>$)", RegexOptions.IgnoreCase).Value));
                }
                // 注釋或其他程式語言标簽
                if (Regex.IsMatch(value, @"^<[!%\?]"))
                {
                    node.AppendChild(node.OwnerDocument.CreateCDataSection(value));
                    //XMLExpand.AppendNode(node, "REM").InnerText = value;
                }
                // 正常标簽
                if (Regex.IsMatch(value, @"^<(?!(script|style))\w+"))
                {
                    name = Regex.Match(value, @"(?<=^<)\w+", RegexOptions.IgnoreCase).Value.ToLower();
                    // 如果不是自閉合标簽則将目前增加的标簽放入到待閉合标簽中
                    if (!Regex.IsMatch(value, @"/>$") && !_self.Contains(name))
                    {
                        _tags.Add(name);
                    }
                    xn = XMLExpand.AppendNode(node, name);
                    node = xn;
                }
                // 正常标簽結束
                if (Regex.IsMatch(value, @"^</"))
                {
                    name = Regex.Match(value, @"(?<=^</)\w+", RegexOptions.IgnoreCase).Value.ToLower();
                    if (node.Name == name)
                    {
                        _tags.RemoveAt(_tags.Count - 1);
                        node = node.ParentNode;
                    }
                    else
                    {
                        // 如果待閉合标簽中包含對應标簽則關閉對應标簽,否則忽視
                        if (_tags.Contains(name))
                        {
                            for (int i = _tags.Count; i > 0; i--)
                            {
                                if (_tags[i - 1] == name)
                                {
                                    _tags.RemoveRange(i - 1, _tags.Count - i + 1);
                                    break;
                                }
                            }
                            while (node.Name != name)
                            {
                                node = node.ParentNode;
                            }
                        }
                    }
                }
                if (Regex.IsMatch(value, @"^<(?![/!%\?])") && xn != null)
                {
                    Match m = Regex.Match(value, @"^<[^<>]*?>", RegexOptions.IgnoreCase);
                    ParseAttribute(xn, m);
                }
                // 如果是自閉合标簽
                if (xn != null && xn == node && !string.IsNullOrEmpty(name) && (Regex.IsMatch(value, @"/>$") || _self.Contains(name)))
                {
                    node = node.ParentNode;
                }
            }
            else
            {
                // 純文字,将文本内容作為節點文本内容
                node.AppendChild(node.OwnerDocument.CreateCDataSection(value));
                //XMLExpand.AppendNode(node, "TEXT").InnerText = value;
            }
        }
        private void ParseAttribute(XmlNode node, Match match)
        {
            string html = match.Value;
            MatchCollection mc = Regex.Matches(html, @"(?<=[\r\n\s\t])(\w+)[\r\n\s\t]*=[\r\n\s\t]*((['""])([^'""]*)\3|[^\s\r\t\n>]+)", RegexOptions.IgnoreCase);
            for (int i = 0; i < mc.Count; i++)
            {
                XMLExpand.SetAttribute(node, mc[i].Groups[1].Value.ToLower(), string.IsNullOrEmpty(mc[i].Groups[4].Value) ? (Regex.IsMatch(mc[i].Groups[2].Value, @"^(['""])\1$") ? "" : mc[i].Groups[2].Value) : mc[i].Groups[4].Value);
            }
        }
    }
           

恩。。。。。。反正就是這麼個代碼,呵呵

構造函數有兩個,一個是帶html文本的,一個是不帶的

加載文檔則有三個方法,一個是直接加載html文本的LoadHtml方法,一個是加載本地檔案的Load方法,一個是加載網址獲得文檔LoadUrl,Hmmmmmmmm,LoadUrl就忽略好了,Load方法也忽略好了。。。。我的代碼中用到的類可以自己去實作後替換,反正意思一樣。。。。

在這個類中,我聲明了兩個私有數組,_tags和_self,_tags是用來存儲解析過程中,未閉合的标簽,而_self則儲存無需閉合的标簽枚舉

然後,就是InitHtml這個核心方法了。。。。。

對html文檔,我使用正則将其切分成一個數組,這個正則大家也可以幫我看看有沒有需要調整的地方

<!(?!-)(?:[^<>'""]|(['""])[^'""]*\1)*?>
|
<([%\?])[\s\S]*?\2>
|
<!--[\s\S]*?-->
|
<(script|style)(?!\w)[^<>]*?>(?:[^'""]|(['""])[^'""]*\4)*?</\3(?!\w)[^<>]*?>
|
<(?![!%\?])(?:[^<>'""]|(['""])[^'""]*\5)*?>
|
[^<]+(?=<|$)
           

我是這麼想的,html中顯示的文本是在标簽之外的,恩,用最後一個正則片段實作,也就是[^<]+(?=<|$)部分

然後是正常的标簽部分,不管是結束标簽還是閉合标簽還是其他什麼html不識别的标簽,隻要是标簽格式,我都拿出來當标簽處理,恩,用倒數第二個正則片段實作,也就是<(?![!%\?])(?:[^<>'""]|(['""])[^'""]*\5)*?>部分

但是,在實際使用過程中,有些标簽中會包含一些特定文本,比如樣式、比如腳本,那麼把樣式和腳本作為特定标簽處理,于是産生了倒數第三個正則片段。。。恩,主要是為了在腳本片段中允許出現小于号,還有</script>這樣的常量,是以這個正則稍微麻煩了些

再然後,發現還有注釋内容也很蛋疼。。。。例如<!--這裡是包含标簽的注釋内容<a href="" target="_blank" rel="external nofollow" >連結</a>-->。。。。沒辦法,繼續加特例。。。。于是倒數第四個正則片段也出現了。。。。。

哦,寫到這裡,發現還會可能出現其他腳本語言片段。。。例如<% %>啦,例如<? ?>啦。。。得,再來搞個正則用來把它也摘出來

最後。。。。還有html聲明。。。。也就是<!doctype html>這樣的html代碼片段也得特殊聲明下。。。。。。好了,第一步我們完成了。。。。。把html用正則拆開了。。。。

MatchCollection mc = Regex.Matches(_html, @"<!(?!-)(?:[^<>'""]|(['""])[^'""]*\1)*?>|<([%\?])[\s\S]*?\2>|<!--[\s\S]*?-->|<(script|style)(?!\w)[^<>]*?>(?:[^'""]|(['""])[^'""]*\4)*?</\3(?!\w)[^<>]*?>|<(?![!%\?])(?:[^<>'""]|(['""])[^'""]*\5)*?>|[^<]+(?=<|$)", RegexOptions.IgnoreCase);
           

說真的,如果這個正則還有其他文盲沒有考慮到的情況,請在本文後留言,文盲會盡快測試,或者,同學們要是發現使用這個正則拆分html的時候出現内容丢失或者拆分結果不符合預期的時候,也請留言,并将html片段貼出來

恩。。。。。。第一步完成了,就繼續下一步,解析節點。。。,也就是ParseNode方法了

解析節點的思路也比較簡單,如果是文本,則扔個CDataSection節點到xml裡,如果是标簽,則按照标簽格式扔不同的節點到xml裡,如果是非閉合标簽,則目前标簽修正為新增标簽,如果是閉合标簽,則目前标簽修正為對應的開始标簽的父級,如果新增了标簽,順便把新增标簽的屬性也解析一下,恩,也就是ParseAttribute

不知道會不會有其他異常,也請大家幫忙測試

好了,第一階段完成,可以把Html轉成xml了,實作選擇的的内容,我們下次再說