網頁正文提取——Html2Article
為什麼要做正文提取
一般做輿情分析,都會涉及到網頁正文內容提取。對於分析而言,有價值的資訊是正文部分,大多數情況下,為了便於分析,需要將網頁中和正文不相干的部分給剔除。可以說正文提取的好壞,直接影響了分析結果的好壞。
對於特定的網站,我們可以分析其html結構,根據其結構來獲取正文資訊。先看一下下面這張圖:
正文部分,不同的網站,正文所在的位置不同,並且Html的結構也不同,對於爬蟲而言,抓取的頁面是各種各樣的,不可能針對所有的頁面去寫抓取規則來提取正文內容,因此需要一種通用的演算法將正文提取出來。
現有的網頁正文提取演算法
- 基於標籤用途的正文提取演算法(比如title或h1,h2標籤一般用作標題,p一般表示正文段落,根據標籤的含義去提取正文)
- 基於標籤密度判定(這個簡單,說白了就是字元統計,正文部分html標籤的密度比較低,確定一個閾值,按照標籤密度提取正文部分)
- 基於資料探勘思想的網頁正文抽取方法(這裡會涉及到統計學和概率論的一些知識,在高深點就成了機器學習了,沒有深入研究)
- 基於視覺網頁塊分析技術的正文抽取(CV這種高階大氣上檔次的東西,豈是我等這麼容易就能研究明白的。雖然實現上覆雜,但就提取效果而言,這種方法提取的精度還是不錯的)
前2中方法還是比較容易實現的,主要是處理簡單,先前我把標籤密度的提取演算法實現了,但實際用起來錯誤率還是蠻高的;後2種方法在實現上就略複雜了,從演算法效率上講應該也高不了哪去。
我們需要的是一種簡單易實現的,既能保證處理速度,提取的準確率也不錯的
網頁分析
我任意取了百度,搜狐,網易的一篇新聞類網頁,拿來作分析。
先看一篇百度的文章
首先請求這個頁面,然後過濾到所有的html標籤,只保留文字資訊,我們可以看到正文資訊集中在一下位置:
使用Excel分析行數與每行的字元的關係可以發現:
很明顯,正文內容集中在65-100行之間的位置上,而這個區間的字元數也是比較密集的。
再來一篇網易的文章
還是先看下過濾html標籤後的正文部分:
再來一個Excel的分析結果:
正文部分集中在279-282行之間,從圖上看,也正是這麼幾行的文字密度特別高。
最後分析一篇搜狐的新聞
還是先看下過後標籤後的正文:
再看下Excel的分析結果:
而搜狐的這篇文章正文部分主要集中在200-255行之間。其餘的文字全部是雜亂的標籤文字。
抱歉,漏了很重要的一點說明:為什麼分析的時候要把html標籤過濾掉呢?過濾html標籤是為了降低干擾,因為我們關注的是正文內容,如果帶著這樣的標籤<span style="color: #0000ff;">var</span> chart = <span style="color: #0000ff;">new</span><span style="color: #000000;">去分析,可想而知,對我們的正文分析會有多大的干擾了,也正因如此需要將html標籤掉,只對文字做分析,降低干擾。
基於網頁分析構思出的正文提取演算法
回顧以上的網頁分析,如果按照文字密度來找提取正文,那麼就是寫這麼一個演算法,能夠從過濾html標籤後的文字中找到正文文字的起止行號,行號之間的文字就是網頁正文部分。
還是從上面三個網頁的分析結果看,他們都有這麼一個特性:正文部分的文字密度要高出非正文部分很多。我們按照這個特性就可以很容易將演算法實現,那就是基於閾(讀音:yu)值去分析正文所在的位置。
那麼接下來就需要解決一些問題:
- 如何確定閾值?
- 如何分析,一行行的分析?還是?
閾值的確定可以通過統計分析得出一個比較好的值,我在實際處理過程中,發現這個值取180是比較合適的,也就是分析文字的時候,如果所分析的文字超過了180,那麼就可以認為到達了正文部分。
再有就是如何分析的問題,這個其實比較容易確定,一行行的分析效果肯定不好,如果在按行分析的過程中往下在分析幾行作為一次分析效果比較好。也就是一次性分析上5行左右,將字元累加起來,看看有沒有達到設定的閾值,如果達到了,那麼認為已經進入正文部分了。
嗯,主要的處理邏輯就是這樣,怎麼樣,不復雜吧。
我把實現的核心演算法也貼出來吧:
int preTextLen = 0; // 記錄上一次統計的字元數量(lines就是去除html標籤後的文字,_limitCount是閾值,_depth是我們要分析的深度,sb用於記錄正文)int startPos = -1; // 記錄文章正文的起始位置for (int i = 0; i < lines.Length - _depth; i++){ int len = 0; for (int j = 0; j < _depth; j++) { len += lines[i + j].Length; } if (startPos == -1) // 還沒有找到文章起始位置,需要判斷起始位置 { if (preTextLen > _limitCount && len > 0) // 如果上次查詢的文字數量超過了限定字數,且當前行數字符數不為0,則認為是開始位置 { // 查詢文章起始位置, 如果向上查詢,發現2行連續的空行則認為是頭部 int emptyCount = 0; for (int j = i - 1; j > 0; j--) { if (String.IsNullOrEmpty(lines[j])) { emptyCount++; } else { emptyCount = 0; } if (emptyCount == _headEmptyLines) { startPos = j + _headEmptyLines; break; } } // 如果沒有定位到文章頭,則以當前查詢位置作為文章頭 if (startPos == -1) { startPos = i; } // 填充發現的文章起始部分 for (int j = startPos; j <= i; j++) { sb.Append(lines[j]); } } } else { if (len <= _endLimitCharCount && preTextLen < _endLimitCharCount) // 當前長度為0,且上一個長度也為0,則認為已經結束 { if (!_appendMode) { break; } startPos = -1; } sb.Append(lines[i]); } preTextLen = len;}
核心的提取演算法不足60行,經過驗證提取的效果還是非常不錯的,至少做到了正文提取正確率90%上,效率上做到了平均提取時間30ms左右。
還需解決的一些問題
html標籤剔除:這個簡單,直接使用正則表示式替換(Regex.Replace(html, "(?is)<.*?>", "")),將所有的html標籤剔除即可
html壓縮型網頁的處理: 壓縮後的html程式碼一般只有一行,對這類的html處理也比較簡單(不需要複雜的程式碼格式化),直接在標籤末尾強制新增換行符即可。
正文標題:大多數規範的網址會用h1標籤作文正文標題,處理時如果有h1那麼從h1標籤中提取標題,沒有的話,直接從title標籤中那吧。
文章釋出時間:並不是所有的文章都有釋出時間(不過貌似大多數都有哈),直接使用正則從去除標籤後的正文中提取時間吧。
保留帶標籤的正文:我們的演算法是和標籤無關的,因為演算法處理時首先要過濾html標籤,去除干擾,那麼如果想要帶標籤的正文怎麼辦(比如要保留正文中的圖片)?這時只能保留2個數組了,一個數組存放過濾標籤的文字,便於分析,另一個數組則保留html標籤,便於提取原始資訊。
Html2Article網頁正文提取演算法
Html2Article就是我基於以上思想實現的網頁正文提取演算法。有以下特點:
- 標籤無關,提取正文不依賴標籤。
- 支援從壓縮的html文件中提取正文內容。
- 支援帶標籤輸出原始正文。
- 核心演算法簡潔高效,平均提取時間在30ms左右。
演算法已開源(也算是為開源做點貢獻了吧):
使用方法請參考文件介紹說明。
演算法是用C#實現的,玩.NET的同學有福了,可以直接使用nuget將html2article新增到你的專案中哦。
另外發現直接從百度搜索“html2article”也能找到很快的找打它,演算法實現已經將近半年了,一直比較懶,也沒寫過文章跟大家分享一下。