今天来总结最后一种说句解析的方式——XPath。
XPath是一门在XML文档中查找信息的语言,用于在XML文档中通过元素和属性进行导航。
下面来简单介绍一下XPath。
XPath的特点
- XPath使用路径表达式在XML文档中进行导航
- XPath包含一个标准函数库
- XPath是XSLT(Extensible Stylesheet Language Transformation)中的主要元素
- XPath是一个W3C标准
XPath解析方法
- 实例化一个etree对象,在实例化的时候讲被解析的页面源码数据加载到该对象中
- 调用etree的xpath方法结合xpath表达式实现标签的定位和数据的获取
环境的配置
XPath需要lxml库,lxml库的安装我们在前面讲bs4的时候已经说过了,可以通过pip直接安装。
pip3 install lxml
etree对象的实例化
和bs4一样,etree对象的实例化分两种情况
加载本地的html文件
加载本地html文件有两种方式,比方有个test.html文件
####################方法1#################### from lxml import etree with open('./test.html','r') as f: data = f.read() tree = etree.HTML(data) ####################方法2#################### from lxml import etree tree = etree.parse('./test.html',etree.HTMLParser())
一个是吧html代码读取以后用HTML方法处理,还有一种方法是直接指定路径,但是这种方法一定要指定一个解释器。
加载互联网上爬取的源码数据
加载网络爬取的数据的方法和上面的第一种方法一样,就是把拿到的字符串加载到HTML方法。
在拿到tree对象以后,我们要通过XPath表达式来定位到所需要的标签以及里面的内容 。假设我们本地存有一个test.html文件,内容和上一章的一样
<html> <head> <title> The Dormouse's story </title> </head> <body> <p class="title"> <b> The Dormouse's story </b> </p> <p class="story"> Once upon a time there were three little sisters; and their names were <a class="sister" href="http://example.com/elsie" id="link1"> Elsie </a> , <a class="sister" href="http://example.com/lacie" id="link2"> Lacie </a> and <a class="sister" href="http://example.com/tillie" id="link2"> Tillie </a> ; and they lived at the bottom of a well. </p> <p class="story"> ... </p> </body> </html>
然后实例化一个tree对象,并且把带解析的源码加载进去
from lxml import etree tree = etree.parse('./test.html',etree.HTMLParser())
下面我们就用这个tree对象来讲xpath到用法
XPath术语
想要了解XPath的用法,我们要先了解XPath的基本术语
节点(Node)
在XPath中有其中类型的节点:元素,属性,文本,命名空间,处理指令以及文档节点,整个文档是被作为节点树来对待的,树的根被称为文档节点或根节点。
基本值(Atomic Value)
又称原子值,无父或子节点
节点关系
节点关系分为父,子同胞,先辈,后代。顾名思义就是各个节点之间的关系,要注意的是先辈是包含父类关系的,而后代也包含子级关系的。
xpath节点选取
首先要了解最基础的xpath表达式
表达式 | 描述 |
nodename | 选取此节点的所有子节点 |
/ | 从根节点选取 |
// |
放在开始从匹配到当前节点选择文档中的节点,而不考虑其位置 放在两个标签内表示间隔多个标签层级 |
. | 选取当前节点 |
.. | 选取当前节点的父节点 |
@ | 选取属性 |
下面可以针对上面的表达式,结合test.html实例化的tree对象代码来演示一下
依靠节点
a_tags = tree.xpath('/html/body/p/a') print(a_tags) ##########输出########## [<Element a at 0x7fdb8f189e00>, <Element a at 0x7fdb8f226b00>, <Element a at 0x7fdb8f10f8c0>]
上面的代码就是搜索html——>body——>p——>a标签,返回值是一个列表,里面放到都是<class 'lxml.etree._Element'>对象
依靠属性定位
print(tree.xpath('//a[@class="sister"]')) ##########输出########## [<Element a at 0x7fdb8f36cfc0>, <Element a at 0x7fdb8f11d4c0>, <Element a at 0x7fdb8f099380>]
上面的代码就实现了通过指定class的值搜索到对应的a标签。
谓语
我们还可以通过谓语(Predicates)来超找某个特定的节点或者包含某个指定值得节点
在下面的表格中,列出来一些带有谓语的表达式和其对应的结果
表达式 | 结果 |
/html/body/p/a[1] | 选取a元素的第一个元素 |
/html/body/p/a[last()] | 获取最后一个a元素(注意带括号) |
/html/body/p/a[last()-1] | 获取倒数第二个a元素(注意-1的位置) |
/html/body/p/a[position()<3] | 获取最前面两个属于p元素的子元素a |
//a[@class] | 获取所有有class属性的a元素 |
//a[@class='sister] |
获取所有class属性值为sister的a元素 |
/html/body/div[p>20] | 获取html->body->div下值大于20的p标签(该用法常用于xml中) |
/html/body/div[p>20]/a | 获取html->body->div下单a标签,且其中p标签内的值应大于20 |
这里有个要注意点地方,就是在获取第n个元素的时候,在表达式和列表里切片的n的值出来的是不一样的
st = """ <html> <body> <ul> <li>0</li> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> </ul> <body> </html> """ tree = etree.HTML(st,etree.HTMLParser()) t1 = tree.xpath('/html/body/ul/li[1]/text()') print('t1',t1) t2 = tree.xpath('/html/body/ul/li/text()') print('t2',t2[1]) ##########输出########## t1 ['0'] t2 1
通配符
在匹配到时候还可以使用通配符来匹配任意一个节点
通配符 | 效果 | 案例 | 效果 |
* | 匹配任何元素节点 | /html/body/*/li | 匹配body标签下任意标签下单li标签 |
@* | 匹配任何属性节点 | //p[@*] | 匹配具有任意属性的p标签 |
node() | 匹配任何类型的节点 | //* | 选取文档中所有元素 |
多个路径的选取
在xpath表达式中我们可以通过管道符来选取多个路径,这里不在放案例了。
文本获取
在索引到需要的标签后我们就要获取标签里的文本。文本的获取有两个方法
- /text()
- //text()
注意,一个是单斜杠,另一个是双斜杠,效果如下
s=""" <div class='test'>div标签内<p>p标签内</p></div> """ tree = etree.HTML(s,etree.HTMLParser()) print('//',tree.xpath('//div//text()')) print('/',tree.xpath('//div/text()')) ##########输出########## // ['div标签内', 'p标签内'] / ['div标签内']
单斜杠是获取本标签直系的文本内容
双斜杠包含了本标签及后代标签内的文本内容。
下面结合一个案例来试一下XPath的使用
需求,从站长之家爬取免费的简历模板
url:https://sc.chinaz.com/jianli/free.html,要爬取5页的数据内容
直接放代码吧,看看怎么讲一下
1 import requests 2 from lxml import etree 3 4 for page_index in range(1,6): #爬取1-5页内容,指定url 5 if page_index ==1: #第一页的url和后面的规律不同 6 url = 'https://sc.chinaz.com/jianli/free.html' 7 else: 8 url = 'https://sc.chinaz.com/jianli/free_{}.html'.format(page_index) 9 10 page = requests.get(url=url) 11 page.encoding='utf-8' 12 page_text = page.text 13 14 15 tree = etree.HTML(page_text,etree.HTMLParser()) 16 17 tag = tree.xpath('//div[@class="box col3 ws_block masonry-brick"]') 18 19 tags = tree.xpath('//div[@id="main"]//div[contains(@class,"box") and contains(@class,"ws_block")]//p/a') 20 for tag in tags: 21 model_url = 'http:'+tag.xpath('./@href')[0] 22 model_name = tag.xpath('./text()')[0] 23 print(model_url) 24 25 model_page_text = requests.get(url=model_url).text 26 model_tree = etree.HTML(model_page_text,etree.HTMLParser()) 27 28 model_link = model_tree.xpath('//ul[@class="clearfix"]//a/@href')[0] 29 30 31 print(model_link) 32 file_name = model_name+'.rar' 33 34 with open('./简历模板/'+file_name,'wb') as f: 35 data = requests.get(url=model_link).content 36 f.write(data) 37 print(file_name,'finished!') 38 39 print('----------page{}finish!----------')
就是分了三个层次
第一层先爬取主页内容,在主页上有若干模板的子链接(页面2),第二层爬取页面2的内容,获取到待下载文件的名称以及链接地址;第三层直接爬取文件(二进制的压缩包),爬取后永久化存储至本地。
这里有个问题,运行一段时间后可能会报一个错误:Httpconnectionpool,是因为短时间发起来高频的请求导致ip被禁,或者http连接处中的连接资源被耗尽。针对第一个我们在下一章会讲到请求代理操作。
第二个可以在headers里加一个新的键值对:Conection:"close"