Python爬虫 3.HTML 解析基础XPath与BeautifulSoup


一、为什么需要数据解析

在前两篇中,我们已经学会了如何发送请求并获取网页内容(HTML)。

但是获取到的 HTML 通常是结构复杂的标签内容,不能直接使用。

例如:

<div>
    <h1>标题</h1>
    <p>价格:100</p>
</div>

我们真正需要的是:

  • 标题
  • 价格

HTML 本质上是一个树结构(DOM),浏览器会根据这些标签进行渲染,但爬虫拿到的只是原始文本。

因此,我们需要“解析”这些 HTML,找到我们关心的节点,并提取出数据。

总结:

  • 请求阶段:获取数据
  • 解析阶段:提取数据(本篇重点)

补充:真实网页中的复杂情况

在真实网站中,HTML 结构通常比示例复杂很多,例如:

  • 标签嵌套层级很深
  • 文本可能被多个标签拆分
  • 存在隐藏标签或无效节点

例如:

<div>
    <h1><span>标题</span></h1>
    <p>价格:<span>100</span></p>
</div>

此时如果直接使用 text(),可能会得到多个结果。

因此在实际开发中,我们需要:

  • 熟悉 DOM 结构
  • 学会逐层定位
  • 多使用调试工具(浏览器 / scrapy shell)

二、HTML 解析的两种主流方式

在 Python 爬虫中,常见的 HTML 解析方式主要有两种:

1. XPath(路径解析)

  • 通过路径定位节点
  • 类似文件系统路径
  • 适合结构清晰的页面

2. BeautifulSoup(标签解析)

  • 通过标签查找内容
  • 更接近“查找元素”的思维
  • 使用简单、灵活

总结:

  • XPath:更强大、更高效
  • BeautifulSoup:更简单、更适合入门

在实际开发中,两者都很常用。

如何选择解析方式(实战建议)

  • 页面结构规则、稳定 → 优先 XPath
  • 页面结构混乱、不规则 → BeautifulSoup 更方便
  • 需要高性能(大量数据) → XPath 更优

实际项目中,经常是两者结合使用。


三、XPath 入门

1. 安装 lxml

pip install lxml

2. 创建解析对象

# 导入 etree 模块
from lxml import etree

# 示例 HTML 字符串
html = """
<html>
    <body>
        <div id="main">
            <h1>标题</h1>
        </div>
    </body>
</html>
"""

# 将 HTML 字符串解析为树结构
html_tree = etree.HTML(html)

# 解析本地 HTML 文件(可选)
# html_tree = etree.parse('test.html')

3. XPath 基本语法

路径

/   直接子节点
//  所有子孙节点

示例

# 查找所有 div 标签
result = html_tree.xpath('//div')
print(result)

4. 属性选择

# 查找 id 为 main 的 div
result = html_tree.xpath('//div[@id="main"]')
print(result)

5. 谓词查询

谓词可以理解为“筛选条件”,常用于进一步限定节点。

# 查找所有带有 id 属性的 div
result = html_tree.xpath('//div[@id]')
print(result)

# 查找 id 为 main 的 div
result = html_tree.xpath('//div[@id="main"]')
print(result)

6. 文本提取

# 提取 div 中的文本内容
result = html_tree.xpath('//div/text()')
print(result)

注意:

html_tree.xpath('//div/text()')

只会获取当前节点的直接文本,不包括子标签中的内容。

如果需要获取所有文本,可以使用:

html_tree.xpath('//div//text()')

7. 属性提取

除了获取节点本身,也可以直接获取节点属性。

# 提取所有 class 属性
result = html_tree.xpath('//div/@class')
print(result)

8. 模糊匹配

# 查找 class 包含 item 的元素
result = html_tree.xpath('//div[contains(@class, "item")]')
print(result)

# 查找 id 以 he 开头的元素
result = html_tree.xpath('//div[starts-with(@id, "he")]')
print(result)

9. 返回值说明(非常重要)

result = html_tree.xpath('//div')

在 XPath 中,不同写法返回的内容类型可能不同。例如,选择节点时返回元素对象,选择 text()@属性 时通常返回字符串。

# 返回节点对象列表
nodes = html_tree.xpath('//div')
print(nodes)

# 返回文本列表
texts = html_tree.xpath('//h1/text()')
print(texts)

# 返回属性值列表
attrs = html_tree.xpath('//div/@id')
print(attrs)

返回值通常是:

  • list(列表)
  • 元素对象(Element)或字符串

因此,通常需要:

# 获取第一个元素
first = result[0]

# 获取文本
text = html_tree.xpath('//h1/text()')[0]

推荐写法:

# 安全获取第一个元素
result = html_tree.xpath('//h1/text()')
text = result[0] if result else None

可以避免 IndexError 异常。


XPath 调试技巧(非常重要)

可以在浏览器中直接测试 XPath:

  1. 打开开发者工具(F12)
  2. 使用 Elements 面板
  3. 按 Ctrl + F
  4. 输入 XPath 表达式

例如:

//div[@class="quote"]

如果能匹配到元素,说明 XPath 正确。


四、BeautifulSoup 入门

1. 安装

pip install bs4

2. 创建对象

# 导入 BeautifulSoup
from bs4 import BeautifulSoup

html = """
<html>
    <body>
        <a class="name">Python</a>
    </body>
</html>
"""

# 创建解析对象
soup = BeautifulSoup(html, 'lxml')

除了直接解析字符串,也可以解析本地 HTML 文件:

# 导入 BeautifulSoup
from bs4 import BeautifulSoup

# 读取本地 HTML 文件时,建议显式指定编码
with open('test.html', 'r', encoding='utf-8') as f:
    soup = BeautifulSoup(f, 'lxml')

3. 基本查找方式

标签查找

# 查找第一个 a 标签
tag = soup.find('a')
print(tag)

# 查找所有 a 标签
tags = soup.find_all('a')
print(tags)

属性查找

# 查找 class 为 name 的标签
tag = soup.find('a', class_='name')
print(tag)

直接通过标签访问

# 直接获取第一个 a 标签
print(soup.a)

# 获取标签名称
print(soup.a.name)

# 获取标签属性字典
print(soup.a.attrs)

4. select(推荐)

# 类选择器
result = soup.select('.name')

# id 选择器
result = soup.select('#main')

# 层级选择
result = soup.select('div > a')

print(result)

select() 本质上使用的是 CSS 选择器,语法非常灵活。

# 属性选择器:查找带有 class 属性的 li
result = soup.select('li[class]')
print(result)

# 属性值选择器:查找 class 为 item 的 li
result = soup.select('li[class="item"]')
print(result)

# 多元素选择器:同时查找 a 和 span
result = soup.select('a, span')
print(result)

select_one(推荐)

# 获取第一个匹配元素
result = soup.select_one('.name')
print(result)

相比 find()select_one() 更统一(与 select 配套)。


5. 获取内容

# 获取文本内容
text = tag.get_text()
print(text)

# 获取属性
attrs = tag.attrs
print(attrs)

获取属性值时,常见写法有三种:

# 方式一:通过 attrs.get 获取
value = tag.attrs.get('class')
print(value)

# 方式二:通过 get 获取
value = tag.get('class')
print(value)

# 方式三:通过字典方式获取
value = tag['class']
print(value)

注意区别:

print(tag.text)
print(tag.get_text())

两者基本一致,但 get_text() 更推荐,因为支持参数扩展。


五、XPath 与 BeautifulSoup 对比

方式 优点 缺点
XPath 功能强、速度快 语法较复杂
BeautifulSoup 简单易用 性能较低

建议:

  • 初学者先使用 BeautifulSoup
  • 熟练后建议掌握 XPath

六、实战案例:提取网页标题

# 模拟 HTML 数据
html = """
<html>
    <body>
        <h1>Python 爬虫</h1>
    </body>
</html>
"""

# 导入解析库
from lxml import etree

# 解析 HTML
html_tree = etree.HTML(html)

# 使用 XPath 提取标题
result = html_tree.xpath('//h1/text()')

# 输出结果
print(result)

输出:

['Python 爬虫']

BeautifulSoup 实现同样功能

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, 'lxml')

# 提取标题
result = soup.select_one('h1').get_text()
print(result)

七、常见问题

  1. xpath 返回空结果
  • 路径写错
  • HTML 结构和预期不一致
  1. 返回结果是列表
  • xpath 默认返回 list,需要取索引
  1. text() 获取不到内容
  • 标签中可能嵌套子标签
  1. BeautifulSoup 查不到元素
  • 解析器问题(建议使用 lxml)
  • HTML 结构变化
  1. XPath 提取结果不完整
  • 可能使用了 /text() 而不是 //text()
  1. 页面结构变化
  • 网站更新后 XPath 失效
  • 建议使用更稳定的属性(id / class)

八、XPath 进阶补充

1. 位置选择

# 获取第一个 div
result = html_tree.xpath('(//div)[1]')
print(result)

# 获取最后一个 div
result = html_tree.xpath('(//div)[last()]')
print(result)

2. 逻辑运算

# 同时满足多个条件
result = html_tree.xpath('//div[@id="head" and @class="item"]')
print(result)

3. 并集选择

# 同时获取 title 和 price
result = html_tree.xpath('//title | //price')
print(result)

4. 轴(了解即可)

# 获取父节点
result = html_tree.xpath('//h1/..')
print(result)

# 获取所有祖先节点
result = html_tree.xpath('//h1/ancestor::*')
print(result)

5. 浏览器 XPath 插件(可选)

用于辅助定位节点

  1. 打开 Chrome 浏览器
  2. 进入扩展程序页面
  3. 安装 XPath Helper 插件
  4. 使用快捷键 Ctrl + Shift + X 打开

通过在页面中输入 XPath,可以快速验证路径是否正确。


九、BeautifulSoup 补充用法

1. find_all 常用参数

# 限制返回数量
result = soup.find_all('a', limit=2)
print(result)

# 同时查找多个标签
result = soup.find_all(['a', 'span'])
print(result)

2. string 与 get_text 区别

# string:仅当标签中只有一个字符串时可用
print(tag.string)

# get_text:推荐,能获取所有文本内容
print(tag.get_text())

3. 层级查找补充

# 查找 div 下所有 p
result = soup.select('div p')
print(result)

# 只查找直接子节点
result = soup.select('div > p')
print(result)

4. recursive 参数

# 只查找当前层级(不递归)
result = soup.find_all('p', recursive=False)
print(result)

十、JSON 数据解析补充

除了 HTML 页面,爬虫中还经常遇到 JSON 数据。很多 Ajax 接口返回的不是 HTML,而是结构化 JSON。

1. 使用 json 模块

如果接口返回内容如下:

{
  "code": 0,
  "data": {
    "list": [
      {"title": "第一条数据", "price": 100},
      {"title": "第二条数据", "price": 200}
    ]
  }
}

可以使用 json.loads() 转成 Python 字典。

import json

text = '''
{
  "code": 0,
  "data": {
    "list": [
      {"title": "第一条数据", "price": 100},
      {"title": "第二条数据", "price": 200}
    ]
  }
}
'''

data = json.loads(text)

for item in data['data']['list']:
    print(item['title'], item['price'])

2. 使用 JSONPath

当 JSON 层级比较深时,也可以使用 JSONPath 来提取数据。

pip install jsonpath

示例:

import json
import jsonpath

data = json.loads(text)

titles = jsonpath.jsonpath(data, '$.data.list[*].title')
prices = jsonpath.jsonpath(data, '$.data.list[*].price')

print(titles)
print(prices)

JSONPath 的写法和 XPath 有点像:

写法 含义
$ 根节点
. 子节点
[*] 匹配列表中的所有元素
$..title 递归查找所有 title 字段

3. 图片下载的基本流程

如果页面中有图片地址,通常可以先用 XPath 或 BeautifulSoup 提取图片 URL,再逐个下载。

from urllib import request
from lxml import etree

html = '''
<html>
  <body>
    <img src="https://example.com/a.jpg">
    <img src="https://example.com/b.jpg">
  </body>
</html>
'''

tree = etree.HTML(html)
img_urls = tree.xpath('//img/@src')

for index, img_url in enumerate(img_urls):
    request.urlretrieve(img_url, f'img_{index}.jpg')

真实网站中还需要注意相对路径问题。有些图片地址可能是:

/images/a.jpg

这时需要和网站域名拼接后再下载。

十一、总结

本篇我们重点学习了 HTML 解析的两种核心方式:

  • XPath(路径解析)
  • BeautifulSoup(标签解析)

同时掌握了:

  • 如何定位节点
  • 如何提取文本与属性
  • 如何处理返回结果
  • 常见问题的排查方法
  • JSON 数据解析和图片下载的基本流程

这一篇的内容是整个爬虫体系中的核心,因为:

爬虫的本质 = 请求 + 解析

下一篇通常会进入:

  • 动态页面(Selenium)
  • 或接口抓取(JSON 数据)

文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录