Xpath是什么?

XPath 是 XML/HTML 文档的查询语言,爬虫里经常用到,lxml、Scrapy、Selenium 都支持,常用来解析网页。


一、基本概念

XPath 把html文档看成一棵树,用路径定位节点。

<html>
  <body>
    <div class="box">
      <p id="t1">hello</p>
    </div>
  </body>
</html>
  • 元素:div、p
  • 属性:class="box"、id="t1"
  • 文本:hello

二、路径表达式

符号含义例子
/绝对路径,从根开始/html/body/div
//任意位置的子孙节点//p
.当前节点./div
..父节点../p
@属性//div[@class="box"]
*通配,任意节点//div/*
@*任意属性//div/@*

选择元素

/html/body/div            # 绝对路径
//p                        # 所有 p 元素
//div/p                    # div 下的直接子 p
//div//p                   # div 下的所有后代 p

选择属性

//a/@href                  # 所有 a 的 href 属性
//div/@class               # 所有 div 的 class

选择文本

//p/text()                 # p 的直接文本
//div//text()              # div 下所有文本

三、谓词(条件)

用 [] 加条件筛选节点。

按位置

//p[1]                      # 每个父节点下第一个 p
//p[last()]                 # 最后一个 p
//p[position()<3]           # 前两个 p(position()函数意味着第几个位置,<3则说明有1,2)
//p[last()-1]               # 倒数第二个

注意://p[1] 是"每个父节点下的第一个 p",不是"整个文档第一个 p"。
要拿整个文档第一个,用 (//p)[1]。注意优先级

按属性

//p[@id]                    # 有 id 属性的 p
//div[@class="box"]         # 所有class 等于 box 的 div
//a[@href="http://x.com"]   # href 精确匹配

按文本

//p[text()="hello"]              # 文本精确等于 hello
//p[contains(text(),"ell")]      # 文本包含 ell
//p[starts-with(text(),"he")]    # 文本以 he 开头

组合条件

//div[@class="box"][@id="main"]   # 两个属性都满足等价and
//p[@id="a" or @id="b"]           # 满足一个条件就行
//p[@id and @class]               # 同一

四、通配符

写法含义
*任意元素
@*任意属性
node()任意节点(元素、文本、注释)
text()文本节点
//div/*                     # div 下所有子元素
//div/@*                    # div 的所有属性
//div/node()                # div 下所有节点

五、轴(axes)

轴用来沿某个方向选择节点。

轴含义
child::子节点(默认轴)
parent::父节点
ancestor::所有祖先
descendant::所有后代
following-sibling::后面的兄弟
preceding-sibling::前面的兄弟
following::文档中之后的所有节点
preceding::文档中之前的所有节点
self::自身
attribute::属性
//p/ancestor::div                     # p 的所有祖先 div
//p/following-sibling::p              # p 后面的兄弟 p
//div/descendant::a                   # div 下所有后代 a
//p/parent::div                       # p 的父 div

六、运算符

运算符含义例子
+ - * div mod算术//p[position() mod 2 = 1]
= !=相等 / 不等//p[@id="a"]
< <= > >=比较//p[count(*) > 2]
and与//p[@id and @class]
or或//p[@id="a" or @id="b"]
``并集

注意 div 既可以是"除法"运算符,也可以是标签名,看上下文。
因为/要作为路径,所以div是除法,同理mod就是取余啦。

七、常用函数

字符串函数

contains(s, t)              # s 是否包含 t
starts-with(s, t)           # s 是否以 t 开头
concat(s1, s2, ...)         # 拼接
string-length(s)            # 长度
normalize-space(s)          # 去首尾空白 + 合并中间空格
substring(s, start, len)    # 截取(从 1 开始)

位置函数

position()                  # 当前位置(从 1 开始)
last()                      # 最后一个位置
count(nodeset)              # 节点个数

数值函数

number(x)                   # 转数字
sum(nodeset)                # 求和
floor(x) / ceiling(x)       # 取整
round(x)                    # 四舍五入

逻辑函数

not(expr)                   # 取反
true() / false()            # 布尔值

示例

//a[contains(@href, "login")]              # href 包含 login 的 a
//p[normalize-space(text())="hello"]       # 去空白后匹配文本
//div[count(p) > 3]                         # 有 3 个以上 p 的 div
//p[not(@class)]                            # 没有 class 属性的 p

八、实战示例

假设 HTML 如下:

<div class="list">
  <ul>
    <li class="item"><a href="/a">A</a></li>
    <li class="item"><a href="/b">B</a></li>
    <li class="item"><a href="/c">C</a></li>
  </ul>
</div>

常用定位

//div[@class="list"]//a                      # 所有链接
//div[@class="list"]//a/@href                # 所有链接的 href
//li[2]/a/text()                             # 第二个 li 的文本
//a[contains(@href, "/b")]                   # href 包含 /b 的 a
//li[last()]                                 # 最后一个 li
//li[last()-1]                               # 倒数第二个
//div[@class="list"]//text()                 # 所有文本
//li/parent::ul                              # li 的父节点

九、lxml 中的写法

Python 里用 lxml:

from lxml import etree

html = '''
<div class="list">
  <ul>
    <li class="item"><a href="/a">A</a></li>
    <li class="item"><a href="/b">B</a></li>
  </ul>
</div>
'''

tree = etree.HTML(html)

# 取 href
hrefs = tree.xpath('//div[@class="list"]//a/@href')
print(hrefs)   # ['/a', '/b']

# 取文本
texts = tree.xpath('//li/a/text()')
print(texts)   # ['A', 'B']

# 取节点对象
nodes = tree.xpath('//li')
for node in nodes:
    print(node.xpath('./a/text()'))   # 相对路径

几点注意:

  1. 返回的列表可能是字符串(@href、text())或节点对象
  2. 用 ./ 表示相对当前节点
  3. // 从整个文档开始找,不依赖上下文
  4. 文本里如果有 HTML 实体,用 etree.tostring(node, encoding='unicode') 还原

十、常见坑

1. //p[1] 和 (//p)[1] 的区别

//p[1]          # 每个父节点下的第一个 p
(//p)[1]        # 整个文档第一个 p

2. 文本不在 text() 里的情况

<p>hello <b>world</b></p>
  • //p/text() → ['hello '](不含子元素里的文本)
  • //p//text() → ['hello ', 'world']

要取全部文本,可以用:

''.join(node.xpath('.//text()'))

3. 属性值含空格

//div[@class="box green"]         # 精确匹配整个 class 字符串
//div[contains(@class, "box")]    # class 里含 box

4. @href 取不到时返回空列表

tree.xpath('//a[@href="x"]/@href')   # 没匹配可以返回 []

别假设一定有一条,要判空。

5. lxml 会自动补全 html/body

tree.xpath('/html/body/div')   # 可能不是你想的那样
tree.xpath('//div')            # 用 // 更稳妥

十一、速查表

需求写法
所有 p//p
直接子元素//div/p
所有后代//div//p
属性//a/@href
文本//p/text()
第一个(//p)[1]
最后一个(//p)[last()]
按属性筛选//div[@class="box"]
属性包含//a[contains(@href, "login")]
文本包含//p[contains(text(), "hello")]
多条件//div[@id and @class]
父节点//p/..
后面的兄弟//p/following-sibling::p
节点数量count(//p)

十二、总结一下

XPath 的核心就几样:

  1. 路径:/ 绝对、// 任意、. 当前、.. 父
  2. 谓词:[] 里写条件
  3. 属性:@、文本 text()
  4. 函数:contains、starts-with、normalize-space、count
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐