Xpath和lxml
一、内容介绍
尽管正则表达式很厉害,但是写出功能强大的正则表达式不容易,而且遇到不同的页面就要重写,难以维护(确实)
Xpath非常容易理解的路径方式选择XML和HTML中的节点,容易维护和编写
本章主要内容:
1、安装lxml
2、用lxml操作XML和HTML文档
3、XPath的基本概念
4、用XPath选取节点(所有节点、子节点、父节点等)
5、用Xpath匹配和选取属性
6、按序选取节点
7、节点轴
8、实战案例,演示使用requests和XPath花去和分析HTML代码
二、知识点干货
8.1 解析xml文件实例
- product.xml文件:
<products>
<product id = "0001">
<name>手机</name>
<price>4500</price>
</product>
<product id = "0002">
<name>电脑</name>
<price>6000</price>
</product>
</products>
- py解析文件:
from lxml import etree
# 读取products.xml文件
tree = etree.parse('products.xml')
print(type(tree))
# 将tree重新转换为字符串形式的XML文档,并输出
print(str(etree.tostring(tree,encoding="utf-8"),'utf-8'))
# 获得根节点对象
root = tree.getroot()
print(type(root))
# 输出根节点名称
print('root:',root.tag)
# 获得根节点的所有子节点
children = root.getchildren()
print('----------输出产品信息----------')
# 迭代这些子节点,并输出对应的属性和节点文本
for child in children:
print('product id = ', child.get('id'))
print('child[0].name:',child[0].text)
print('child[1].price:', child[1].text)
# 分析字符串形式的XML文档
root = etree.fromstring('''
<products>
<prduct1 name='iPhone'/>
<prduct2 name='iPAD'/>
</products>
''')
print('-----新产品-----')
# 输出根节点的节点名
print('root=',root.tag)
children = root.getchildren()
# 迭代这些子节点,并输出节点的名称和name属性名
for child in children:
print(child.tag,'name = ',child.get('name'))
8.2 解析html实例
读取test.html的HTML文件,并输出根节点以及其他节点的属性值和文本
- test.html文件
<html lang="en"><head><meta charset="UTF-8">
<title>通过XPath过滤的HTML文档</title>
</head>
<body>
</body>
</html>
- py解析文件
from lxml import etree
# 创建lxml.etree.HTMLParser对象
parser = etree.HTMLParser()
print(type(parser))
# 读取并解析test.html文件
tree = etree.parse('test.html', parser)
# 获取根节点
root = tree.getroot()
# 将html文档转换为可读模式
result = etree.tostring(root,encoding='utf-8',pretty_print=True,method="html")
print(str(result,'utf-8'))
# 输出根节点的名称
print(root.tag)
# 输出根节点的lang属性的值
print('lang =',root.get('lang'))
# 输出meta节点的charset属性的值
print('charset =',root[0][0].get('charset'))
# 输出title节点的文本
print('charset =',root[0][1].text)
8.3 lxml库和XPath结合实例
lxml装载HTML代码 有两种方式
1、从文件装载,通过parse函数指定HTML文件名
2、从代码装载,通过HTML函数指定HTML代码
Xpath语法规则:
nodename :选取此节点的所有子节点
/ :从当前节点选取直接子节点
// :从当前节点选取子孙节点
. :选取当前节点
.. :选取当前节点的父节点
@ :选取属性
- 使用lxml库和XPath选取test.html文件中标题
from lxml import etree
parser = etree.HTMLParser()
tree = etree.parse('test.html',parser)
# 使用XPath定位title节点,返回一个节点集合
titles = tree.xpath('/html/head/title')
if len(titles) > 0:
# 输出title节点的文本
print(titles[0].text)
- 一段HTML代码中特定节点中的href属性值和节点文本
# 定义一段HTML代码
html = '''
<div>
<ul>
<li class="item1"><a href="https://geekori.com">geekori.com</a></li>
<li class="item2"><a href="https://www.jd.com">京东商城</a></li>
<li class="item3"><a href="https://www.taobao.com">淘宝</a></li>
</ul>
</div>
'''
# 分析HTML代码
tree = etree.HTML(html)
# 使用XPath定位class属性值为item2的<li>节点
aTags = tree.xpath("//li[@class='item2']")
if len(aTags)>0:
# 得到该<li>节点中<a>节点的href属性值和文本
print(aTags[0][0].get('href'),aTags[0][0].text)
8.4 allnode获取所有节点
语法规则:
// 会选取所有符合要求的节点
//* 会选取整个HTML文档中所有的节点
//li 会选取所有的<li>节点
- demo.html文件内容
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>XPath演示</title>
</head>
<body>
<div>
<ul>
<li class="item1"><a href="https://geekori.com"> geekori.com</a></li>
<li class="item2"><a href="https://www.jd.com"> 京东商城</a></li>
<li class="item3"><a href="https://www.taobao.com">淘宝</a></li>
<li class="item4" value="1234"><a href="https://www.microsoft.com">微软</a></li>
<li class="item5"><a href="https://www.google.com">谷歌</a></li>
</ul>
</div>
</body>
</html>
- py解析文件
from lxml import etree
parser = etree.HTMLParser()
html = etree.parse('demo.html', parser)
# 选取demo.html文件中所有的节点
nodes = html.xpath('//*')
print('共',len(nodes),'个节点')
print(nodes)
# 输出所有节点,indent是缩进
def printNodeTree(node,indent):
print(indent + node.tag)
indent += " "
children = node.getchildren()
if len(children) > 0:
for i in range(0, len(children)):
# 递归调用
printNodeTree(children[i],indent)
print()
# 按层次输出节点的节点,nodes[0]是根节点(html节点)
printNodeTree(nodes[0],"")
# 选取demo.html文件汇总所有的<a>节点
nodes = html.xpath('//a')
print()
print('共', len(nodes), '个<a>节点')
print(nodes)
# 输出所有<a>节点的文本
for i in range(0, len(nodes)):
print(nodes[i].text, end=' ')
解析文件中用了递归调用的方式,可以看到html文件中节点的结构
8.5 childnode节点
使用XPath根据不同的规则选取demo.html文件中所有<a>节点,并输出<a>节点的文本
值得注意的是 孙子节点、子节点和后代节点 的区别
from lxml import etree
parser = etree.HTMLParser()
html = etree.parse('demo.html', parser)
# 成功选取<a>节点
# //直接选取此节点,/直接选取此节点下的子节点
nodes = html.xpath('//li/a')
print("共{}个节点".format(len(nodes)))
print(nodes)
for i in range(0,len(nodes)):
print(nodes[i].text,end=' ')
print()
# 成功选取<a>
# //直接选取此节点,但a不是ul的子节点,所以再用一个//a选取ul节点下的所有a节点
nodes = html.xpath('//ul//a')
print("共{}个节点".format(len(nodes)))
print(nodes)
for i in range(0,len(nodes)):
print(nodes[i].text,end=' ')
print()
# 无法选取<a>节点,因为<a>不是<ul>的直接子节点
nodes = html.xpath('//ul/a')
print(nodes)
for i in range(0,len(nodes)):
print(nodes[i].text,end=' ')
8.6 parentnode父节点
使用XPath选取特定<a>节点的父节点(<li>节点),并输出父节点的class属性值
知道子节点,得到父节点的方法:
1、可以使用..
2、也可以使用parent::*
使用demo.html测试上述规则,案例如下:
from lxml import etree
parser = etree.HTMLParser()
html = etree.parse('demo.html',parser)
# 选取href属性值为https://www.jd.com的<a>节点的父节点,并输出父节点的class属性值
result = html.xpath('//a[@href="https://www.jd.com"]/../@class')
print('class属性 =',result)
# # 选取href属性值为https://www.jd.com的<a>节点的父节点,并输出父节点的class属性值
result = html.xpath('//a[@href="https://www.jd.com"]/parent::*/@class')
print('class属性 =',result)
8.7 property获取节点属性
使用XPath根据<a>节点的href属性过滤特定的<a>节点,并输出<a>节点的文本和URL
XPath的过滤条件需要放到一对中括号([...])中,
1、'//a[@class="item1"]'表示过滤所有class属性值为item1的<a>节点
2、如不将属性引用放在[...]中,就是获取属性值,如'//a/@href'表示获取所有<a>节点的href的值
使用demo.html测试上述规则,案例如下:
from lxml import etree
parser = etree.HTMLParser()
html = etree.parse('demo.html',parser)
# 选取所有href属性包含https://geekori.com的<a>节点
nodes = html.xpath('//a[@href="https://geekori.com"]')
print('共{}节点'.format(len(nodes)))
for i in range(0,len(nodes)):
print(nodes[i].text)
# 选取所有href属性值包含www的<a>节点
nodes = html.xpath('//a[contains(@href,"www")]')
print('共{}节点'.format(len(nodes)))
for i in range(0,len(nodes)):
print(nodes[i].text)
# 获取所有href属性值包含www的<a>节点的href属性值,urls是href属性值的列表
urls = html.xpath('//a[contains(@href,"www")]/@href')
for i in range(0,len(urls)):
print(urls[i])
8.8 multiproperties多条件选择节点
- and和or关键字以及contains函数的使用,注意大小写敏感,不能写成And或者AND
- 用and和or选取也定的<a>节点,并输出<a>节点的文本和标题
from lxml import etree
parser = etree.HTMLParser()
html = etree.parse('demo.html', parser)
# 选取href属性值为https://www.jd.com或https://geekori.com的<a>节点
aList = html.xpath('//a[@href="https://www.jd.com" or @href="https://geekori.com"]')
for a in aList:
print(a.text,a.get('href'))
# 匹配<li class="item4" value="1234"><a href="https://www.microsoft.com">微软</a></li>
# 选取href属性值包含www,且父节点中value属性值等于1234的<a>结点
print("---------------------------")
aList = html.xpath('//a[contains(@href,"www") and ../@value="1234"]')
for a in aList:
print(a.text,a.get('href'))
😕
# 再补充一些XPath中的其他的运算符
# | : 计算节点集 ://book|//cd : 返回所有拥有book和cd元素的节点集
# +、-、*、div、=、!=、<、<=、>、>=、or、and、mod
8.9 ordernode获取某个节点的规则
使用position()选取第几个节点,使用last()选取最后一个结点
from lxml import etree
parser = etree.HTMLParser()
text = '''
<div>
<a href="https://geekori.com"> geekori.com</a>
<a href="https://www.jd.com"> 京东商城</a>
<a href="https://www.taobao.com">淘宝</a>
<a href="https://www.microsoft.com">微软</a>
<a href="https://www.google.com">谷歌</a>
</div>
'''
html = etree.HTML(text)
# 选择第1个<a>节点
a1 = html.xpath('//a[1]/text()')
# 选择第2个<a>节点
a2 = html.xpath('//a[position()=2]/text()')
print(a1,a2)
# 选择最后一个<a>节点
lasta = html.xpath('//a[last()]/text()')
print(lasta)
# 选择索引大于3的<a>节点
aList = html.xpath('//a[position() > 3]/text()')
print(aList)
# 选择第2个<a>节点和倒数第2个<a>节点
aList = html.xpath('//a[position()=2 or position() = last()-1]/text()')
print(aList)
8.10 nodeaxis节点轴获取特定节点方法
常用节点轴:
ancestor轴,用于获取某节点的所有祖先节点
attribute轴,用于获取某节点的所有属性值
child轴,用于获取某节点的所有子节点
descendant轴,用于获取某节点的所有子孙节点
following轴,用于获取某节点后的所有子节点(包括子孙节点)
following-sibling轴,用于获取某节点后所有同级的节点
使用XPAth和不同的节点轴选择方法得到特定的结点,并输出节点的文本
from lxml import etree
parser = etree.HTMLParser()
text = '''
<html>
<head>
<meta charset="UTF-8">
<title>XPath演示</title>
</head>
<body class="item">
<div>
<ul class="item" >
<li class="item1"><a href="https://geekori.com"> geekori.com</a></li>
<li class="item2"><a href="https://www.jd.com">京东商城</a>
<value url="https://geekori.com"/>
<value url="https://www.google.com"/>
</li>
<li class="item3"><a href="https://www.taobao.com">淘宝</a>
<a href="https://www.tmall.com/">天猫</a></li>
<li class="item4" value="1234"><a href="https://www.microsoft.com">微软</a></li>
<li class="item5"><a href="https://www.google.com">谷歌</a></li>
</ul>
</div>
</body>
</html>
'''
html = etree.HTML(text)
# 使用ancestor轴,用于获取所有祖先结点。后面必须跟两个冒号(::),然后是节点选择器
# 这里的*表示匹配所有的结点
result = html.xpath('//li[1]/ancestor::*')
# 输出结果:html body div ul
for value in result:
print(value, end=" ")
print()
# 使用ancestor轴匹配所有的class属性值为item的祖先节点
result = html.xpath('//li[1]/ancestor::*[@class="item"]')
# 输出结果body ul
for value in result:
print(value, end=" ")
print()
# 使用attribute轴获取第4个<li>节点的所有属性值
result = html.xpath('//li[4]/attribute::*')
# 输出结果:['item4','1234']
print(result)
print()
# 使用child轴获取第3个<li>节点的所有子节点
result = html.xpath('//li[3]/child::*')
# https://www.taobao.com 淘宝 https://www.tmall.com/ 天猫
for value in result:
print(value.get('href'), value.text, end=" ")
print()
# 使用descendant轴获取第2个<li>节点的所有名为value的子孙节点
result = html.xpath('//li[2]/descendant::value')
# 输出结果:https://geekori.com https://www.google.com
for value in result:
print(value.get('url'),end=" ")
print()
# 使用following轴获取第1个<li>节点后的所有子节点(包括子孙节点)
result = html.xpath('//li[1]/following::*')
# 输出结果 li a value value li a a li a li a
for value in result:
print(value.tag,end=" ")
print()
# 使用following轴获取第1个<li>节点后位置大于4的所有子节点
result = html.xpath('//li[1]/following::*[position()>4]')
# 输出结果 li a a li a li a
for value in result:
print(value.tag,end=" ")
print()
# 使用following-sibling轴获取第1个<li>节点后所有同级的节点
result = html.xpath('//li[1]/following-sibling::*')
# 输出结果 li item2 li item3 li item4 li item5
for value in result:
print(value.tag,value.get('class'),end=" ")
8.11 autoXpath
- 在Chrome浏览器中通过开发者工具获取京东商城首页与导航条对应的XPath代码,并稍加修改,然后利用requests库抓取导航条文本
- 进入www.jd.com选取秒杀那一行,然后右键Copy,选取XPath,有’//*[@id=“navitems-group1”]/li[1]/a’
- 本例需要抓取所有符合条件的<a>节点,而通过开发者工具获取XPath代码只是选择了第1个<li>节点中的,<a>节点,做如下修改
‘//*[@id=“navitems-group1”]//a/’
import requests
from lxml import etree
# 抓取京东商城首页的HTMl代码
r = requests.get('https://www.jd.com')
parser = etree.HTMLParser()
html = etree.HTML(r.text)
# 提取导航条第1部分的链接文本
nodes = html.xpath('//*[@id="navitems-group1"]//a/text()')
print(nodes)
# 提取导航条第2部分的链接文本
nodes = html.xpath('//*[@id="navitems-group2"]//a/text()')
print(nodes)
# 提取导航条第3部分的链接文本
nodes = html.xpath('//*[@id="navitems-group3"]//a/text()')
print(nodes)
输出的结果打印:
['秒杀 ', '优惠券 ', 'PLUS会员 ', '品牌闪购']
['拍卖 ', '京东家电 ', '京东超市', '京东生鲜']
['京东国际 ', '京东云 ']
实战案例:豆瓣图书排行榜Top250
- 分析豆瓣读书排行榜页面代码的规律
https://book.douban.com/top250?start=0
https://book.douban.com/top250?start=25 - 打开页面提取图书标题所在XPath://*[@id=“content”]/div/div[1]/div/table[1]/tbody/tr/td[2]/div[1]/a
<tr class="item>"中存有需要的信息,可以直接选择tr节点,即//tr[@class=“item”]
图书名称:td/div/a/@title
图书主页链接:tf/div/a/@href - 关于Python的内容不再做解释,需要复习的传送门==>>Python学习笔记合集.
from lxml import etree
from lxml import *
import requests
import json
# 根据URL抓取HTML代码,并返回这些代码,如果抓取失败,返回None
def getOnePage(url):
try:
res = requests.get(url,headers=headers)
if res.status_code == 200:
print('获取成功,页面长度:',len(res.text))
return res.text
return None
except Exception:
return None
# 分析HTML代码,这是一个生产期函数
def parseOnePage(html):
selector = etree.HTML(html)
# 选择<tr>节点
items = selector.xpath('//tr[@class="item"]')
# 在<tr>节点内部继续使用XPath选择对应的节点
for item in items:
# 这里使用try-except的原因是,有一本书没有作者,只有出版社、日期、价格,会抛出异常
# except中写continue可以跳过爆出一场的那一条目
try:
# 获取<p>节点中的文本,其中包括出版社、作者、出版日期等信息
book_infos = item.xpath('td/p/text()')
yield {
# 获取图书名称
'name': item.xpath('td/div/a/@title')[0],
# 获取图书主页链接
'url': item.xpath('td/div/a/@href')[0],
# 获取图书作者
'author': book_infos[0].split('/')[-4],
# 获取图书出版社
'publisher': book_infos[0].split('/')[-3],
# 获取出版日期
'date': book_infos[0].split('/')[-2],
# 获取图书价格
'price': book_infos[0].split('/')[-1]
}
except Exception:
continue
# 将抓到的数据(JSON)格式保存到top250.txt中
def save(content):
with open('top250books.txt', 'a+', encoding='utf-8') as f:
f.write(json.dumps(content, ensure_ascii=False) + '\n')
# 抓取url对应的页面,并将页面内容保存到文本中
def getTop250(url):
html = getOnePage(url)
for item in parseOnePage(html):
print(item)
save(item)
# 设置请求头(User-Agent),否则无法获取页面内容
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) '
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36',
}
# 产生10个URL,分别对应TOP250排行榜10个页面的URL
urls = ['https://book.douban.com/top250?start={}'.format(str(i)) for i in range(0,250,25)]
# 循环抓取TOp250排行榜的10个页面的图书信息
for url in urls:
getTop250(url)
抓取结果: