Python爬虫技术系列-02HTML解析-xpath与lxml

2 XPath介绍与lxml库

参考连接:
XPath教程
https://www.w3school.com.cn/xpath/index.asp
lxml文档
https://lxml.de/index.html#support-the-project
爬虫专栏
https://blog.csdn.net/m0_38139250/category_12001010.html

2.1 XPath概述

XPath的中文名称为XML路径语言(XML Path Language),其最初的设计是用来搜索 XML 文档,但也适用于HTML文档搜索。1996年11月,XPath 成为W3C标准, XQuery 和 XPointer 都构建于 XPath 表达之上。
XPath有着强大的搜索选择功能,提供了简洁的路径选择表达式, 提供了100+的内建函数,可以完成XML和HTML的绝大部分的定位搜索需求。
XML和HTML均可通过树形结构的DOM(文档对象模型,Document Object Model)表示,DOM中包含元素节点,文本节点,属性节点三种节点。

其中元素节点是DOM的基础,元素就是DOM中的标签,
如<html>是根元素,代表整个文档,其他的元素还包括<head><body><div><ul><span>等,元素节点之间可以相互包含。

文本节点:包含在元素节点中,
比如<span>文本节点</span>。

属性节点:元素节点可以包含一些属性,属性的作用是对元素做出更具体的描述,
如<span class="属性节点值">文本节点</span>

XPath的核心思想就是写地址,通过地址查找到XML和HTML中的元素,文本,属性等信息。
获取元素n:

//标签[@属性1="属性值1"]/标签[@属性2="属性值2"]/.../标签n

获取文本:

//标签[@属性1="属性值1"]/标签[@属性2="属性值2"]/.../text()

获取属性n的值:

//标签[@属性1="属性值1"]/标签[@属性2="属性值2"]/.../@属性n

[@属性1=“属性值1”]是谓语,用于过滤相同的标签,如果不需要通过属性过滤标签,可以不加谓语过滤。
下面介绍XPath的节点类型和常用语法。

1)节点(Node): XPath包括元素、属性、文本、命名空间、处理指令、注释以及文档(根)等七种类型的节点。XML 文档是被作为节点树来对待的。树的根被称为文档节点或者根节点。节点之间的关系包括父(Parent),子(Children),同胞(Sibling),先辈(Ancestor),后代(Descendant)。

2)语法:
XPath中,通过路径(Path)和步(Step)在XML文档中获取节点。
a.常用的路径表达式
常见的路径表达式如下表所示:
表 XPath表达式与示例
在这里插入图片描述b.谓语(Predicates)
为查找特点节点或包含某个指定值的节点,可以使用谓语(Predicates),谓语用方括号[]表示,如:
//div[@class=‘useful’]
表示选取所有div 元素,且这些元素拥有值为 useful的 class属性。
//div[@class=‘useful’]//li[last()]
表示选取具有class值为useful的div标签下的任意li元素的最后一个li元素。
c.选取未知节点
XPath可以通过通配符搜索未知节点,如*表示匹配任何元素,@*表示匹配任何带有属性的节点,node()表示匹配任何类型的节点。如:
//title[@*]
表示选取所有带有属性的title元素。
d.选取若干路径
XPath可以通过“|”运算符表示选取若干路径。如
//title | //price
表示选取文档中的所有 title 和 price 元素

3)轴与步:
a.XPath轴(axis)
轴表示当前节点的节点集XPath轴的名称见表13-2所示:
表13-2 XPath轴名称与结果
在这里插入图片描述
b.步(Step)
步可以根据当前节点集中的节点来进行计算搜索。
步的语法:
轴名称::节点测试[谓语]
其中,轴(axis)表示所选节点与当前节点之间的关系,节点测试(node-test)表示是某给定轴内部的节点,谓语(predicate)用于搜索特定的节点集。
步的使用如表13-3所示:
在这里插入图片描述
步的使用案例如下:
//div[@class=“useless”]/descendant::a’)
获取任意class属性值为useless的div标签下得所有子孙a标签节点。

2.2 lxml库介绍

Web数据展示都通过HTML格式,如果采用正则表达式匹配lxml是Python中的第三方库,主要用于处理搜索XML和HTML格式数据。

2.2.1 lxml库安装

安装lxml:

pip install lxml==4.8.0 -i https://pypi.tuna.tsinghua.edu.cn/simple

如果安装不成,可以在

https://www.lfd.uci.edu/~gohlke/pythonlibs/

下载对应的whl安装包,然后安装即可。
如果部分读者还是安装不成,可以把whl包解压,然后把解压后的两个文件夹放在python安装文件夹下的Lib\site-packages目录下即可。

2.2.2 lxml库基本使用

lxml的使用首先需要导入lxml的etree模块:

from lxml import etree

etree模块可以对HTML文件进行自动修正,lxml中的相关使用方法如下:
读取数据:

etree.HTML(text, parser=None, base_url=None,)

第一个参数text为一个字符串,字符串应该可以转换为HTML或XML文档,如果字符串中的标签存在不闭合等问题,本方法会自动修正,并把文本转换成为HTML格式文档。返回结果类型为’lxml.etree._Element’。

etree.fromstring(text, parser=None, base_url=None)

与etree.HTML()类似,但转换过程中,要求text字符串为标准的XML或HTML格式,否则会抛出异常。返回结果类型为’lxml.etree._Element’。

etree.parse(source, parser=None, base_url=None) 

可如果没有解析器作为第二个参数提供,则使用默认解析器。返回一个加载了源元素的ElementTree对象,返回结果类型为’lxml.etree._ElementTree’。

etree.tostring(element_or_tree, encoding=None,)

输出修正后的HTML代码,返回结果为bytes类型。

搜索数据:
假定有变量html为etree模块读取数据后返回’lxml.etree._Element’或’lxml.etree._ElementTree’类型,可以调用:

html.xpath(self, _path, namespaces=None, extensions=None, smart_strings=True, **_variables)

_path为xpath中的路径表达式和步,xpath函数可以通过_path参数值实现对文档的搜索。

2.2.3 lxml案例

下面根据具体案例来介绍lxml的基本使用。

a.读取数据并补全
from lxml import etree
# 定义一个不规则的html文本
text = '''
<html><body><div><ul>
        <li class="item-0">01 item</a></li>
'''

html = etree.HTML(text)  # etree把不规则文本进行修正
complete_html = etree.tostring(html)  # toString可输出修正后的HTML代码,返回结果为bytes
print("原数据------->", text)
print("修正后的数据--->\n",complete_html.decode('utf-8'))  # 输出修正后的html

输出结果如下:

原数据-------> 
<html><body><div><ul>
        <li class="item-0">01 item</a></li>

修正后的数据--->
 <html><body><div><ul>
        <li class="item-0">01 item</li>
</ul></div></body></html>

从输出结果可以看出,etree.toString()可以对缺少闭合标签的HTML文档进行自动修正。

etree模块可以调用HTML读取字符串,也可以调用parse()方法读取一个HTML格式的文件。把上面代码中的text变量保存在文本文件中,文件命名为lxml.html。

from lxml import etree

# 读取html文件
html = etree.parse("./lxml.html",etree.HTMLParser())  # etree把不规则文本进行修正
complete_html = etree.tostring(html)  # toString可输出修正后的HTML代码,返回结果为bytes
print(complete_html.decode('utf-8'))

输出结果如下:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>&#13;
<div><ul>&#13;
    <li class="item-0"><a href="/link1.html">01 item</a></li></ul></div></body></html>

从输出结果看可以看出,etree完成了HTML自动修正,同时还加上了!DOCTYPE标签。

b.读取数据并选取节点:

创建Demo11-03.html文件,内容如下:

<!DOCTYPE html>
<html>
<body>
<div class="useful">
    <ul>
        <li class="cla-0" id="id-0"><a href="/link1">01</a></li>
        <li class="cla-1"><a href="/link2">02</a></li>
        <li><strong><a href="/link3">03</a></strong></li>
        <li class="cla-1"><a href="/link4">04</a></li>
        <li class="cla-0"><a href="/link5">05</a></li>
    </ul>
</div>
<div class="useless">
    <ul>
        <li class="cla-0"><a href="/link1">useless-01</a></li>
        <li class="cla-1"><a href="/link2">useless-02</a></li>
    </ul>
</div>
</body>
</html>

导入库,并通过etree读取html文档:

from lxml import etree
# 加载HTML文档
html = etree.parse("./Demo11-03.html",etree.HTMLParser())

00.获取根路径的div元素:

print('--result00----/div-----')
result00 = html.xpath('/div')  # 匹配/div节点
print(result00)

输出如下:

--result00----/div-----
[]

因为根路径下标签为,所以无法匹配度根路径下的div标签。

01.获取任意路径的div元素:

print('--result01----/div-----')
result00 = html.xpath('/div')  # 匹配所有div节点
print(result01)

输出如下:

--result01----//div-----
[<Element div at 0x182e1169e80>, <Element div at 0x182e1169e00>]

匹配到两个div元素。//表示任意路径。

02.获取任意路径div元素的所以子节点:

print('--result02----//div/*-----')
result02 = html.xpath('//div/*')  # 匹配所有div节点的子节点
print(result02)

输出如下:

--result02----//div/*-----
[<Element ul at 0x182e1169f80>, <Element ul at 0x182e1169fc0>]

*表示匹配任意节点。

03.匹配所有class属性的值:

print('--result03----//@class-----')
result03 = html.xpath('//@class')  # 匹配所有class属性的值
print(result03)

输出如下:

--result03----//@class-----
['useful', 'cla-0', 'cla-1', 'cla-1', 'cla-0', 'useless', 'cla-0', 'cla-1']

@class表示获取属性class的值。

04.获取任意路径下li标签的a标签子节点:

print('--result04----//li/a-----')
result04 = html.xpath('//li/a')  # 匹配所有li标签下的子节点a标签
print(result04)

输出如下:

--result04----//li/a-----
[<Element a at 0x182e116a400>, <Element a at 0x182e116a480>, <Element a at 0x182e116a4c0>, <Element a at 0x182e116a500>, <Element a at 0x182e116a540>, <Element a at 0x182e116a5c0>]

原始数据中一共7个a标签,返回值为6个a标签,是因为如下原始数据

<li><strong><a href="/link3">03</a></strong></li>

a标签不是li标签的子节点。

05.获取任意路径下li标签的任意a标签子孙节点:

print('--result05----//li//a-----')
result05 = html.xpath('//li//a')  # 匹配所有li标签下的所有a标签
print(result05)

输出如下:

--result05----//li//a-----
[<Element a at 0x182e116a400>, <Element a at 0x182e116a480>, <Element a at 0x182e116a600>, <Element a at 0x182e116a4c0>, <Element a at 0x182e116a500>, <Element a at 0x182e116a540>, <Element a at 0x182e116a5c0>]

原始数据中一共7个a标签,返回值为7个a标签,全部获取到。

06. 匹配具有herf属性为/link2的元素的父元素的class属性的值:

print('--result06----//a[@href="/link2"]/../@class-----')
result06 = html.xpath('//a[@href="/link2"]/../@class')print(result06)

输出如下:

--result06----//a[@href="/link2"]/../@class-----
['cla-1', 'cla-1']

…表示当前节点的父元素。

07.查找所有class="cla-0"的li节点:

print('--result07----//li[@class="cla-0"]-----')
result07 = html.xpath('//li[@class="cla-0"]')  # 查找所有class="cla-0"的li节点:
print(result07)

输出如下:

--result07----//li[@class="cla-0"]-----
[<Element li at 0x182e116a140>, <Element li at 0x182e116a2c0>, <Element li at 0x182e116a3c0>]

//表示匹配任意路径,[]代表谓语,@class="cla-0"代表过滤出class属性值为cla-0的元素。

08.获取a节点下的文本:

print('--result08----//li[@class="cla-0"]/a/text()-----')
result08_1 = html.xpath('//li[@class="cla-0"]/a/text()')  # 先选取a节点,再获取a节点下的文本
print(result08_1)

输出如下:

--result08----//li[@class="cla-0"]/a/text()-----
['01', '05', 'useless-01']

text()表示获取匹配节点的文本内容。

09.获取li节点下a节点的href属性:

print('--result09----//li/a/@href-----')
result09 = html.xpath('//li/a/@href')  # 获取li节点下a节点的href属性
print(result09)

输出如下:

--result09----//li/a/@href-----
['/link1', '/link2', '/link4', '/link5', '/link1', '/link2']

//li/a/@href表示匹配任意路径下的li元素的a标签子节点的href属性值。

10.获取li节点下所有a节点的href属性:

print('--result10----//li//a/@href-----')
result10 = html.xpath('//li//a/@href')  # 获取li节点下所有a节点的href属性
print(result10)

输出如下:

--result10----//li//a/@href-----
['/link1', '/link2', '/link3', '/link4', '/link5', '/link1', '/link2']

相比result9,本次结果匹配到了/link3。

11.获取class属性值包含-0的li元素下的a标签的文本:

print('--result11----//li[contains(@class,"-0")]/a/text()-----')
result11 = html.xpath('//li[contains(@class,"-0")]/a/text()') # 获取class属性值包含-0的li元素下的a标签的文本
print(result11)

输出如下:

--result11----//li[contains(@class,"-0")]/a/text()-----
['01', '05', 'useless-01']

contains(@class,“-0”)表示过滤条件为class属性包含-0。于此类似的还有starts-with,starts-with表示以什么开头。

12.用多个属性获取:

print('--result12----//li[contains(@class,"-0") and @id="id-0"]/a/text()-----')
result12 = html.xpath('//li[contains(@class,"-0") and @id="id-0"]/a/text()')  # 多个属性用and运算符来连接
print(result12)

输出如下:

--result12----//li[contains(@class,"-0") and @id="id-0"]/a/text()-----
['01']

contains(@class,“-0”) and @id="id-0"表示待匹配的元素需要具有满足以上两种条件。and 操作符也可以替换为or 操作符。由于同时包含两种属性条件的a标签只有一个,所以返回的文本只有01。

13.按照顺序获取节点:

print('--result13----//li[last()]/a/text()-----')
result13 = html.xpath('//li[last()]/a/text()')  # 取最后一个li节点下a节点的文本
print(result13)

输出如下:

--result13----//li[last()]/a/text()-----
['05', 'useless-02']

返回结果表示,通过last()返回了两个li列表中的最后一个节点。

14.通过ancestor轴获取所有的祖先节点:

print('--result14----//li[1]/ancestor::*-----')
result14 = html.xpath('//li[1]/ancestor::*') # ancestor轴可以获取所有的祖先节点
print(result14)

输出如下:

--result14----//li[1]/ancestor::*-----
[<Element html at 0x182e0de9c80>, <Element body at 0x182e116abc0>, <Element div at 0x182e1169e80>, <Element ul at 0x182e1169f80>, <Element div at 0x182e1169e00>, <Element ul at 0x182e1169fc0>]

//li[1]表示获取任意路径的li中的第一个元素,/ancestor::*表示获取当前节点的任意祖先节点。

15.通过ancestor轴获取祖先div节点:

print('--result15----//li[1]/ancestor::div-----')
# 只获取div这个祖先节点
result15 = html.xpath('//li[1]/ancestor::div')
print(result15)
for result15_1 in result15:
    print(result15_1.xpath('.//li[contains(@class,"-0")]/a/text()'))

输出如下:

--result15----//li[1]/ancestor::div-----
[<Element div at 0x182e1169e80>, <Element div at 0x182e1169e00>]
['01', '05']
['useless-01']

result15的返回结果为div节点,然后对result15进行遍历,在遍历中,通过xpath路径进一步获取a标签的文本。这里需要注意的是循环内的xpath路径以“.”开头,表示相对于当前div元素下,第一次输出为[‘01’, ‘05’],第二次输出为[‘useless-01’]。如果循环内的xpath路径去掉“.”,则循环内的两次输出是一致,应该都为[‘01’, ‘05’, ‘useless-01’]。

16.获取所有属性值:

print('--result16----//li[1]/attribute::*-----')
result16 = html.xpath('.//li[1]/attribute::*')
print(result16)

输出如下:

--result16----//li[1]/attribute::*-----
['cla-0', 'id-0', 'cla-0']

输出结果为所有li中的第1个节点的属性值。

17.获取所有子孙节点a标签:

print('--result17----//div/descendant::a-----')
result17 = html.xpath('//div[@class="useless"]/descendant::a')
print(result17)

输出如下:

--result17----//div/descendant::a-----
[<Element a at 0x1f34cf2a540>, <Element a at 0x1f34cf2a5c0>]

descendant表示匹配子孙节点。
以上就是lxml的基本操作,更多操作可以自行组合或参考官网,需要说明的是,在浏览器端通过开发者工具–查看器–选择元素–右键复制–选择XPath路径,可以获取选择元素的XPath路径,通过这种方法可以加快XPath路径构建。
另外需要注意的是,xpath()函数的返回值为列表,可以通过先抓取外层的数据,然后通过遍历或是索引的方式获取节点数据,然后通过相对路径的方式进一步读取内层元素节点。案例如下:

18.先获取外层元素,再通过相对路径的方式获取内部元素:
print('--result18----//li[1]/ancestor::div-----')
result18 = html.xpath('//li[1]/ancestor::div')
print(result18)
print(result18[0].xpath('./ul/li/a/text()'))

在上面代码中 ,result18[0]表示获取列表中的第一个Element 类型元素,然后对Element 类型元素进行xpath操作。./ul/li/a/text()中的“.”表示相对当前节点。
输出为:

--result18----//li[1]/ancestor::div-----
[<Element div at 0x28e3b83a000>, <Element div at 0x28e3b83a0c0>]
['01', '02', '04', '05']

2.3 urllib整合lxml

urllib获取百度数据

from urllib import parse
from urllib import request
url='http://www.baidu.com/s?'
dict_data={'wd':'百度翻译'}
#unlencode() 将字典{k 1:v 1,k2:v2}转化为k1=v1&k2=v2
url_data=parse.urlencode(dict_data)
#urldata:wd=%E7%99%BE%E5%BA%A6%E7%BF%BB%E8%AF%91
print(url_data)#读取URL响应结果
response_data=request.urlopen((url+url_data))#用utf 8对响应结果编码
data=response_data.read().decode('utf-8')
print(data)

输出为:

wd=%E7%99%BE%E5%BA%A6%E7%BF%BB%E8%AF%91
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <title>百度安全验证</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0">
    <meta name="format-detection" content="telephone=no, email=no">
    <link rel="shortcut icon" href="https://www.baidu.com/favicon.ico" type="image/x-icon">
    <link rel="icon" sizes="any" mask href="https://www.baidu.com/img/baidu.svg">
    <meta http-equiv="X-UA-Compatible" content="IE=Edge">
    <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
    <link rel="stylesheet" href="https://ppui-static-wap.cdn.bcebos.com/static/touch/css/api/mkdjump_aac6df1.css" />
</head>
<body>
    <div class="timeout hide-callback">
        <div class="timeout-img"></div>
        <div class="timeout-title">网络不给力,请稍后重试</div>
        <button type="button" class="timeout-button">返回首页</button>
    </div>
    <div class="timeout-feedback hide-callback">
        <div class="timeout-feedback-icon"></div>
        <p class="timeout-feedback-title">问题反馈</p>
    </div>

<script src="https://ppui-static-wap.cdn.bcebos.com/static/touch/js/mkdjump_v2_21d1ae1.js"></script>
</body>
</html>

基于lxml进行解析百度数据

from lxml import etree
# 定义一个不规则的html文本

html = etree.HTML(data)  # etree把不规则文本进行修正
res = html.xpath("//body[1]//div[@class='timeout-title']/text()")
print(res)

输出为:

[‘网络不给力,请稍后重试’]

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 44
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 44
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT从业者张某某

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值