python實(shí)現(xiàn)精準(zhǔn)搜索并提取網(wǎng)頁(yè)核心內(nèi)容
文 | 李曉飛
來(lái)源:Python 技術(shù)「ID: pythonall」
爬蟲(chóng)程序想必大家都很熟悉了,隨便寫(xiě)一個(gè)就可以獲取網(wǎng)頁(yè)上的信息,甚至可以通過(guò)請(qǐng)求自動(dòng)生成 Python 腳本[1]。
最近我遇到一個(gè)爬蟲(chóng)項(xiàng)目,需要爬取網(wǎng)上的文章。感覺(jué)沒(méi)有什么特別的,但問(wèn)題是沒(méi)有限定爬取范圍,意味著沒(méi)有明確的頁(yè)面的結(jié)構(gòu)。
對(duì)于一個(gè)頁(yè)面來(lái)說(shuō),除了核心文章內(nèi)容外,還有頭部,尾部,左右列表欄等等。有的頁(yè)面框架用 div 布局,有的用 table,即使都用 div,不太的網(wǎng)站風(fēng)格和布局也不同。
但問(wèn)題必須解決,我想,既然搜索引擎抓取到各種網(wǎng)頁(yè)的核心內(nèi)容,我們也應(yīng)該可以搞定,拎起 Python, 說(shuō)干就干!
各種嘗試
如何解決呢?
生成PDF
開(kāi)始想了一個(gè)取巧的方法,就是利用工具(wkhtmltopdf[2])將目標(biāo)網(wǎng)頁(yè)生成 PDF 文件。
好處是不必關(guān)心頁(yè)面的具體形式,就像給頁(yè)面拍了一張照片,文章結(jié)構(gòu)是完整的。
雖然 PDF 是可以源碼級(jí)檢索,但是,生成 PDF 有諸多缺點(diǎn):
耗費(fèi)計(jì)算資源多、效率低、出錯(cuò)率高,體積太大。
幾萬(wàn)條數(shù)據(jù)已經(jīng)兩百多G,如果數(shù)據(jù)量上來(lái)光存儲(chǔ)就是很大的問(wèn)題。
提取文章內(nèi)容
不生成PDF,有簡(jiǎn)單辦法就是通過(guò) xpath[3] 提取頁(yè)面上的所有文字。
但是內(nèi)容將失去結(jié)構(gòu),可讀性差。更要命的是,網(wǎng)頁(yè)上有很多無(wú)關(guān)內(nèi)容,比如側(cè)邊欄,廣告,相關(guān)鏈接等,也會(huì)被提取下來(lái),影響內(nèi)容的精確性。
為了保證有一定的結(jié)構(gòu),還要識(shí)別到核心內(nèi)容,就只能識(shí)別并提取文章部分的結(jié)構(gòu)了。像搜索引擎學(xué)習(xí),就是想辦法識(shí)別頁(yè)面的核心內(nèi)容。
我們知道,通常情況下,頁(yè)面上的核心內(nèi)容(如文章部分)文字比較集中,可以從這個(gè)地方著手分析。
于是編寫(xiě)了一段代碼,我是用 Scrapy[4] 作為爬蟲(chóng)框架的,這里只截取了其中提取文章部分的代碼 :
divs = response.xpath("body//div") sel = None maxvalue = 0 for d in divs: ds = len(d.xpath(".//div")) ps = len(d.xpath(".//p")) value = ps - ds if value > maxvalue: sel = { "node": d, "value": value } maxvalue = value print("".join(sel['node'].getall()))
response
是頁(yè)面的一個(gè)響應(yīng),其中包含了頁(yè)面的所有內(nèi)容,可以通過(guò)xpath
提取想要的部分"body//div"
的意思是提取所以body
標(biāo)簽下的div
子標(biāo)簽,注意://
操作是遞歸的- 遍歷所有提取到的標(biāo)簽,計(jì)算其中包含的
div
數(shù)量,和p
數(shù)量 p
數(shù)量 和div
數(shù)量的差值作為這個(gè)元素的權(quán)值,意思是如果這個(gè)元素里包含了大量的p
時(shí),就認(rèn)為這里是文章主體- 通過(guò)比較權(quán)值,選擇出權(quán)值最大的元素,這便是文章主體
- 得到文章主體之后,提取這個(gè)元素的內(nèi)容,相當(dāng)于 jQuery[5] 的
outerHtml
簡(jiǎn)單明了,測(cè)試了幾個(gè)頁(yè)面確實(shí)挺好。
不過(guò)大量提取時(shí)發(fā)現(xiàn),很多頁(yè)面提取不到數(shù)據(jù)。仔細(xì)查看發(fā)現(xiàn),有兩種情況。
- 有的文章內(nèi)容被放在了
<article>
標(biāo)簽里了,所以沒(méi)有獲取到 - 有的文章每個(gè)
<p>
外面都包裹了一個(gè)<div>
,所以p
的數(shù)量 和div
的抵消了
再調(diào)整了一下策略,不再區(qū)分 div
,查看所有的元素。
另外優(yōu)先選擇更多的 p
,在其基礎(chǔ)上再看更少的 div
。調(diào)整后的代碼如下:
divs = response.xpath("body//*") sels = [] maxvalue = 0 for d in divs: ds = len(d.xpath(".//div")) ps = len(d.xpath(".//p")) if ps >= maxvalue: sel = { "node": d, "ps": ps, "ds": ds } maxvalue = ps sels.append(sel) sels.sort(lambda x: x.ds) sel = sels[0] print("".join(sel['node'].getall()))
- 方法主體里,先挑選出
p
數(shù)量比較大的節(jié)點(diǎn),注意if
判斷條件中 換成了>=
號(hào),作用時(shí)篩選出同樣具有p
數(shù)量的結(jié)點(diǎn) - 經(jīng)過(guò)篩選之后,按照
div
數(shù)量排序,然后選取div
數(shù)量最少的
經(jīng)過(guò)這樣修改之后,確實(shí)在一定程度上彌補(bǔ)了前面的問(wèn)題,但是引入了一個(gè)更麻煩的問(wèn)題。
就是找到的文章主體不穩(wěn)定,特別容易受到其他部分有些 p
的影響。
選擇最優(yōu)
既然直接計(jì)算不太合適,需要重新設(shè)計(jì)一個(gè)算法。
我發(fā)現(xiàn),文字集中的地方是往往是文章主體,而前面的方法中,沒(méi)有考慮到這一點(diǎn),只是機(jī)械地找出了最大的 p
。
還有一點(diǎn),網(wǎng)頁(yè)結(jié)構(gòu)是個(gè)顆 DOM 樹(shù)[6]
那么越靠近 p
標(biāo)簽的地方應(yīng)該越可能是文章主體,也就是說(shuō),計(jì)算是越靠近 p
的節(jié)點(diǎn)權(quán)值應(yīng)該越大,而遠(yuǎn)離 p
的結(jié)點(diǎn)及時(shí)擁有很多 p
但是權(quán)值也應(yīng)該小一點(diǎn)。
經(jīng)過(guò)試錯(cuò),最終代碼如下:
def find(node, sel): value = 0 for n in node.xpath("*"): if n.xpath("local-name()").get() == "p": t = "".join([s.strip() for s in (n.xpath('text()').getall() + n.xpath("*/text()").getall())]) value += len(t) else: value += find(n, a)*0.5 if value > sel["value"]: sel["node"] = node sel["value"] = value return value sel = { 'value': 0, 'node': None } find(response.xpath("body"), sel)
- 定義了一個(gè)
find
函數(shù),這是為了方便做遞歸,第一次調(diào)用的參數(shù)是body
標(biāo)簽,和前面一樣 - 進(jìn)入方法里,只找出該節(jié)點(diǎn)的直接孩子們,然后遍歷這些孩子
- 判斷如果孩子是
p
節(jié)點(diǎn),提取出其中的所有文字,包括子節(jié)點(diǎn)的,然后將文字的長(zhǎng)度作為權(quán)值 - 提取文字的地方比較繞,先取出直接的文本,和間接文本,合成
list
,對(duì)每部分文本做了去除前后空字符,最后合并為一個(gè)字符串,得到了所包含的文本 - 如果孩子節(jié)點(diǎn)不是
p
,就遞歸調(diào)用find
方法,而find
方法返回的是 指定節(jié)點(diǎn)所包含的文本長(zhǎng)度 - 在獲取子節(jié)點(diǎn)的長(zhǎng)度時(shí),做了縮減處理,用以體現(xiàn)距離越遠(yuǎn),權(quán)值越低的規(guī)則
- 最終通過(guò) 引用傳遞的
sel
參數(shù),記錄權(quán)值最高的節(jié)點(diǎn)
通過(guò)這樣改造之后,效果特別好。
為什么呢?其實(shí)利用了密度原理,就是說(shuō)越靠近中心的地方,密度越高,遠(yuǎn)離中心的地方密度成倍的降低,這樣就能篩選出密度中心了。
50% 的坡度比率是如何得到的呢?
其實(shí)是通過(guò)實(shí)驗(yàn)確定的,剛開(kāi)始時(shí)我設(shè)置為 90%,但結(jié)果時(shí) body
節(jié)點(diǎn)總是最優(yōu)的,因?yàn)?body
里包含了所有的文字內(nèi)容。
反復(fù)實(shí)驗(yàn)后,確定 50% 是比較好的值,如果在你的應(yīng)用中不合適,可以做調(diào)整。
總結(jié)
描述了我如何選取文章主體的方法后,后沒(méi)有發(fā)現(xiàn)其實(shí)很是很簡(jiǎn)單的方法。而這次解決問(wèn)題的經(jīng)歷,讓我感受到了數(shù)學(xué)的魅力。
一直以來(lái)我認(rèn)為只要了解常規(guī)處理問(wèn)題的方式就足以應(yīng)對(duì)日常編程了,可以當(dāng)遇到不確定性問(wèn)題,沒(méi)有辦法抽取出簡(jiǎn)單模型的問(wèn)題時(shí),常規(guī)思維顯然不行。
所以平時(shí)我們應(yīng)該多看一些數(shù)學(xué)性強(qiáng)的,解決不確定性問(wèn)題的方法,以便提高我們的編程適應(yīng)能力,擴(kuò)展我們的技能范圍。
期望這篇短文能對(duì)你有所啟發(fā),歡迎在留言區(qū)交流討論,比心!
參考資料
[1]
Curl 轉(zhuǎn) Python: https://curlconverter.com/
[2]
wkhtmltopdf: https://wkhtmltopdf.org/
[3]
xpath: https://www.w3school.com.cn/xpath/xpath_syntax.asp
[4]
Scrapy: https://scrapy.org/
[5]
jQuery: jquery.com
[6]
DOM 樹(shù): https://baike.baidu.com/item/DOM%20Tree/6067246
以上就是python實(shí)現(xiàn)精準(zhǔn)搜索并提取網(wǎng)頁(yè)核心內(nèi)容的詳細(xì)過(guò)程,更多關(guān)于python搜索并提取網(wǎng)頁(yè)內(nèi)容的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決PyCharm的Python.exe已經(jīng)停止工作的問(wèn)題
今天小編就為大家分享一篇解決PyCharm的Python.exe已經(jīng)停止工作的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-11-11人機(jī)交互程序 python實(shí)現(xiàn)人機(jī)對(duì)話
這篇文章主要為大家詳細(xì)介紹了人機(jī)交互程序,初步實(shí)現(xiàn)python人機(jī)對(duì)話,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11Python實(shí)現(xiàn)的下載8000首兒歌的代碼分享
這篇文章主要介紹了Python實(shí)現(xiàn)的下載8000首兒歌的代碼分享,本文直接給出實(shí)現(xiàn)代碼,下載的是有伴網(wǎng)的資源,需要的朋友可以參考下2014-11-11Python學(xué)習(xí)筆記之open()函數(shù)打開(kāi)文件路徑報(bào)錯(cuò)問(wèn)題
這篇文章主要介紹了Python學(xué)習(xí)筆記之open()函數(shù)打開(kāi)文件路徑報(bào)錯(cuò)問(wèn)題,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-04-04python圖形界面開(kāi)發(fā)之wxPython樹(shù)控件使用方法詳解
這篇文章主要介紹了python圖形界面開(kāi)發(fā)之wxPython樹(shù)控件使用方法詳解,需要的朋友可以參考下2020-02-02python圖形用戶界面tkinter之標(biāo)簽Label的使用說(shuō)明
這篇文章主要介紹了python圖形用戶界面tkinter之標(biāo)簽Label的使用說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06Python設(shè)計(jì)模式之備忘錄模式原理與用法詳解
這篇文章主要介紹了Python設(shè)計(jì)模式之備忘錄模式原理與用法,結(jié)合實(shí)例形式詳細(xì)分析了備忘錄模式的相關(guān)概念、原理及Python相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2019-01-01利用Tensorflow的隊(duì)列多線程讀取數(shù)據(jù)方式
今天小編就為大家分享一篇利用Tensorflow的隊(duì)列多線程讀取數(shù)據(jù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-02-02