概述
在爬虫工作中,对页面的解析工作是不可避免的,因此如何准确高效的匹配出目标信息,对于数据的提取尤为重要。对于网页的节点来说,它可以定义 id、class 或其他属性。而且节点之间还有层次关系,在网页中可以通过 XPath 或 CSS 选择器来定位一个或多个节点。
在 Python 中,除了可以继续使用正则表达式外,还提供了一系列解析库,其中比较强大的库有 lxml、Beautiful Soup、pyquery 等,在实际工作中,根据个人习惯选择适合自己的便捷解析方式,不管是通过 Xpath 和 CSS 选择器来定位解析数据,对于一个懂点前端知识的人来说,都是非常合适。
XPath
XPath,全称 XML Path Language,即 XML 路径语言,它是一门在 XML 文档中查找信息的语言。它最初是用来搜寻 XML 文档的,但是它同样适用于 HTML 文档的搜索。在 Python 中通过 lxml 库来实现 XPath 解析。
pyquery
pyquery:一个类似 jquery 的 python 库。pyquery 允许您对 xml 文档进行 jquery 查询。API 尽可能与 jquery 类似。
实战分析
XPath 的使用
XPath 的几个常见规则
比较常用的组合方式如下:
//div[@class="xxx"]/ul #全局搜索class名称为xxx的div模块,然后再在该div下搜索它的子节点ul,返回结果如:[<Element ul at 0x2b16b9d2c88>]
//div[@id="xxx"]/ul #全局搜索id名称为xxx的div模块
现在通过实例来感受一下使用 XPath 来对网页进行解析的过程,相关代码如下:
输出文本
from lxml import etree
text= '''
<div>
<ul class="list">
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a></li>
</ul>
</div>
‘’‘
html = etree.HTML(text) #使用该模块对字符串进行处理,返回符合HTML格式的文本
result = etree.tostring(html) #输出HTML文本,格式为byte类型
print(result.decode('utf-8')) #byte转str
上述代码是为了输出即将要解析的文本内容。
子节点
1、当节点没有属性名称时,需要按照节点嵌套顺序进行定位
html = etree.HTML(text)
result = html.xpath('//ul/li')
#result = html.xpath('//ul/a') #由于ul节点下的直接节点为li,无法定位到a节点,a节点是li下的直接节点
print(result)
执行结果为:
[<Element li at 0x2b16b9d2d08>, <Element li at 0x2b16b9d2d48>, <Element li at 0x2b16b9d2d88>, <Element li at 0x2b16b9d2dc8>, <Element li at 0x2b16b9d2e08>]
2、当节点有属性名称时,可以通过属性进行精确定位
html = etree.HTML(text)
result = html.xpath('//ul[@class="list"]//span[@class="bold"]')
print(result)
执行结果为:
[<Element span at 0x26f74552b88>]
在以上的结果可以看出提取结果是一个列表形式,其中每个元素都是一个 Element 对象。如果要取出其中一个对象,可以直接用中括号加索引,如[0]。
文本获取
html = etree.HTML(text)
result = html.xpath('//ul[@class="list"]')
print([a.text for a in result[0].xpath('li')])
print(result[0].xpath('li/a/text()'))
print(result[0].xpath('li//text()'))
result = html.xpath('//ul[@class="list"]//span[@class="bold"]/text()')
print(result)
执行结果为:
['first item', None, None, None, None]
['second item', 'fourth item', 'fifth item']
['first item', 'second item', 'third item', 'fourth item', 'fifth item']
['third item']
根据结果进行分析,如果想获取 li 节点内部的文本,就有两种方式,一种是按照嵌套顺序定位到直接与文本相接的节点,或通过属性进行定位,另一种就是使用 //。
属性获取
html = etree.HTML(text)
result = html.xpath('//li/a/@href')
print(result)
result = html.xpath('//li/a')[0].attrib['href']
print(result)
执行结果为:
['link2.html', 'link3.html', 'link4.html', 'link5.html']
link2.html
通过 @href 即可获取节点的 href 属性,或者是通过 attrib()
方法进行获取。如果是别的名称的属性,同理进行操作即可。
属性多值匹配
html = etree.HTML(text)
result = html.xpath('//li[@class="item-0"]//text()')
print(result)
result = html.xpath('//li[contains(@class,"item-0")]//text()')
print(result)
执行结果为:
['first item', 'fifth item']
['first item', 'third item', 'fifth item']
根据属性模糊匹配的时候,需要用到 contains()
方法,第一个参数传入属性名称,第二个参数传入属性值,只要此属性包含所传入的属性值,就可以完成匹配了。
按序选择
html = '''
<div class="wrap">
<div id="container">
<ul class="list">
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a></li>
</ul>
</div>
</div>
'''
html = etree.HTML(html)
print(html.xpath('//ul/li[2]//text()'))
result = html.xpath('//li[last()]/a/text()')
print(result)
result = html.xpath('//li[position()<3]/a/text()')
print(result)
result = html.xpath('//li[last()-2]/a//text()')
print(result)
执行结果为:
['second item']
['fifth item']
['second item']
['third item']
在选择的时候某些属性可能同时匹配了多个节点,但是只想要其中的某个节点,如第二个节点或者最后一个节点,又或者没法通过属性进行匹配的时候。利用中括号传入索引的方法获取特定次序的节点。注意,中括号传入的数值和代码中不同,序号是以 1 开头的,不是以 0 开头。
遍历不同子节点
from lxml import etree
html = '''
<div class="wrap">
<div id="container">
<ul class="list">
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a></li>
</ul>
<ol class="list">
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a></li>
</ol>
</div>
</div>
'''
html = etree.HTML(html)
result = html.xpath('//div[@id="container"]/child::*')
for elem in result:
if elem.tag == 'ul':
print('ul--------result:')
for e in elem.xpath('li//text()'):
print(e)
if elem.tag == 'ol':
print('ol--------result:')
for e in elem.xpath('li[@class="item-0"]//text()'):
print(e)
执行结果为:
ul--------result:
first item
second item
third item
fourth item
fifth item
ol--------result:
first item
fifth item
pyquery 的使用
输出文本
from pyquery import PyQuery as pq
html = '''
<div>
<ul class="list">
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a></li>
</ul>
</div>
'''
doc = pq(html)
print(doc)
子节点
doc = pq(html)
print(doc('#idxx li'))
print(type(doc('.list li')))
items = doc('.list li').items()
print(type(items))
for item in items:
print(item)
items = doc('.list')
lis = items.find('li')
print(lis)
执行结果为:
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a></li>
<class 'pyquery.pyquery.PyQuery'>
<class 'generator'>
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a></li>
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a></li>
通过结果可以看出,CSS 选择器定位的时候,class 属性一般用 .属性值
来识别,id 属性用 #属性值
匹配,不必严格按照节点嵌套顺序来写。返回结果类型为 PyQuery 类型,不同于 XPath 返回结果为列表类型,如果需要遍历结果,则需要调用 items()
方法。
文本获取
doc = pq(html)
items = doc('.list li').items()
print([item.text() for item in items])
print(doc('li').text())
执行结果为:
['first item', 'second item', 'third item', 'fourth item', 'fifth item']
first item second item third item fourth item fifth item
pyquery 相对于 XPath 来说,获取文本相对比较简单。text()
则返回了节点内部的纯文本,不需要定位到与文本直接相连的节点。如果返回的文本有多个,则用空格分开,为字符串类型,无法遍历。
属性获取
doc = pq(html)
a = doc('.item-0.active a')
print(a.attr('href'))
a = doc('.active a')
print(a.attr('href'))
a_items = doc('a').items()
for item in a_items:
print(item.attr('href'))
执行结果为:
link3.html
link3.html
link2.html
link3.html
link4.html
link5.html
调用 attr()
方法。在这个方法中传入属性的名称,就可以得到相应节点的属性值。当选中的是节点返回多个元素时,则需要进行遍历。
属性多值匹配
doc = pq(html)
a = doc('.item-0.active a')
print(a.attr('href'))
a = doc('.active a')
print(a.attr('href'))
执行结果为:
link3.html
link3.html
当属性较长含有空格时,可以分别用 .属性值
相连的方式进行匹配,或者单独匹配特有的一段属性值。
伪类选择器
CSS 选择器之所以强大,还有一个很重要的原因,那就是它支持多种多样的伪类选择器,例如选择第一个节点、最后一个节点、奇偶数节点、包含某一文本的节点等。
html = '''
<div class="wrap">
<div id="container">
<ul class="list">
<li class="item-0">first item</li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-0 active"><a href="link3.html"><span class="bold">third item</span></a></li>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a></li>
</ul>
</div>
</div>
'''
from pyquery import PyQuery as pq
doc = pq(html)
li = doc('li:first-child')
print(li)
li = doc('li:last-child')
print(li)
li = doc('li:nth-child(2)')
print(li)
li = doc('li:gt(2)')
print(li)
li = doc('li:nth-child(2n)')
print(li)
执行结果为:
<li class="item-0">first item</li>
<li class="item-0"><a href="link5.html">fifth item</a></li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
<li class="item-0"><a href="link5.html">fifth item</a></li>
<li class="item-1"><a href="link2.html">second item</a></li>
<li class="item-1 active"><a href="link4.html">fourth item</a></li>
在实际提取网页数据的过程中,有些情况下是无法通过属性进行匹配的,为了准确定位到某一节点,就需要通过伪类选择器来进行点位。
总结
XPath 和 pyquery 都比较高效便捷,具体选择哪种使用方法,根据个人习惯进行选择,两者在功能上基本都一致,如果对 CSS 样式有所了解的话,选择 pyquery 比较容易一些。