從HTML文件中抽取正文的簡單方案

譯者導讀:這篇文章主要介紹了從不一樣類型的HTML文件中抽取出真正有用的正文內容的一種有普遍適應性的方法。其功能相似於CSDN近期推出的「剪 影」,可以去除頁眉、頁腳和側邊欄的無關內容,很是實用。其方法簡單有效而又出乎意料,看完後不免大呼原來還能夠這樣!行文簡明易懂,雖然應用了人工神經 網絡這樣的算法,但由於FANN良好的封裝性,並不要求讀者須要懂得ANN。全文示例以Python代碼寫成,可讀性更佳,具備科普氣息,值得一讀。
每一個人手中均可能有一大堆討論不一樣話題的HTML文檔。但你真正感興趣的內容可能隱藏於廣告、佈局表格或格式標記以及無數連接當中。甚至更糟的是,你但願 那些來自菜單、頁眉和頁腳的文本可以被過濾掉。若是你不想爲每種類型的HTML文件分別編寫複雜的抽取程序的話,我這裏有一個解決方案。
本文講述如何編寫與從大量HTML代碼中獲取正文內容的簡單腳本,這一方法無需知道HTML文件的結構和使用的標籤。它可以工做於含有文本內容的全部新聞文章和博客頁面……
你想知道統計學和機器學習在挖掘文本方面可以讓你省時省力的緣由嗎?
答案極其簡單:使用文本和HTML代碼的密度來決定一行文件是否應該輸出。(這聽起來有點離奇,但它的確有用!)基本的處理工做以下:
  • 1、解析HTML代碼並記下處理的字節數。
  • 2、以行或段的形式保存解析輸出的文本。
  • 3、統計每一行文本相應的HTML代碼的字節數
  • 4、經過計算文本相對於字節數的比率來獲取文本密度
  • 5、最後用神經網絡來決定這一行是否是正文的一部分。
僅僅經過判斷行密度是否高於一個固定的閾值(或者就使用平均值)你就能夠得到很是好的結果。但你也可使用機器學習(這易於實現,簡直不值一提)來減小這個系統出現的錯誤。如今讓我從頭開始……

轉換HTML爲文本

你須要一個文本模式瀏覽器的核心,它應該已經內建了讀取HTML文件和顯示原始文本功能。經過重用已有代碼,你並不須要把不少時間花在處理無效的 XML文件上。咱們將使用Python來完成這個例子,它的htmllib模塊可用以解析HTML文件,formatter模塊可用以輸出格式化的文本。 嗯,實現的頂層函數以下:
def extract_text(html):
# Derive from formatter.AbstractWriter to store paragraphs.
writer = LineWriter()
# Default formatter sends commands to our writer.
formatter = AbstractFormatter(writer)
# Derive from htmllib.HTMLParser to track parsed bytes.
parser = TrackingParser(writer, formatter)
# Give the parser the raw HTML data.
parser.feed(html)
parser.close()
# Filter the paragraphs stored and output them.
return writer.output()
TrackingParser覆蓋瞭解析標籤開始和結束時調用的回調函數,用以給緩衝對象傳遞當前解析的索引。一般你不得不這樣,除非你使用不被推薦的方法——深刻調用堆棧去獲取執行幀。這個類看起來是這樣的:
class TrackingParser(htmllib.HTMLParser):
"""Try to keep accurate pointer of parsing location."""
def __init__(self, writer, *args):
htmllib.HTMLParser.__init__(self, *args)
self.writer = writer
def parse_starttag(self, i):
index = htmllib.HTMLParser.parse_starttag(self, i)
self.writer.index = index
return index
def parse_endtag(self, i):
self.writer.index = i
return htmllib.HTMLParser.parse_endtag(self, i)
LinWriter的大部分工做都經過調用formatter來完成。若是你要改進或者修改程序,大部分時候其實就是在修改它。咱們將在後面講述怎麼爲它加上機器學習代碼。但你也能夠保持它的簡單實現,仍然能夠獲得一個好結果。具體的代碼以下:
class Paragraph:
def __init__(self):
self.text = ''
self.bytes = 0
self.density = 0.0
class LineWriter(formatter.AbstractWriter):
def __init__(self, *args):
self.last_index = 0
self.lines = [Paragraph()]
formatter.AbstractWriter.__init__(self)
def send_flowing_data(self, data):
# Work out the length of this text chunk.
t = len(data)
# We've parsed more text, so increment index.
self.index += t
# Calculate the number of bytes since last time.
b = self.index - self.last_index
self.last_index = self.index
# Accumulate this information in current line.
l = self.lines[-1]
l.text += data
l.bytes += b
def send_paragraph(self, blankline):
"""Create a new paragraph if necessary."""
if self.lines[-1].text == '':
return
self.lines[-1].text += 'n' * (blankline+1)
self.lines[-1].bytes += 2 * (blankline+1)
self.lines.append(Writer.Paragraph())
def send_literal_data(self, data):
self.send_flowing_data(data)
def send_line_break(self):
self.send_paragraph(0)
這裏代碼尚未作輸出部分,它只是聚合數據。如今咱們有一系列的文字段(用數組保存),以及它們的長度和生成它們所須要的HTML的大概字節數。如今讓咱們來看看統計學帶來了什麼。

數據分析

幸運的是,數據里老是存在一些模式。從下面的原始輸出你能夠發現有些文本須要大量的HTML來編碼,特別是標題、側邊欄、頁眉和頁腳。
雖然HTML字節數的峯值屢次出現,但大部分仍然低於平均值;咱們也能夠看到在大部分低HTML字節數的字段中,文本輸出卻至關高。經過計算文本與HTML字節數的比率(即密度)可讓咱們更容易明白它們之間的關係:
密度值圖更加清晰地表達了正文的密度更高,這是咱們的工做的事實依據。

過濾文本行

過濾文本行的最簡單方法是經過與一個閾值(如50%或者平均值)比較密度值。下面來完成LineWriter類:
    def compute_density(self):
"""Calculate the density for each line, and the average."""
total = 0.0
for l in self.lines:
l.density = len(l.text) / float(l.bytes)
total += l.density
# Store for optional use by the neural network.
self.average = total / float(len(self.lines))
def output(self):
"""Return a string with the useless lines filtered out."""
self.compute_density()
output = StringIO.StringIO()
for l in self.lines:
# Check density against threshold.
# Custom filter extensions go here.
if l.density > 0.5:
output.write(l.text)
return output.getvalue()
這個粗糙的過濾器可以獲取大部分正確的文本行。只要頁眉、頁腳和側邊欄文本並不很是長,那麼全部的這些都會被剔除。然而,它仍然會輸出比較長的版本 聲明、註釋和對其它故事的概述;在圖片和廣告周邊的比較短小的文本,卻被過濾掉了。要解決這個問題,咱們須要更復雜些的啓發式過濾器。爲了節省手工計算需 要花費的無數時間,咱們將利用機器學習來處理每一文本行的信息,以找出對咱們有用的模式。

監督式機器學習

這是一個標識文本行是否爲正文的接口界面:所謂的監督式學習就是爲算法提供學習的例子。在這個案例中,咱們給定一系列已經由人標識好的文檔——咱們 知道哪一行必須輸出或者過濾掉。咱們用使用一個簡單的神經網絡做爲感知器,它接受浮點輸入並經過「神經元」間的加權鏈接過濾信息,而後輸後另外一個浮點數。 大致來講,神經元數量和層數將影響獲取最優解的能力。咱們的原型將分別使用單層感知器(SLP)和多層感知器(MLP)模型。咱們須要找些數據來供機器學 習。以前的LineWriter.output()函數正好派上用場,它使咱們可以一次處理全部文本行並做出決定哪些文本行應該輸出的全局結策。從直覺和 經驗中咱們發現下面的幾條原則可用於決定如何過濾文本行:
  • 當前行的密度
  • 當前行的HTML字節數
  • 當前行的輸出文本長度
  • 前一行的這三個值
  • 後一行的這三個值
咱們能夠利用FANN的Python接口來實現,FANN是Fast Artificial Neural NetWork庫的簡稱。基本的學習代碼以下:
from pyfann import fann, libfann
# This creates a new single-layer perceptron with 1 output and 3 inputs.
obj = libfann.fann_create_standard_array(2, (3, 1))
ann = fann.fann_class(obj)
# Load the data we described above.
patterns = fann.read_train_from_file('training.txt')
ann.train_on_data(patterns, 1000, 1, 0.0)
# Then test it with different data.
for datin, datout in validation_data:
result = ann.run(datin)
print 'Got:', result, ' Expected:', datout
嘗試不一樣的數據和不一樣的網絡結構是比較機械的過程。不要使用太多的神經元和使用太好的文本集合來訓練(過擬合),相反地應當嘗試解決足夠多的問題。使用不一樣的行數(1L-3L)和每一行不一樣的屬性(1A-3A)獲得的結果以下:
有趣的是做爲一個猜想的固定閾值,0.5的表現很是好(看第一列)。學習算法並不能僅僅經過比較密度來找出更佳的方案(第二列)。使用三個屬性,下 一個 SLP比前兩都好,但它引入了更多的假陰性。使用多行文本也增進了性能(第四列),最後使用更復雜的神經網絡結構比全部的結果都要更好,在文本行過濾中減 少了80%錯誤。注意:你可以調整偏差計算,以給假陽性比假陰性更多的懲罰(寧缺勿濫的策略)。

結論

從任意HTML文件中抽取正文無需編寫針對文件編寫特定的抽取程序,使用統計學就能得到使人驚訝的效果,而機器學習能讓它作得更好。經過調整閾值, 你可以避免出現魚目混珠的狀況。它的表現至關好,由於在神經網絡判斷錯誤的地方,甚至人類也難以斷定它是否爲正文。如今須要思考的問題是用這些「乾淨」的 正文內容作什麼應用好呢?
相關文章
相關標籤/搜索