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库中的使用

  1. 官方网站
  2. 安装:pip install lxml
  3. 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库中的使用

  1. 环境配置可分别参看appium自动化环境搭建selenium环境安装
  2. 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相关库主要有如下三个:

  1. jsonpath: python中的jsonpath是Perl和JavaScript版的翻译版本,详细可参看jsonpath文档
  2. jsonpath-rw: Python的完整实现,而且jsonpath-RW-EXT模块还提供了一些附加的扩展来扩展其功能
  3. jsonpath-ng: JSONPath的最终实现,旨在实现标准兼容,包括算术和二进制比较运算符。该库整合了jsonpath-rw和jsonpath-rw-ext模块,并进一步对其进行了增强

这里主要介绍jsonpath和jsonpath-ng

jsonpath

  1. 安装
    pip3 install jsonpath
    
  2. jsonpath操作符
XPath JSONPath 描述
/ $ 根元素。查询根节点对象,用于表示一个json数据,可以是数组或者对象
. @ 当前元素。一般用于过滤器断言处理的当前节点对象
/ . 或 [] 子元素
.. 不支持 当前元素的父元素
// .. 递归搜索,任意层次
* * 通配符,表示任意名字或者数字
@ 不支持 属性获取,jsonpath无
[] [] 下标操作
| [,] 连接操作符,在XPath中结果合并其它结点集合(或操作),jsonpath允许name或者数组索引
不支持 [start:end:step] 数组切片
[] [?(<expression>)] 过滤器表达式,<expression>结果必须是布尔类型
不支持 () 脚本表达式,如(<expression>)(@.length-1)
() 不支持 Xpath分组,jsonpath无
  1. fastjson中关于jsonpath的介绍
  2. 例子
    如下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 判空符号
  1. 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-ng

  1. 安装
    pip install jsonpath-ng
    
  2. 使用示例
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 官方文档

更多阅读