Python lxml 庫:從網頁HTML/XML提取資料
Python 的 lxml 模組是一個非常好用且效能高的HTML、XML解析工具,通過它解析網頁,爬蟲就可以輕鬆的從網頁中提取想要的資料。lxml是基於C語言的libxml2和libxslt庫開發的,所以速度是沒的說。
使用lxml提取網頁資料的流程
要從網頁裡面提取資料,使用lxml需要兩步:
-
第一步,用lxml把網頁(或xml)解析成一個DOM樹。這個過程,我們可以選擇
etree
、etree.HTML
和lxml.html
這三種來實現,它們基本類似但又有些許差別,後面我們會詳細講到。 -
第二步,使用xpath遍歷這棵DOM 樹,找到你想要的資料所在的節點並提取。這一步要求我們對xpath規則比較熟練,xpath規則很多,但別怕,我來總結一些常用的套路。
生成DOM樹
上面我們說了,可以有三種方法來把網頁解析成DOM樹,有選擇困難症的同學要犯難了,選擇那種好呢?別急,我們逐一探究一下。下面我通過例項來解析一下下面這段html程式碼:
<div class="1">
<p class="p_1 item">item_1</p>
<p class="p_2 item">item_2</p>
</div>
<div class="2">
<p id="p3"><a href="/go-p3">item_3</a></p>
</div>
使用etree.fromstring()函式
先看看這個函式的說明(docstring):
In [3]: etree.fromstring?
Signature: etree.fromstring(text, parser=None, *, base_url=None)
Call signature: etree.fromstring(*args, **kwargs)
Type: cython_function_or_method
String form: <cyfunction fromstring at 0x7fe538822df0>
Docstring:
fromstring(text, parser=None, base_url=None)
Parses an XML document or fragment from a string. Returns the
root node (or the result returned by a parser target).
To override the default parser with a different parser you can pass it to
the ``parser`` keyword argument.
The ``base_url`` keyword argument allows to set the original base URL of
the document to support relative Paths when looking up external entities
(DTD, XInclude, ...).
這個函式就是把輸入的html解析成一棵DOM樹,並返回根節點。它對輸入的字串text有什麼要求嗎?首先,必須是合法的html字串,然後我們看看下面的例子:
In [19]: html = '''
...: <div class="1">
...: <p class="p_1 item">item_1</p>
...: <p class="p_2 item">item_2</p>
...: </div>
...: <div class="2">
...: <p id="p3"><a href="/go-p3">item_3</a></p>
...: </div>
...: '''
In [20]: etree.fromstring(html)
Traceback (most recent call last):
File "/home/veelion/.virtualenvs/py3.6/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 3267, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-20-aea2e2c2317e>", line 1, in <module>
etree.fromstring(html)
File "src/lxml/etree.pyx", line 3213, in lxml.etree.fromstring
File "src/lxml/parser.pxi", line 1877, in lxml.etree._parseMemoryDocument
File "src/lxml/parser.pxi", line 1758, in lxml.etree._parseDoc
File "src/lxml/parser.pxi", line 1068, in lxml.etree._BaseParser._parseUnicodeDoc
File "src/lxml/parser.pxi", line 601, in lxml.etree._ParserContext._handleParseResultDoc
File "src/lxml/parser.pxi", line 711, in lxml.etree._handleParseResult
File "src/lxml/parser.pxi", line 640, in lxml.etree._raiseParseError
File "<string>", line 6
XMLSyntaxError: Extra content at the end of the document, line 6, column 1
竟然報錯了!究其原因,我們的html是兩個並列的<div>
標籤,沒有一個單獨的root節點。那麼給這個html再加一個最外層的<div>
標籤呢?
In [22]: etree.fromstring('<div>' + html + '</div>')
Out[22]: <Element div at 0x7fe53aa978c8>
這樣就可以了,返回了root節點,它是一個Element物件,tag是div。
總結一下,etree.fromstring()
需要最外層是一個單獨的節點,否則會出錯。這個方法也適用於生成 XML 的DOM樹。
使用etree.HTML()函式
這個函式更像是針對 HTML 的,看看它的docstring:
In [23]: etree.HTML?
Signature: etree.HTML(text, parser=None, *, base_url=None)
Call signature: etree.HTML(*args, **kwargs)
Type: cython_function_or_method
String form: <cyfunction HTML at 0x7fe538822c80>
Docstring:
HTML(text, parser=None, base_url=None)
Parses an HTML document from a string constant. Returns the root
node (or the result returned by a parser target). This function
can be used to embed "HTML literals" in Python code.
To override the parser with a different ``HTMLParser`` you can pass it to
the ``parser`` keyword argument.
The ``base_url`` keyword argument allows to set the original base URL of
the document to support relative Paths when looking up external entities
(DTD, XInclude, ...).
介面引數跟etree.fromstring()
一模一樣,實操一下:
In [24]: etree.HTML(html)
Out[24]: <Element html at 0x7fe53ab03748>
輸入兩個並列節點的html也沒有問題。等等,返回的root節點物件Element的標籤是html?把它用etree.tostring()
還原成html程式碼看看:
In [26]: print(etree.tostring(etree.HTML(html)).decode())
<html><body><div class="1">
<p class="p_1 item">item_1</p>
<p class="p_2 item">item_2</p>
</div>
<div class="2">
<p id="p3"><a href="/go-p3">item_3</a></p>
</div>
</body></html>
In [27]: print(html)
<div class="1">
<p class="p_1 item">item_1</p>
<p class="p_2 item">item_2</p>
</div>
<div class="2">
<p id="p3"><a href="/go-p3">item_3</a></p>
</div>
也就是說,etree.HTML()
函式會補全html程式碼片段,給它們加上<html>
和<body>
標籤。
使用lxml.html函式
lxml.html
是lxml的子模組,它是對etree
的封裝,更適合解析html網頁。用這個子模組生成DOM樹的方法有多個:
- lxml.html.document_fromstring()
- lxml.html.fragment_fromstring()
- lxml.html.fragments_fromstring()
- lxml.html.fromstring()
它們的docstring可以在ipython裡面查一下,這裡就不再列舉。通常,我們解析網頁用最後一個fromstring()
即可。這個fromstring()
函式也會給我們的樣例html程式碼最頂層的兩個並列節點加一個父節點div
。
上面三種方法介紹完,相信你自己已經有了選擇,那必須是lxml.html
。
因為它針對html做了封裝,所以也多了寫特有的方法:
-
比如我們要獲得某個節點下包含所有子節點的文字內容時,通過
etree
得到的節點沒辦法,它的每個節點有個text
屬性只是該節點的,不包括子節點,必須要自己遍歷獲得子節點的文字。而lxml.html
有一個text_content()
方法可以方便的獲取某節點內包含的所有文字。 -
再比如,好多網頁的連結寫的都是相對路徑而不是完整url:
<a href="/index.html">
,我們提取連結後還要自己手動拼接成完整的url。這個時候可以用lxml.html
提供的make_links_absolute()
方法,這個方法是節點物件Element
的方法,etree的Element
物件卻沒有。
使用xpath提取資料
我們還以下面這段html程式碼為例,來看看如何定位節點提取資料。
<div class="1">
<p class="p_1 item">item_1</p>
<p class="p_2 item">item_2</p>
</div>
<div class="2">
<p id="p3"><a href="/go-p3">item_3</a></p>
</div>
首先匯入lxml.html
模組,生成DOM樹:
In [50]: import lxml.html as lh
In [51]: doc = lh.fromstring(html)
(1)通過標籤屬性定位節點
比如我們要獲取<div class="2">
這節點:
In [52]: doc.xpath('//div[@class="2"]')
Out[52]: [<Element div at 0x7fe53a492ea8>]
In [53]: print(lh.tostring(doc.xpath('//div[@class="2"]')[0]).decode())
<div class="2">
<p id="p3"><a href="/go-p3">item_3</a></p>
</div>
(2)contains
語法
html中有兩個<p>
標籤的class含有item
,如果我們要提取這兩個<p>
標籤,則:
In [54]: doc.xpath('//p[contains(@class, "item")]')
Out[54]: [<Element p at 0x7fe53a6a3ea8>, <Element p at 0x7fe53a6a3048>]
## 獲取<p>的文字:
In [55]: doc.xpath('//p[contains(@class, "item")]/text()')
Out[55]: ['item_1', 'item_2']
(3)starts-with
語法
跟(2)一樣的提取需求,兩個<p>
標籤的class都是以p_
開頭的,所以:
In [60]: doc.xpath('//p[starts-with(@class, "p_")]')
Out[60]: [<Element p at 0x7fe53a6a3ea8>, <Element p at 0x7fe53a6a3048>]
## 獲取<p>的文字:
In [61]: doc.xpath('//p[starts-with(@class, "p_")]/text()')
Out[61]: ['item_1', 'item_2']
(4)獲取某一屬性的值
比如,我們想提取網頁中所有的連結:
In [63]: doc.xpath('//@href')
Out[63]: ['/go-p3']
如果你有更多xpath的巧妙用法,歡迎分享出來,謝謝。
我的公眾號:猿人學 Python 上會分享更多心得體會,敬請關注。
***版權申明:若沒有特殊說明,文章皆是猿人學 yuanrenxue.com 原創,沒有猿人學授權,請勿以任何形式轉載。***