[譯] 如何從網頁中獲取主要文本信息

原文 Extracting meaningful content from raw HTML

解析HTML頁面不算難事, 開源庫Beautiful Soup就給你提供了緊湊直觀的接口來處理網頁, 讓你使用喜歡的語言去實現. 但這也僅僅是第一步. 更有趣的問題是: 怎么才能把有用的信息(主題內容)從網頁中抓取出來?

我在過去的幾天里一直在嘗試回答這個問題, 以下是我找到的.

Arc90 Readability

我最喜歡的方法是叫做 Arc90 Readability 的算法. 由 Arc90 Labs 開發, 目的是用來使網頁閱讀更佳舒適(例如在移動端上). 你可以找到它的chrome plugin. 整個項目放在Google Code上, 但更有趣的是它的算法, Nirmal Patel用python實現了這個算法, 源碼在這里.

整個算法基于HTML-ID-names和HTML-CLASS-names生成了2個列表. 一個是包含了褒義IDs和CLASSes, 一個列表包含了貶義IDs和CLASSes. 如果一個tag有褒義的ID和CLASS, 那么它會得到額外的分數. 反之,如果它包含了貶義的ID和CLASS則丟分. 當我們算完了所有的tag的分數之后, 我們只需要渲染出那些得分較高的tags, 就得到了我們想要的內容.例子如下:

<div id="post"><h1>My post</h1><p>...</p></div>
<div class="footer"><a...>Contact</a></div>

第一個div tag含有一個褒義的ID (“id"=“post”), 所以很有可能這個tag包含了真正的內容(post). 然而, 第二行的tag footer是一個貶義的tag, 我們可以認為這個tag所包含的東西不是我們想要的真正內容. 基于以上, 我們可以得到如下方法:

  1. 在HTML源碼里找到所有的p tag(即paragraph)
  2. 對于每一個paragraph段落:
  3. 把該段落的父級標簽加入到列表中
  4. 并且把該父級標簽初始化為0分
  5. 如果父級標簽包含有褒義的屬性, 加分
  6. 如果父級標簽包含有貶義的屬性, 減分
  7. 可選: 加入一些額外的標準, 比如限定tag的最短長度
  8. 找到得分最多的父級tag
  9. 渲染得分最多的父級tag

這里我參考了Nirmal Patel的代碼, 寫了一個簡單的實現. 主要的區別是: 我在算法之前, 寫了一個用來清除雜項的代碼. 這樣最終會得到一個沒有腳本, 圖片的文本內容, 就是我們想要的網頁內容.

import re
from bs4 import BeautifulSoup
from bs4 import Comment
from bs4 import Tag
 
NEGATIVE = re.compile(".*comment.*|.*meta.*|.*footer.*|.*foot.*|.*cloud.*|.*head.*")
POSITIVE = re.compile(".*post.*|.*hentry.*|.*entry.*|.*content.*|.*text.*|.*body.*")
BR = re.compile("<br */? *>[ rn]*<br */? *>")
 
def extract_content_with_Arc90(html):
 
    soup = BeautifulSoup( re.sub(BR, "</p><p>", html) )
    soup = simplify_html_before(soup)
 
    topParent = None
    parents = []
    for paragraph in soup.findAll("p"):
        
        parent = paragraph.parent
        
        if (parent not in parents):
            parents.append(parent)
            parent.score = 0
 
            if (parent.has_key("class")):
                if (NEGATIVE.match(str(parent["class"]))):
                    parent.score -= 50
                elif (POSITIVE.match(str(parent["class"]))):
                    parent.score += 25
 
            if (parent.has_key("id")):
                if (NEGATIVE.match(str(parent["id"]))):
                    parent.score -= 50
                elif (POSITIVE.match(str(parent["id"]))):
                    parent.score += 25
 
        if (len( paragraph.renderContents() ) > 10):
            parent.score += 1
 
        # you can add more rules here!
 
    topParent = max(parents, key=lambda x: x.score)
    simplify_html_after(topParent)
    return topParent.text
 
def simplify_html_after(soup):
 
    for element in soup.findAll(True):
        element.attrs = {}    
        if( len( element.renderContents().strip() ) == 0 ):
            element.extract()
    return soup
 
def simplify_html_before(soup):
 
    comments = soup.findAll(text=lambda text:isinstance(text, Comment))
    [comment.extract() for comment in comments]
 
    # you can add more rules here!
 
    map(lambda x: x.replaceWith(x.text.strip()), soup.findAll("li"))    # tag to text
    map(lambda x: x.replaceWith(x.text.strip()), soup.findAll("em"))    # tag to text
    map(lambda x: x.replaceWith(x.text.strip()), soup.findAll("tt"))    # tag to text
    map(lambda x: x.replaceWith(x.text.strip()), soup.findAll("b"))     # tag to text
    
    replace_by_paragraph(soup, 'blockquote')
    replace_by_paragraph(soup, 'quote')
 
    map(lambda x: x.extract(), soup.findAll("code"))      # delete all
    map(lambda x: x.extract(), soup.findAll("style"))     # delete all
    map(lambda x: x.extract(), soup.findAll("script"))    # delete all
    map(lambda x: x.extract(), soup.findAll("link"))      # delete all
    
    delete_if_no_text(soup, "td")
    delete_if_no_text(soup, "tr")
    delete_if_no_text(soup, "div")
 
    delete_by_min_size(soup, "td", 10, 2)
    delete_by_min_size(soup, "tr", 10, 2)
    delete_by_min_size(soup, "div", 10, 2)
    delete_by_min_size(soup, "table", 10, 2)
    delete_by_min_size(soup, "p", 50, 2)
 
    return soup
 
def delete_if_no_text(soup, tag):
    
    for p in soup.findAll(tag):
        if(len(p.renderContents().strip()) == 0):
            p.extract()
 
def delete_by_min_size(soup, tag, length, children):
    
    for p in soup.findAll(tag):
        if(len(p.text) < length and len(p) <= children):
            p.extract()
 
def replace_by_paragraph(soup, tag):
    
    for t in soup.findAll(tag):
        t.name = “p"
        t.attrs = {}  

空格符渲染

這個方法是主要思路很簡單: 把HTML源碼里面的所有tag (所有在<和>之間的代碼) 用空格符代替. 當你再次渲染網頁的時候, 所有的文本塊(text block)依然是”塊”狀, 但是其他部分變成了包含很多空格符的分散的語句. 你剩下唯一要做的就是把文本快給找出來, 并且移除掉所有其他的內容.
我寫了一個簡單的實現.

#!/usr/bin/python
# -*- coding: utf-8 -*-
 
import requests
import re
 
from bs4 import BeautifulSoup
from bs4 import Comment
 
if __name__ == "__main__":
    
    html_string = requests.get('http://www.zdnet.com/windows-8-microsofts-new-coke-moment-7000014779/').text
    
    soup = BeautifulSoup(str( html_string ))
    
    map(lambda x: x.extract(), soup.findAll("code"))
    map(lambda x: x.extract(), soup.findAll("script"))
    map(lambda x: x.extract(), soup.findAll("pre"))
    map(lambda x: x.extract(), soup.findAll("style"))
    map(lambda x: x.extract(), soup.findAll("embed"))
    
    comments = soup.findAll(text=lambda text:isinstance(text, Comment))
        
    [comment.extract() for comment in comments]
    
    white_string = ""
    isIn = False;
    
    for character in soup.prettify():
 
        if character == "<":
            isIn = True;
        
        if isIn:
            white_string += " "
        else:
            white_string += character
            
        if character == ">":
            isIn = False;
            
    for string in white_string.split("           "):    # tune here!
        
        p = string.strip()
        p = re.sub(' +',' ', p)
        p = re.sub('n+',' ', p)
        
        if( len( p.strip() ) > 50):
            print p.strip()

這里有個問題是, 這個方法不是很通用. 你需要進行參數調優來找到最合適空格符長度來分割得到的字符串. 那些帶有大量markup標記的網站通常要比普通網站有更多的空格符. 除此之外, 這個還是一個相當簡單有效的方法.

開源社區的庫
有句話說: 不要重新造輪子. 事實上有很多的開源庫可以解決網頁內容抓取的問題. 我最喜歡的是Boilerpipe. 你可以在這里找到它的web服務(demo)http://boilerpipe-web.appspot.com/, 以及它的JAVA實現https://code.google.com/p/boilerpipe/. 相比較上面2個簡單的算法, 它真的很有用, 代碼也相對更復雜. 但是如果你只把它當做黑盒子使用, 確實是一個非常好的解決方法.

Best regards,
Thomas Uhrig

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,791評論 6 545
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,795評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,943評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,057評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,773評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,106評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,082評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,282評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,793評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,507評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,741評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,220評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,929評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,325評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,661評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,482評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,702評論 2 380

推薦閱讀更多精彩內容

  • 1. 介紹 瀏覽器可能是最廣泛使用的軟件。本書將介紹瀏覽器的工作原理。我們將看到,當你在地址欄中輸入google....
    康斌閱讀 2,046評論 7 18
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,806評論 25 708
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,836評論 18 139
  • 蘇州常熟新顏街的櫻花開了, 好多人去賞花, 前天我倆去踩了個點。 像我和丫頭這么懶的人, 睡了個自然醒后才去, 所...
    囡囡nannanxbk閱讀 198評論 0 1
  • 臺風尼伯特總算離開福建了,連日的暴雨過后,炎炎的夏日透著一絲小清涼。今天是周末,可我還得上班去,心情真心不咋滴。 ...
    若hjy閱讀 748評論 0 0