Python網頁解析


續上篇文章,網頁抓取到手之后就是解析網頁了。

在Python中解析網頁的庫不少,我最開始使用的是BeautifulSoup,貌似這個也是Python中最知名的HTML解析庫。它主要的特點就是容錯性很好,能很好地處理實際生活中各種亂七八糟的網頁,而且它的API也相當靈活而且豐富。

但是我在自己的正文提取項目中,逐漸無法忍受BeautifulSoup了,主要是因為下面幾個原因:

  • 由於BeautifulSoup 3(當前的版本)依賴於Python內建的sgmllib.py,而sgmllib.py有好些無法容忍的問題,因此在解析某些網頁的時候無法得到正確的結果
  • 由於BeautifulSoup 3的上層和底層解析完全都是Python的,因此速度簡直是無法容忍,有時候簡直比網絡還要慢

下面我們來一一說說上述的問題。

首先是解析上的問題,看看下面的Python代碼:

from BeautifulSoup import BeautifulSoup
 
html = u'<div class=my-css>hello</div>'
print BeautifulSoup(html).find('div')['class']
html = u'<div class=我的CSS類>hello</div>'
print BeautifulSoup(html).find('div')['class']
html = u'<div class="我的CSS類">hello</div>'
print BeautifulSoup(html).find('div')['class']

第一次print的結果my-css,第二次是啥都沒有,第三次print的結果是我們期待的”我的CSS類”。

原來,sgmllib在解析屬性的時候使用的正則表達式沒有考慮到非ASCII字符的問題。這個問題相對好解決一些,只要我們在程序的開頭導入sgmllib模塊,然后修改它對應的屬性正則表達式變量就可以了,就像下面這樣:

import re, sgmllib
 
sgmllib.attrfind = re.compile(r'\s*([a-zA-Z_][-.:a-zA-Z_0-9]*)(\s*=\s*(\'[^\']*\'|"[^"]*"|[^\s^\'^\"^>]*))?')

第二個問題就稍微麻煩一些了,還是看代碼:

from BeautifulSoup import BeautifulSoup
 
html = u'<a onclick="if(x>10) alert(x);" href="javascript:void(0)">hello</a>'
print BeautifulSoup(html).find('a').attrs

打印的結果是[(u'onclick', u'if(x>10) alert(x);')]

顯然,a元素的href屬性被弄丟了。原因就是sgmllib庫在解析屬性的時候一旦遇到了>等特殊符號就會結束屬性的解析。要解決這個問題,只能修改sgmllib中SGMLParser的parse_starttag方法,找到292行,即k = match.end(0)這一行,添加下面的代碼即可:

if k > j:
    match = endbracket.search(rawdata, k+1)
    if not match: return -1
    j = match.start(0)

至於BeautifulSoup速度慢的問題,那就無法通過簡單的代碼修改搞定了,只能換一個HTML解析庫。實際上,我現在使用的是lxml,它是基於C語言開發的libxml2libxslt庫的。經我個人測試,速度比BeautifulSoup 3平均要快10倍。

使用lxml庫解析HTML非常簡單,兼容性也非常高,大部分實際網站上的網頁都可以正確解析。而且lxml使用非常方便的XPath語法進行元素查詢,它支持string-length、count等XPath 1.0函數(參見XPath and XSLT with lxml)。不過2.0的函數,如序列操作的函數就不行了,這需要底層libxml2和libxslt庫的開發者升級這兩個庫,添加對XPath 2.0函數的支持才行。

假設我們得到了unicode類型的網頁,想要獲取所有帶href屬性的鏈接,代碼如下:

import lxml.html
 
dom = lxml.html.fromstring(html)
all_links = dom.xpath('.//a[@href]')

 

使用lxml也有一些注意事項:

  1. 不能向lxml.html.fromstring傳遞長度為0的字符串,不然會拋出解析異常。需要事先判斷一下,如果長度為零,可以傳入<html></html>作為內容
  2. 某些網頁不知什么原因,網頁內容里有\x00,也就是ASCII編碼為0的字符。由於lxml底層是C語言開發的,\x00在C語言里表示字符串結尾,因此需要將這些字符替換一下(html.replace('\x00', ''))
  3. 一般情況下,為了減少編碼猜測的錯誤,我們傳遞給lxml.html.fromstring的網頁字符串都是unicode字符串,也就是經過編碼檢測和decode后的字符串。但是如果網頁是<?xml開頭,而且有編碼設定的(如<?xml version="1.0" encoding="UTF-8" ?>這樣)的,也就是說是一個XML包裹的HTML,則我們必須將原始字符串傳遞給lxml,不然lxml也會報異常,因為針對這種文檔,lxml會嘗試使用自己的解碼機制來做
  4. lxml兼容性是有限的,沒有主流瀏覽器那么寬容。因此,有少部分瀏覽器能大致正常顯示的網頁,lxml仍然無法解析出來

上面幾個問題中,第一二個問題好解決,但是第三個問題最麻煩。因為你事先是不知道這個網頁是否是帶編碼的XML文檔的,只有出了ValueError異常才行,但是lxml使用ValueError報告一切錯誤,因此為了精確一點,你要分析異常的字符串信息才知道是否是這個問題導致的解析異常,如果是的話,則將未解碼的網頁內容再給lxml傳一次,如果不是就報錯。

第四個問題很煩人,但是說句實話,我們也做不了太多工作,lxml的兼容性已經相當好了,可行的方法是放棄掉這些網頁。要么就換一個工具,不使用lxml做網頁解析,但是在Python庫里很難找到比lxml更好的HTML解析庫。

用lxml解析HTML代碼的示例,可以參考正文提取程序里的__create_dom方法。

另外還有一個懸疑問題,那就是在解析網頁多,內存壓力大的情況下,lxml似乎會出現內存溢出的問題。

我有一個程序,每天都要掃描上萬個網頁,解析四五千個網頁。大概每過一個月或一個半月,這個程序就會導致服務器內存全滿,不管是多少內存,全部吃光。我以前一直以為可能是底層代碼的重入性問題(因為我用多線程來做的程序),但是后來換成多進程和單線程模式都會出現這個問題,才跟蹤到出錯的代碼就是在調用lxml.html.fromstring的時候溢出的。

不過這個bug超級難以重現,而且我有很多程序都會不停調用lxml(有的每天,有的每小時,有的每次會解析幾百個網頁)來做HTML解析的工作,可是只有這個程序會偶爾出現溢出情況,太郁悶了。

網上也有類似的報告,但是迄今仍然無法有效重現和確定這個bug。因此我只能寫一個腳本限制python程序的占用內存數,然后通過這個腳本來調用python程序,像這樣:

#!/bin/bash
 
ulimit -m 1536000 -v 1536000
python my-prog.py

解析網頁還有一些其他要做的工作,例如將非標准(網站自定義的)的HTML標簽轉化為span或者div,這樣才能被識別為正文。剩下的就是調試的工作了。

另外,就在本文寫作的時候,BeautifulSoup 4將要發布了,它承諾可以支持Python內建庫、lxml與html5lib等不同的HTML解析引擎來進行網頁解析,也許新版本的BeautifulSoup也是一個不錯的選擇。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM