爬虫 - XPath

简介

  • XPath(XML Path Language)是一种用于在 XML 文档中定位和选择节点的语言。它是一种基于树结构的查询语言,常用于解析和处理 XML 文档。XPath 提供了一组用于导航和选择 XML 文档中节点的表达式。
  • XPath 表达式由一系列路径表达式组成,用于描述节点的层次结构和关系。
  • XPath 于 1999 年 11 月 16 日 成为 W3C 标准。
  • XPath 广泛应用于各种 XML 处理工具和库中,例如 XPath 可以用于解析 XML 文档、提取数据、遍历节点等。在 Web 开发中,XPath 也常用于解析 HTML 文档,从中提取所需的数据。

安装

1
pip install lxml

XML 和 HTML 有什么区别?

XML(可扩展标记语言)和 HTML(超文本标记语言)是两种不同的标记语言,用于表示和组织文档内容。它们有以下几个主要区别:

  1. 设计目的:XML 的设计目的是传输和存储数据,强调数据的结构化和扩展性。它被广泛用于数据交换和配置文件等领域。HTML 的设计目的是在 Web 上呈现和展示文档,强调文档的可视化和布局。
  2. 标签定义:XML 允许用户自定义标签,因此可以根据需要创建自定义的标记结构。HTML 使用预定义的标签,这些标签具有特定的语义和功能,用于描述文档的结构和内容。
  3. 标签语义:XML 中的标签没有预定义的语义,其含义由应用程序或处理工具来解释。HTML 中的标签具有固定的语义,例如 <p> 表示段落,<h1> 表示一级标题,<table> 表示表格等。
  4. 样式和布局:HTML 提供了丰富的样式和布局功能,可以通过 CSS(层叠样式表)来控制文档的外观和排版。XML 本身并不关注样式和布局,它更专注于数据的结构和内容。
  5. 解析方式:HTML 的解析器通常比较宽松,能够容忍一些语法错误,并尝试修复错误以展示页面。XML 的解析器要求文档必须符合严格的语法规则,否则会报错。

尽管 XML 和 HTML 有一些区别,但它们也有一些相似之处。例如,它们都使用尖括号包围标签,都采用树状结构表示文档的层次关系,都支持属性用于描述标签的附加信息。此外,XML 和 HTML 可以相互转换,可以使用 XSLT(可扩展样式表语言转换)将 XML 转换为 HTML,或者使用 HTML 解析器解析 XML 文档。

常用的路径表达式

表达式 描述
/ 从根节点选取。
./ 从当前节点开始往下查找
../ 从当前节点的父节点查找
// 不考虑位置的查找。
@ 选取属性。

实例

路径表达式 结果
bookstore/book 查找根节点 bookstore 下面所有直接子节点 book
/bookstore/* 查找 bookstore 元素的所有子元素
//book 查找所有 book
bookstore//book 查找 bookstore 下面所有的 book
/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[not(@lang)] 查找所有的不带有 lang 属性的 title 节点
//title[@lang="eng"] 查找所有的 lang 属性值为 eng 的 title 节点
//title[not(@lang="eng")] 排除所有的 lang 属性值为 eng 的 title 节点
//title[contains(@lang, "eng")] 查找所有的 lang 属性值中包含 eng 的 title 节点
//title[not(contains(@lang, "eng"))] 排除所有的 lang 属性值中包含 eng 的 title 节点
//title[@*] 查找所有带有属性的 title 元素
//a[text()="下一页"] 查找所有文本内容为下一页的的 a 标签
//book[price>35.00] 查找所有 book 元素,且其中的 price 元素的值须大于 35.00
//book/title | //book/price 选取 book 元素的所有 title 和 price 元素

安装 Xpath 插件 (XPath Helper)

  • 下载 XPath 插件
  • 将 Xpath 插件拖动到谷歌浏览器扩展程序中,安装成功(或在 Chrome 应用商店下载,需要梯子)
  • 启动和关闭插件
    1
    ctrl + shift + x

属性定位

1
2
//input[@id="kw"]
//input[@class="bg s_btn"]

层级定位

1
2
//div[@id="head"]/div[2]/a[1]
//div[@id="head"]/div[2]/a[@class="toindex"]

索引定位

1
2
3
4
//div[@id="head"]/div/div[2]/a[@class="toindex"]
# 索引从1开始
//div[@id="head"]//a[@class="toindex"]
# 双斜杠代表下面所有的a节点,不管位置

逻辑运算

1
2
//input[@class="s_ipt" and @name="wd"]
//input[@class="s_ipt" or @name="wd"]

模糊匹配

  • contains

    1
    2
    3
    4
    5
    6
    # 查找所有class属性中包含“s_i”的input
    //input[contains(@class, "s_i")]
    # 查找所有文本中包含“爱”的input
    //input[contains(text(), "爱")]
    # 排除所有文本中包含“爱”的input
    //input[not(contains(text(), "爱"))]
  • starts-with

    1
    2
    # 查找所有class属性中以“s”开头的input
    //input[starts-with(@class, "s")]

取文本

1
2
3
4
5
6
7
//div[@id="s-top-left"]/a[5]/text()     # 获取节点内容
//div[@id="s-top-left"]//text() # 获取节点里面不带标签的所有内容

# 直接将所有的内容拼接起来返回给你
ret = tree.xpath('//div[@class="song"]')
string = ret[0].xpath('string(.)')
print(string.replace('\n', '').replace('\t', ''))

取属性

1
2
# 获取节点属性
//div[@id="s-top-left"]/a[5]/@href

XPath 轴(Axes)

轴可定义相对于当前节点的节点集。

轴名称 结果
ancestor 选取当前节点的所有先辈(父、祖父等)。
ancestor-or-self 选取当前节点的所有先辈(父、祖父等)以及当前节点本身。
attribute 选取当前节点的所有属性。
child 选取当前节点的所有子元素。
descendant 选取当前节点的所有后代元素(子、孙等)。
descendant-or-self 选取当前节点的所有后代元素(子、孙等)以及当前节点本身。
following 选取文档中当前节点的结束标签之后的所有节点。
following-sibling 选取当前节点之后的所有兄弟节点
namespace 选取当前节点的所有命名空间节点。
parent 选取当前节点的父节点。
preceding 选取文档中当前节点的开始标签之前的所有节点。
preceding-sibling 选取当前节点之前的所有同级节点。
self 选取当前节点。

示例:

例子 结果
child::book 选取所有属于当前节点的子元素的 book 节点。
attribute::lang 选取当前节点的 lang 属性。
child::* 选取当前节点的所有子元素。
attribute::* 选取当前节点的所有属性。
child::text() 选取当前节点的所有文本子节点。
child::node() 选取当前节点的所有子节点。
descendant::book 选取当前节点的所有 book 后代。
ancestor::book 选择当前节点的所有 book 先辈。
ancestor-or-self::book 选取当前节点的所有 book 先辈以及当前节点(如果此节点是 book 节点)
child::*/child::price 选取当前节点的所有 price 孙节点。

XPath 运算符

XPath 表达式可返回节点集、字符串、逻辑值以及数字。

运算符 描述 实例 返回值
| 计算两个节点集 //book | //cd 返回所有拥有 book 和 cd 元素的节点集
+ 加法 6 + 4 10
- 减法 6 - 4 2
* 乘法 6 * 4 24
div 除法 8 div 4 2
= 等于 price=9.80 如果 price 是 9.80,则返回 true。如果 price 是 9.90,则返回 false。
!= 不等于 price!=9.80 如果 price 是 9.90,则返回 true。如果 price 是 9.80,则返回 false。
< 小于 price<9.80 如果 price 是 9.00,则返回 true。如果 price 是 9.90,则返回 false。
<= 小于或等于 price<=9.80 如果 price 是 9.00,则返回 true。如果 price 是 9.90,则返回 false。
> 大于 price>9.80 如果 price 是 9.90,则返回 true。如果 price 是 9.80,则返回 false。
>= 大于或等于 price>=9.80 如果 price 是 9.90,则返回 true。如果 price 是 9.70,则返回 false。
or price=9.80 or price=9.70 如果 price 是 9.80,则返回 true。如果 price 是 9.50,则返回 false。
and price>9.00 and price<9.90 如果 price 是 9.80,则返回 true。如果 price 是 8.50,则返回 false。
mod 计算除法的余数 5 mod 2 1

代码中使用 Xpath

  • 引入
    1
    from lxml import etree
  • 两种方式使用:将 html 文档变成一个对象,然后调用对象的方法去查找指定的节点
    1. 本地文件
      1
      tree = etree.parse(文件名)
    2. 网络文件
      1
      tree = etree.HTML(网页字符串)
  • ret = tree.xpath(路径表达式)ret 是一个列表
  • 有时使用 parse() 函数读取本地 HTML 文件时因为 HTML 文件不规范导致报错该如何解决?,参见:使用 lxml 解析本地 html 文件报错?

lxml 进阶

  • 在 Xpath 中使用索引取值时,如果没有获取到值就会报错,如:
    1
    icon = otr.xpath('./td[@class="td-model"]/div/a/i/text()')[0]
  • 那么该如何解决这个问题呢?
    • 方法一
      1
      2
      3
      4
      5
      icon = otr.xpath('./td[@class="td-model"]/div/a/i')
      if icon:
      icon_text = icon[0].text
      else:
      icon_text = "default_value"
    • 方法二(推荐)
      1
      2
      3
      4
      5
      # 获取文本
      icon_text = otr.findtext('./td[@class="td-model"]/div/a/i', default="default_value")

      # 获取属性
      icon_href = otr.find('./td[@class="td-model"]/div/a').get('href', default="default_value")
  • 常用的 lxml 方法及其作用:
    1. ElementTree():创建一个 XML/HTML 文档的根元素对象。
    2. Element():创建一个 XML/HTML 元素对象。
    3. fromstring():将 XML/HTML 字符串解析为一个元素对象。
    4. parse():解析 XML/HTML 文件,并返回一个根元素对象。
    5. find():查找符合指定 XPath 表达式的第一个节点,并返回该节点对象。
    6. findall():查找符合指定 XPath 表达式的所有节点,并返回节点对象列表。
    7. findtext():查找符合指定 XPath 表达式的第一个节点,并返回节点的文本值。
    8. get():获取指定节点的属性值。
    9. iter():迭代遍历文档中的所有节点。
    10. iterfind():迭代查找符合指定 XPath 表达式的节点。
    11. attrib:获取节点的属性字典。
    12. text:获取节点的文本值。
    13. tag:获取节点的标签名。
    14. getparent():获取节点的父节点。
    15. getchildren():获取节点的子节点列表。
    16. addnext():在当前节点之后插入一个新节点。
    17. addprevious():在当前节点之前插入一个新节点。
    18. append():将一个节点添加为当前节点的子节点。
    19. remove():从文档中删除当前节点。

示例 1:测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from lxml import etree

# 生成对象
tree = etree.parse('xpath.html')

# print(tree)
# ret = tree.xpath('//div[@class="tang"]/ul/li[1]/text()')
# ret = tree.xpath('//div[@class="tang"]/ul/li[last()]/a/@href')
# ret = tree.xpath('//div[@class="tang"]/ul/li[@class="love" and @name="yang"]')
# ret = tree.xpath('//li[contains(@class,"l")]')
# ret = tree.xpath('//li[contains(text(),"爱")]/text()')
# ret = tree.xpath('//li[starts-with(@class, "ba")]/text()')

# ret = tree.xpath('//div[@class="song"]')
# string = ret[0].xpath('string(.)')
# print(string.replace('\n', '').replace('\t', ''))

odiv = tree.xpath('//div[@class="tang"]')[0]

ret = odiv.xpath('.//li[@class="balove"]')
print(ret)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>xpath测试</title>
</head>
<body>
<div class="song">
火药
<b>指南针</b>
<b>印刷术</b>
造纸术
</div>
<div class="tang">
<ul>
<li class="balove">停车坐爱枫林晚,霜叶红于二月花</li>
<li id="hua">商女不知亡国恨,隔江犹唱后庭花</li>
<li class="love" name="yang">一骑红尘妃子笑,无人知是荔枝来</li>
<li id="bei">葡萄美酒夜光杯,欲饮琵琶马上催</li>
<li><a href="http://www.baidu.com/">百度一下</a></li>
</ul>
</div>
<ol>
<li class="balove">寻寻觅觅,冷冷清清,凄凄惨惨戚戚</li>
<li class="balily">乍暖还寒时候,最难将息</li>
<li class="lilei">三杯两盏淡酒</li>
<li>怎敌他晚来风急</li>
<li>雁过也,正伤心,却是旧时相识</li>
<li>爱就一个字,我只说一次</li>
<li>爱情36计,我要立刻美丽</li>
</ol>
</body>
</html>

示例 2:爬取好段子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import urllib.request
import urllib.parse
from lxml import etree
import time
import json

item_list = []


def handle_request(url, page):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36'
}
# 将url和page进行拼接
url = url.format(page)
# print(url)
request = urllib.request.Request(url=url, headers=headers)
return request


def parse_content(content):
# print('haha')
# 生成对象
tree = etree.HTML(content)
# 抓取内容
div_list = tree.xpath('//div[@class="log cate10 auth1"]')
# 遍历div列表
for odiv in div_list:
# print('lala')
# 获取标题
title = odiv.xpath('.//h3/a/text()')[0]
# print(title)
text_lt = odiv.xpath('.//div[@class="cont"]/p/text()')
text = '\n'.join(text_lt)
# print(text)
# print('*' * 50)
item = {
'标题': title,
'内容': text,
}
# 将内容添加到列表中
item_list.append(item)


def main():
start_page = int(input('请输入起始页码:'))
end_page = int(input('请输入结束页码:'))
url = 'http://www.haoduanzi.com/category-10_{}.html'
for page in range(start_page, end_page + 1):
request = handle_request(url, page)
content = urllib.request.urlopen(request).read().decode()
# 解析内容
parse_content(content)
time.sleep(2)

# 写入到文件中
# string = json.dumps(item_list, ensure_ascii=False)
with open('duanzi.txt', 'w', encoding='utf8') as fp:
fp.write(str(item_list))


if __name__ == '__main__':
main()

换行问题

  • 存储的时候显示的 \n 是有效的
  • 在读取过来单独使用的时候换行符生效
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    lt = [{'name': '宫本武藏\n小田纯一郎'}]

    # with open('lala.txt', 'w', encoding='utf8') as fp:
    # fp.write(str(lt))

    fp = open('lala.txt', 'r', encoding='utf8')
    string = fp.read()
    fp.close()

    lt = eval(string)
    print(lt[0]['name'])
    1
    2
    3
    宫本武藏
    小田纯一郎

    运行结果

下载图片

  • 懒加载技术:用到时候再加载
  • 实现方式
    1
    2
    <img src2="图片路径">
    <img src="图片路径" src2="">
  • 示例:http://sc.chinaz.com/tupian/xingganmeinvtupian.html
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    import urllib.request
    import urllib.parse
    from lxml import etree
    import time
    import os


    def handle_request(url, page):
    # 由于第一页和后面的页码规律不一样,所以要进行判断
    if page == 1:
    url = url.format('')
    else:
    url = url.format('_' + str(page))

    headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36'
    }
    request = urllib.request.Request(url=url, headers=headers)
    return request


    # 解析内容,并且下载图片
    def parse_content(content):
    tree = etree.HTML(content)
    image_list = tree.xpath('//div[@id="container"]/div/div/a/img/@src2')
    # 懒加载
    # print(image_list)
    # print(len(image_list))
    # 遍历列表,依次下载图片
    for image_src in image_list:
    download_image(image_src)


    def download_image(image_src):
    dirpath = 'xinggan'
    # 创建一个文件夹
    if not os.path.exists(dirpath):
    os.mkdir(dirpath)

    # 搞个文件名
    filename = os.path.basename(image_src)
    # 搞图片路径
    filepath = os.path.join(dirpath, filename)
    # 发送请求,保存图片
    headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36'
    }
    request = urllib.request.Request(url=image_src, headers=headers)
    response = urllib.request.urlopen(request)
    with open(filepath, 'wb') as fp:
    fp.write(response.read())


    def main():
    url = 'http://sc.chinaz.com/tupian/xingganmeinvtupian{}.html'
    # http://sc.chinaz.com/tupian/xingganmeinvtupian_2.html
    start_page = int(input('请输入起始页码:'))
    end_page = int(input('请输入结束页码:'))
    for page in range(start_page, end_page + 1):
    request = handle_request(url, page)
    content = urllib.request.urlopen(request).read().decode()
    parse_content(content)
    time.sleep(2)


    if __name__ == '__main__':
    main()