XPath:元素定位在爬虫或自动化测试中的应用
xpath定位在lxml/appium/selenium中使用简介
json库:json.loads(), json.dumps(), json.load(), json.dump()
jsonpath相关库:jsonpath, jsonpath-ng
Xpath
无论是爬虫还是使用appuim或selenium做自动化测试,都难免会使用到xpath的元素定位,因此记录一下
XPath,全称 XML Path Language,即 XML 路径语言,它是一门在XML文档中查找信息的语言。XPath 最初设计是用来搜寻XML文档的,但是它同样适用于 HTML 文档的搜索
官方网站
XPath测试工具
- xpath测试
- chrome浏览器开发者工具:F12 - Elements - select an element - ctrl/command + f - 输入XPath表达式
- chrome - F12 - Console - 输入如下表达式进行校验
$x("需要校验的xpath表达式")
- chrome xpath helper插件:安装并在需要定位元素的网页打开插件 - 键盘按住shift键 - 光标移动到需要定位的元素上,则可在插件输入框内实时看到xpath路径及结果
其实chrome浏览器开发者工具也提供拷贝xpath的功能,开发者模式下同样通过
F12 - Elements - select an element
定位到网页源码,在该元素的源码上右键copy - copy xpath或copy full xpath即可
注意:无论是开发者工具copy到的xpath还是插件帮我们提取到的xpath表达式,虽然可以定位到元素,但显得很冗长,不够简洁或不是最佳的答案,这里提取的xpath表达式仅作参考即可
常用语法介绍
Xpath=//tagname[@attribute='value']
- //:选择当前节点,也就是在文档中全部层级位置位置进行查找
- Tagname: 节点名
- @:选择属性
- Attribute:节点的属性名称
- Value:属性的值
节点关系
父、子、同胞、先辈、后代
选取节点
表达式 | 描述 |
---|---|
nodename | 选取此节点的所有子节点 |
/ | 从根节点选取 |
// | 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置 |
. | 选取当前节点 |
.. | 选取当前节点的父节点 |
@ | 选取属性 |
几个特殊符号
- 通配符
*
:选取未知节点,使用*
匹配任何元素节点或使用@*
来匹配任何属性节点 - “|”运算符:选取若干路径
- 方括号
[]
:谓语。用来查找某个特定的节点或者包含某个指定的值的节点,谓语被嵌在方括号中
上下文节点(轴XPath Axes)
轴可定义相对于当前节点的节点集,从当前上下文节点搜索XML文档中的不同节点,可用于查找动态元素的方法
轴名称 | 结果 |
---|---|
ancestor | 选取当前节点的所有先辈(父、祖父等) |
ancestor-or-self | 选取当前节点的所有先辈(父、祖父等)以及当前节点本身 |
attribute | 选取当前节点的所有属性 |
child | 选取当前节点的所有子元素 |
descendant | 选取当前节点的所有后代元素(子、孙等) |
descendant-or-self | 选取当前节点的所有后代元素(子、孙等)以及当前节点本身 |
following | 选取文档中当前节点的结束标签之后的所有节点 |
namespace | 选取当前节点的所有命名空间节点 |
parent | 选取当前节点的父节点 |
preceding | 选取文档中当前节点的开始标签之前的所有节点 |
preceding-sibling | 选取当前节点之前的所有同级节点 |
self | 选取当前节点 |
- 使用语法:轴名称::节点测试[谓语]
常用运算符
运算符 | 描述 |
---|---|
` | ` |
= != | 等于 不等于 |
< <= > >= | 小于 小于等于 大于 大于等于 |
or and | 或 与 |
常用函数
- contains:包含
- starts-with:以xx开始
- ends-with:以xx结尾
xpath示例
如下xml文档
<?xml version="1.0" encoding="ISO-8859-1"?>
<bookstore>
<book>
<title lang="eng" attr="book">Harry Potter</title>
<price>29.99</price>
</book>
<book>
<title lang="eng" attr="e-book">Learning XML</title>
<price>39.95</price>
</book>
</bookstore>
xpath表达式 | 描述 |
---|---|
/bookstore | 选取根元素 bookstore。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径 |
bookstore/book | 选取属于 bookstore 的子元素的所有 book 元素 |
//book | 选取所有 book 子元素,而不管它们在文档中的位置 |
bookstore//book | 选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置 |
//@lang | 选取名为 lang 的所有属性 |
/bookstore/book[1] | 选取属于 bookstore 子元素的第一个 book 元素 |
/bookstore/book[last()] | 选取属于 bookstore 子元素的最后一个 book 元素 |
/bookstore/book[last()-1] | 选取属于 bookstore 子元素的倒数第二个 book 元素 |
/bookstore/book[position()<3 ] |
选取最前面的两个属于 bookstore 元素的子元素的 book 元素 |
//title[@lang] | 选取所有拥有名为 lang 的属性的 title 元素 |
//title[@lang='eng'] | 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性 |
/bookstore/book[price>35.00] | 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00 |
/bookstore/book[price>35.00]/title | 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00 |
/bookstore/book/title[@lang='eng' and text()='Harry Potter'] | 选取title中lang为eng且文本是Harry Potter的书 |
/bookstore/* | 选取 bookstore 元素的所有子元素 |
//* | 选取文档中的所有元素 |
//title[@*] | 选取所有带有属性的 title 元素 |
//book/title | //book/price | 选取 book 元素的所有 title 和 price 元素 |
bookstore/child::text() | 选取bookstore节点的所有文本子节点 |
//*[contains(@attr, 'book')] | 选取attr属性值包含book的节点 |
//title[starts-with(@attr, 'e-')] | 选取attr属性值以'e-'开头的节点 |
xpath在python的使用
在lxml库中的使用
- 官方网站
- 安装:
pip install lxml
- xpath在lxml库中的使用示例
from lxml import etree
text = '''
<div>
<ul>
<li class="item-0" name="item"><a href="https://1eq066.coding-pages.com/link1.html">first item</a></li>
<li class="item-1"><a href="https://1eq066.coding-pages.com/link2.html">second item</a></li>
<li class="item-inactive"><a href="https://1eq066.coding-pages.com/link3.html">third item</a></li>
<li class="item-1"><a href="https://1eq066.coding-pages.com/link4.html">fourth item</a></li>
<li class="item-0"><a href="https://1eq066.coding-pages.com/link5.html">fifth item</a>
</ul>
</div>
'''
html = etree.HTML(text) # 构造xpath解析对象;etree.HTML(text) 解析HTML文档,返回根节点
print(1, html, type(html))
result = etree.tostring(html).decode('utf-8') # 修正不完整的html文本
print(2, result)
# 也可以保存不完整的html为test.html,直接【读取该文本文件】进行解析
html = etree.parse('./test.html', etree.HTMLParser()) # 采用读取文本文件的方式构造xpath解析对象
print(3, html, type(html))
result = etree.tostring(html).decode('utf-8')
print(4, result) # 比较4与2之间的输出区别
# 解析xpath对象
xpath_result = html.xpath("//*")
print(5, xpath_result) # 返回list
for item, result in enumerate(xpath_result):
print(f'6.{item}', result, type(result), etree.tostring(result).decode('utf-8'))
# 属性匹配
result = html.xpath('//li[@class="item-0"]')
print(7, result)
# 获取属性值
result = html.xpath('//li/@class')
print(8, result) # 注意7和8的区别
# 获取文本 text()
result = html.xpath('//li[@class="item-0"]/a/text()')
print(9, result)
# 位置选择
result = html.xpath('//li[1]/a/text()')
print(10, result)
result = html.xpath('//li[last()]/a/text()')
print(11, result)
result = html.xpath('//li[position()<3]/a/text()')
print(12, result)
# contains函数
result = html.xpath('//li[contains(@class,"item")]/a/text()')
print(13, result)
# contains函数 和 and
result = html.xpath('//li[contains(@class,"item") and @name="item"]/a/text()')
print(14, result)
# 父节点 ..
result = html.xpath('//a[@href="https://1eq066.coding-pages.com/link4.html"]/../@class')
print(15, result)
# 父节点 parent::
result = html.xpath('//a[@href="https://1eq066.coding-pages.com/link4.html"]/parent::*/@class')
print(16, result)
更多示例可参看学爬虫利器XPath
在appium/selenium库中的使用
- 环境配置可分别参看appium自动化环境搭建及selenium环境安装
- xpath使用示例
这里仅介绍xpath的使用,关于更多appium及selenium在自动化测试中的应用请参看后续文章
appium及selenium中关于xpath元素定位基本一致,因此以下示例中的driver即appium中和selenium中的webdriver
# 通过文本属性定位
driver.find_element_by_xpath("//*[@text='扫一扫']").click()
# 通过id定位:resource-id
driver.find_element_by_xpath("//*[@resource-id='com.taobao.taobao:id/tv_scan_text']").click()
# 通过class属性定位
driver.find_element_by_xpath(
"//android.widget.EditText").click() # 也可使用find_element_by_xpath("//*[@class='android.widget.EditText']")
# 通过content-desc属性定位
driver.find_element_by_xpath("//*[@content-desc='帮助']").click()
# contains模糊匹配
driver.find_element_by_xpath('//*[contains(@text, "注册/登录")]').click()
driver.find_element_by_xpath("//*[contains(@content-desc, '帮助')]").click()
driver.find_element_by_xpath("//*[contains(@class, 'EditText')]").click()
driver.find_element_by_xpath("//*[contains(@resource-id, 'id/home_searchedit')]").click()
# 组合定位
## text和index属性 登录/注册
desc_class = '//*[@text="注册/登录" and @index="1"]'
driver.find_element_by_xpath(desc_class).click()
## class和desc 帮助
id_desc = '//*[contains(@resource-id, "aliuser_menu_item_help") and @content-desc="帮助"]'
driver.find_element_by_xpath(id_desc).click()
# 层级定位
## 父找子(可更多层级,如爷找孙等)
father_to_son_ele = '//*[@resoure-id="com.taobao.taobao:id/home_searchbar"]/android.widget.EditText'
print(driver.find_element_by_xpath(father_to_son_ele).text)
father_to_son_ele = '//*[@resource-id="com.taobao.taobao:id/ll_navigation_tab_layout"]/android.widget.FrameLayout[2]' # 多个子,第2个子
driver.find_element_by_xpath(father_to_son_ele).click()
## 子找父 ..
son_to_father_ele = '//*[@resource-id="com.taobao.taobao:id/tv_scan_text"]/..'
print(driver.find_element_by_xpath(son_to_father_ele).tag_name)
## 子找父 parent::*
son_to_father_ele = '//*[@resource-id="com.taobao.taobao:id/tv_scan_text"]/parent::*'
print(driver.find_element_by_xpath(son_to_father_ele).tag_name)
son_to_father_ele = '//*[@resource-id="com.taobao.taobao:id/tv_scan_text"]/parent::android.widget.LinearLayout'
print(driver.find_element_by_xpath(son_to_father_ele).tag_name)
## 兄弟节点之间查找 //x/../y(先找到父后再往下找)
brother_to_brother_ele = '//*[@resource-id="com.taobao.taobao:id/bar_search"]/../android.widget.RelativeLayout'
print(driver.find_element_by_xpath(brother_to_brother_ele).tag_name)
参考文章xpath定位在appium中的应用,请结合参考文章中关于淘宝的真实案例
关于xpath定位的说明
- 可以说xpath定位基本是万能,几乎能找到你想定位的任何元素
- 但是xpath定位元素在在(app)自动化测试是相当慢的。这是因为xpath定位时需要遍历整个元素树,生成一个 xml 数据,然后再做 xpath 查找。而遍历和在 xml 中进行 xpath 查找都相当耗时
- 不推荐使用xpath定位,而对于特别的不方便定位的元素才会使用,对于xpath提速的一些方法待后续文章介绍
- 定位 class 含有空格的复合类时,如果直接将 包含有空格的 class 属性复制过来定位则找不到,如要定位的标签
<div class="page-title filename">12345 </div>
可使用class值取其中之一即driver.find_elements_by_class_name("page-title")
或driver.find_elements_by_class_name("filename")
json解析
在解析json之前,先来熟悉下内建模块json
json库
json中常用的几个方法:json.loads,json.dumps,json.load,json.dump
- 序列化:python对象转换成json字符串,json.dumps(), json.dump()
- json.dumps(obj,ensure_ascii=True,indent=None,sort_keys=False)
- ensure_ascii:默认为 True,输出保证将所有输入的非 ASCII 字符转义。如果 ensure_ascii 是 false,这些字符会原样输出
- indent:一个非负整数或者字符串,JSON 数组元素和对象成员会被美化输出为该值指定的缩进等级。如果缩进等级为零、负数或者 "",则只会添加换行符。None(默认值)选择最紧凑的表达。使用一个正整数会让每一层缩进同样数量的空格。如果是一个字符串(比如 "\t"),那个字符串会被用于缩进每一层
- sort_keys:如果设置为True(默认为 False),字典的输出会以键的顺序排序
- 反序列化:json字符串转换成python对象,json.loads(), json.load()
具体应用见如下示例:
import json
py_dict = {"name": "monkey", "age": 28}
# 例1:json.dumps将python的对象(这里是dict,也可以是其他python数据类型)转成json格式字符串'{"name": "monkey", "age": 28}'
json_str = json.dumps(py_dict)
print(1, json_str, type(json_str))
# json.loads将json格式字符串转换成python对象
py_dict = json.loads(json_str)
print(2, py_dict, type(py_dict))
# 例2:从"文件中"读出(f.read())字符串后,然后再json.loads转换成python对象
with open("config.json", "r") as f:
py_dict = json.loads(f.read())
print(3, py_dict, type(py_dict))
# 例3(区别于例2):json.load读取"文件中"json格式的字符串直接转化成python对象
with open("config.json", "r") as f:
py_dict = json.load(f)
print(4, py_dict, type(py_dict))
# 例4: 将Python类型序列化为json字符串后写入文件
with open("new_config.json", "w") as f:
json.dump(py_dict, f, ensure_ascii=False)
-----------------------------------
1 {"name": "monkey", "age": 28} <class 'str'>
2 {'name': 'monkey', 'age': 28} <class 'dict'>
3 {'name': 'monkey', 'age': 28} <class 'dict'>
4 {'name': 'monkey', 'age': 28} <class 'dict'>
jsonpath相关库
顾名思义,jsonpath是用来解析json数据结构的python库,之所以放在这里介绍,是因为他很像Xpath。在pypi上搜索jsonpath,可发现jsonpath相关的项目比较多
参看文章Python JSONPath Examples,jsonpath相关库主要有如下三个:
- jsonpath: python中的jsonpath是Perl和JavaScript版的翻译版本,详细可参看jsonpath文档
- jsonpath-rw: Python的完整实现,而且jsonpath-RW-EXT模块还提供了一些附加的扩展来扩展其功能
- jsonpath-ng: JSONPath的最终实现,旨在实现标准兼容,包括算术和二进制比较运算符。该库整合了jsonpath-rw和jsonpath-rw-ext模块,并进一步对其进行了增强
这里主要介绍jsonpath和jsonpath-ng
jsonpath
- 安装
pip3 install jsonpath
- jsonpath操作符
XPath | JSONPath | 描述 |
---|---|---|
/ | $ | 根元素。查询根节点对象,用于表示一个json数据,可以是数组或者对象 |
. | @ | 当前元素。一般用于过滤器断言处理的当前节点对象 |
/ | . 或 [] | 子元素 |
.. | 不支持 | 当前元素的父元素 |
// | .. | 递归搜索,任意层次 |
* | * | 通配符,表示任意名字或者数字 |
@ | 不支持 | 属性获取,jsonpath无 |
[] | [] | 下标操作 |
| | [,] | 连接操作符,在XPath中结果合并其它结点集合(或操作),jsonpath允许name或者数组索引 |
不支持 | [start:end:step] |
数组切片 |
[] | [?(<expression>)] |
过滤器表达式,<expression> 结果必须是布尔类型 |
不支持 | () | 脚本表达式,如(<expression>) 、(@.length-1) |
() | 不支持 | Xpath分组,jsonpath无 |
- fastjson中关于jsonpath的介绍
- 例子
如下json字符串:
{
"store": {
"book": [{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
}, {
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
}, {
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
}, {
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}],
"bicycle": {
"color": "red",
"price": 19.95
}
}
}
则jsonpath和path对比表示如下:
XPath | JSONPath | 结果 |
---|---|---|
/store/book/author |
$.store.book[*].author |
store下所有books的author |
//author |
$..author |
所有元素的author |
/store/* |
$.store.* |
store下所有的元素 |
/store//price |
$.store..price |
store下所有元素的price |
//book[3] |
$..book[2] |
第三本书(book) |
//book[last()] |
$..book[(@.length-1)] 或$..book[-1:] |
最后一本书(book) |
//book[position()<3] |
$..book[0,1] 或 $..book[:2] |
前两本书(book) |
//book[isbn] |
$..book[?(@.isbn)] |
筛选出包含有isbn的书(book) |
//book[price<10] |
$..book[?(@.price<10)] |
筛选出价格低于10的书(book) |
//* |
$..* |
所有元素 |
其中包含函数如length(),这里就不一一列举说明,还有常用于筛选器的操作符如下(详细可参看jsonpath(java版)github教程):
操作符 描述 == != < <= > >= 等于,不等于,小于, 小于等于, 大于, 大于等于 =~ 判断是否符合正则表达式,例如[?(@.name =~/foo.*?/i)] in 所属符号,例如[?(@.size in ['S', 'M'])] nin 排除符号,不在 empty 判空符号
- python中jsonpath库的使用
import json
import jsonpath
# 还是上面例子中的json
json_str = '{"store":{"book":[{"category":"reference","author":"Nigel Rees","title":"Sayings of the Century","price":8.95},{"category":"fiction","author":"Evelyn Waugh","title":"Sword of Honour","price":12.99},{"category":"fiction","author":"Herman Melville","title":"Moby Dick","isbn":"0-553-21311-3","price":8.99},{"category":"fiction","author":"J. R. R. Tolkien","title":"The Lord of the Rings","isbn":"0-395-19395-8","price":22.99}],"bicycle":{"color":"red","price":19.95}}}'
json_dict = json.loads(json_str)
expr = "$.store.book[*].author" # 也可表示成store.book[*].author,默认从根开始
result = jsonpath.jsonpath(json_dict, expr)
print(result)
---------------------------------
['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien']
由此可见,jsonpath使用起来很简单,关键是需要写出expr来提取需要的内容
如果没有匹配到,则返回False
jsonpath测试工具
- jsonpath测试工具
- json格式校验(json校验的站点比较多,随便用一个即可)
jsonpath-ng
- 安装
pip install jsonpath-ng
- 使用示例
from jsonpath_ng import parse
json_dict = {"info": [{"son": "tom"}, {"son": "tom_1"}]}
expr = parse('$.info[*].son')
print(1, expr, type(expr))
matches = expr.find(json_dict)
print(2, matches, type(matches)) # 返回matches是list
for num, match in zip(range(1, len(matches)+1), matches):
print(f"3.{num}", match, match.value, type(match.value))
print(4, [item.value for item in matches]) # 提取value值(list)
print(5, [str(item.full_path) for item in matches]) # 能够找到匹配项的来源
-------------------------------------------------------
1 $.info.[*].son <class 'jsonpath_ng.jsonpath.Child'>
2 [DatumInContext(value='tom', path=Fields('son'), context=DatumInContext(value={'son': 'tom'}, path=<jsonpath_ng.jsonpath.Index object at 0x103002860>, context=DatumInContext(value=[{'son': 'tom'}, {'son': 'tom_1'}], path=Fields('info'), context=DatumInContext(value={'info': [{'son': 'tom'}, {'son': 'tom_1'}]}, path=Root(), context=None)))), DatumInContext(value='tom_1', path=Fields('son'), context=DatumInContext(value={'son': 'tom_1'}, path=<jsonpath_ng.jsonpath.Index object at 0x103002588>, context=DatumInContext(value=[{'son': 'tom'}, {'son': 'tom_1'}], path=Fields('info'), context=DatumInContext(value={'info': [{'son': 'tom'}, {'son': 'tom_1'}]}, path=Root(), context=None))))] <class 'list'>
3.1 DatumInContext(value='tom', path=Fields('son'), context=DatumInContext(value={'son': 'tom'}, path=<jsonpath_ng.jsonpath.Index object at 0x103002860>, context=DatumInContext(value=[{'son': 'tom'}, {'son': 'tom_1'}], path=Fields('info'), context=DatumInContext(value={'info': [{'son': 'tom'}, {'son': 'tom_1'}]}, path=Root(), context=None)))) tom <class 'str'>
3.2 DatumInContext(value='tom_1', path=Fields('son'), context=DatumInContext(value={'son': 'tom_1'}, path=<jsonpath_ng.jsonpath.Index object at 0x103002588>, context=DatumInContext(value=[{'son': 'tom'}, {'son': 'tom_1'}], path=Fields('info'), context=DatumInContext(value={'info': [{'son': 'tom'}, {'son': 'tom_1'}]}, path=Root(), context=None)))) tom_1 <class 'str'>
4 ['tom', 'tom_1']
5 ['info.[0].son', 'info.[1].son']
更多高级用法可进一步参看jsonpath-ng 官方文档