1. 程式人生 > >基於行塊分佈函式的通用網頁正文(及圖片)抽取

基於行塊分佈函式的通用網頁正文(及圖片)抽取

問題:如何提取任意(尤其是新聞、資訊類)網頁的正文內容,提取與文章內容相關的圖片,原始碼可見:extractor.py

抓取單個網站網頁內容時通常採用正則匹配的方式,但不同網站之間結構千奇百怪,很難用統一的正則表示式進行匹配。《基於行塊分佈函式的通用網頁正文抽取演算法》的作者總結了一般從網頁中提取文章正文的方法,提出基於行塊分佈的正文抽取演算法,並給出了 PHP 、Java 等實現。這一演算法的主要原理基於兩點:

  1. 正文區密度:在去除HTML中所有tag之後,正文區字元密度更高,較少出現多行空白;
  2. 行塊長度:非正文區域的內容一般單獨標籤(行塊)中較短。

演算法步驟如下:

  • 去除所有tag,包括樣式、Js指令碼內容等,但保留原有的換行符\n
reCOMM = r'<!--.*?-->'
reTRIM = r'<{0}.*?>([\s\S]*?)<\/{0}>'
reTAG  = r'<[\s\S]*?>|[ \t\r\f\v]'

def processTags(body=""):
  body = re.sub(reCOMM, "", body)
  body = re.sub(reTRIM.format("script"), "" ,re.sub(reTRIM.format("style"), "", body))
  body = re.sub(reTAG, "", body)
  return body
  • 將網頁內容按行分割,定義行塊 $block_i$ 為第 $[i, i + blockSize]$ 行文字之和並給出行塊長度基於行號的分佈函式:
def processBlocks(body=""):
  ctexts = body.split("\n")
  textLens = [len(text) for text in ctexts]
  cblocks  = [0] * (len(ctexts) - blockSize)

  lines = len(ctexts)
  for i in range(blockSize):
    cblocks = list(map(lambda x,y: x+y, textLens[i : lines-1-blockSize+i], cblocks))
  return cblocks
  • 正文出現在最長的行塊,擷取兩邊至行塊長度為 0 的範圍:
def getContext(ctexts, cblocks):
  maxTextLen = max(cblocks)

  start = end = cblocks.index(maxTextLen)
  while start > 0 and cblocks[start] > min(textLens):
    start -= 1
  while end < lines - blockSize and cblocks[end] > min(textLens):
    self.end += 1

  return "".join(ctexts[start:end])
  • 如果需要提取正文區域出現的圖片,只需要在第一步去除tag時保留<img>標籤的內容:
reIMG  = re.compile(r'<img[\s\S]*?src=[\'|"]([\s\S]*?)[\'|"][\s\S]*?>')
def processImages(body):
  return reIMG.sub(r'{{\1}}', body)

總結

以上演算法基本可以應對大部分(中文)網頁正文的提取,針對有些網站正文圖片多於文字的情況,可以採用保留<img> 標籤中圖片連結的方法,增加正文密度。目前少量測試發現的問題有:1)文章分頁或動態載入的網頁;2)評論長度過長喧賓奪主的網頁。

參考