1. 什么是 lxml? #
lxml 是 Python 中最强大的 XML/HTML 解析库之一。它基于 C 语言库 libxml2/libxslt,具有高性能和 Pythonic 的 API。无论你是要解析网页、处理配置文件,还是提取结构化数据,lxml 都能帮你快速完成。
为什么要用 lxml?
想象一下,你有一个 XML 配置文件,需要从中提取某些信息,或者你需要从网页中抓取数据。手动解析太慢,而 lxml 提供了强大的工具,可以快速定位和提取你需要的元素。
lxml 的优势:
- 速度快:基于 C 语言实现,比纯 Python 库快得多
- 功能强大:支持 XPath、XSLT 等高级功能
- 容错性好:可以处理格式不完美的 XML/HTML
- API 友好:提供了简单易用的 Python API
前置知识补充:
- XML(可扩展标记语言):XML 是一种用于存储和传输数据的标记语言。它使用标签来标记数据,比如
<book><title>Python 入门</title></book>。XML 是结构化的,便于程序解析。- HTML(超文本标记语言):HTML 是网页的标记语言,与 XML 类似但更宽松。HTML 使用标签来定义网页的结构,比如
<div>、<p>、<a>等。- 标签和属性:标签是
<tag>内容</tag>这样的结构,属性是标签中的额外信息,比如<book id="1">中的id="1"就是属性。- 元素(Element):一个标签及其内容组成一个元素。比如
<title>Python</title>就是一个元素。
2. 环境准备 #
在学习 lxml 之前,需要确保你的 Python 环境已经准备好。lxml 支持 Python 3.8 及以上版本。
2.1 检查 Python 版本 #
在安装之前,先检查一下你的 Python 版本是否符合要求。
# Windows PowerShell:查看 Python 版本
python --version# macOS 终端:查看 Python 版本
python3 --version2.2 安装 lxml #
lxml 可以直接通过 pip 安装。安装过程会自动下载所需的依赖包。
# 说明:Windows PowerShell 安装 lxml
# 先升级 pip 到最新版本,确保能正常安装依赖
python -m pip install --upgrade pip
# 安装 lxml 库
python -m pip install lxml# 说明:macOS / Linux 终端安装 lxml
# 先升级 pip 到最新版本
python3 -m pip install --upgrade pip
# 安装 lxml 库
python3 -m pip install lxml网络加速提示:如果从 PyPI 下载较慢,可以使用国内镜像:
- Windows:
python -m pip install lxml -i https://pypi.tuna.tsinghua.edu.cn/simple- macOS:
python3 -m pip install lxml -i https://pypi.tuna.tsinghua.edu.cn/simple
安装问题提示:
- 如果出现编译错误,可能是因为系统缺少编译工具。Windows 用户可以尝试安装预编译的二进制轮子。
- macOS 用户如果遇到编译错误,可以先执行
xcode-select --install安装 Xcode 命令行工具。
2.3 验证安装 #
安装完成后,验证一下是否安装成功。
# -*- coding: utf-8 -*-
# 说明:验证 lxml 是否安装成功
# 说明:导入 lxml 的 etree 模块
# etree 是 lxml 中用于 XML 解析的核心模块
from lxml import etree
# 说明:创建一个简单的 XML 字符串用于测试
test_xml = "<root><item>测试</item></root>"
# 说明:使用 fromstring() 方法解析 XML 字符串
# fromstring() 将字符串转换为 Element 对象
root = etree.fromstring(test_xml)
# 说明:提取元素的文本内容
# findtext() 方法查找第一个匹配的子元素并返回其文本
text = root.findtext("item")
# 说明:打印结果,验证安装是否成功
print("安装成功!提取的文本:", text)
# 说明:打印 lxml 版本信息
print(f"lxml 版本:{etree.__version__}")
# 说明:如果没有报错并输出了"测试",说明安装成功3. 核心概念 #
在使用 lxml 之前,需要理解几个核心概念。这些概念是理解 lxml 的基础。
3.1 Element(元素对象) #
Element 是 lxml 的核心对象,代表 XML/HTML 中的一个元素(标签及其内容)。每个 Element 对象都有标签名、文本内容、属性等。
3.2 根节点(Root Element) #
根节点是整个 XML/HTML 文档的最外层元素。所有其他元素都是根节点的子元素或后代元素。
3.3 父子关系 #
XML/HTML 是树状结构,元素之间有父子关系。父元素包含子元素,子元素被父元素包含。
3.4 XPath #
XPath 是一种用于在 XML/HTML 文档中定位元素的查询语言。它使用路径表达式来选择元素或节点。
4. 快速体验:解析 XML #
让我们通过一个最简单的例子来快速体验 lxml 的强大功能。
4.1 解析 XML 字符串 #
# -*- coding: utf-8 -*-
# 说明:快速体验 lxml 的基本用法 - 解析 XML
# 说明:导入 lxml 的 etree 模块
# etree 是 lxml 中用于 XML 解析的核心模块
from lxml import etree
# 说明:准备一个示例 XML 字符串
# 这是一个简单的书籍列表,包含两本书的信息
xml_text = """
<books>
<book id="1">
<title>Python 入门</title>
<price>39.9</price>
</book>
<book id="2">
<title>数据科学</title>
<price>49.9</price>
</book>
</books>
"""
# 说明:使用 fromstring() 方法解析 XML 字符串
# fromstring() 将 XML 字符串转换为 Element 对象(根节点)
xml_root = etree.fromstring(xml_text)
# 说明:遍历所有 book 元素
# 可以直接遍历元素的子元素
for book in xml_root:
# 说明:使用 findtext() 方法查找子元素并获取其文本内容
# findtext() 返回第一个匹配的子元素的文本,如果找不到返回 None
title = book.findtext("title")
# 说明:获取价格文本
price = book.findtext("price")
# 说明:打印每本书的信息
print(f"标题:{title} | 价格:{price}")
# 说明:输出结果应该是:
# 标题:Python 入门 | 价格:39.9
# 标题:数据科学 | 价格:49.94.2 解析 HTML #
lxml 也可以解析 HTML,使用 html 模块。
# -*- coding: utf-8 -*-
# 说明:快速体验 lxml 的基本用法 - 解析 HTML
# 说明:导入 lxml 的 html 模块
# html 模块专门用于解析 HTML 文档
from lxml import html
# 说明:准备一个示例 HTML 字符串
# 这是一个简单的 HTML 片段,包含一个段落
html_text = "<div><p class='intro'>欢迎使用 lxml</p></div>"
# 说明:使用 fromstring() 方法解析 HTML 字符串
# html.fromstring() 将 HTML 字符串转换为 Element 对象
html_root = html.fromstring(html_text)
# 说明:使用 XPath 查找所有段落并提取文本
# //p 表示查找所有 p 标签(无论在文档的哪个位置)
# /text() 表示获取标签内的文本内容
# xpath() 方法返回一个列表
paragraphs = html_root.xpath("//p/text()")
# 说明:打印提取的文本
print("段落文本:", paragraphs)
# 说明:输出结果应该是:['欢迎使用 lxml']5. 元素导航:访问和修改元素 #
lxml 提供了多种方式来访问和修改 XML/HTML 元素。理解这些方法对于数据提取和处理非常重要。
5.1 访问元素 #
| 方法/属性 | 功能说明 | 常用参数 | 返回值 |
|---|---|---|---|
etree.fromstring() |
从字符串解析 XML | text:XML 字符串 |
Element 对象(根节点) |
root[index] |
通过索引访问子元素 | index:子元素索引(从 0 开始) |
子 Element 对象 |
.findtext() |
查找第一个匹配的子元素并返回其文本 | tag:标签名 |
文本字符串(找不到返回 None) |
.attrib |
获取元素的所有属性 | 无(属性) | 字典,包含所有属性 |
.find() |
查找第一个匹配的子元素 | tag:标签名 |
Element 对象(找不到返回 None) |
# -*- coding: utf-8 -*-
# 说明:演示如何访问和导航 XML 元素
# 说明:导入 lxml 的 etree 模块
from lxml import etree
# 说明:准备一个示例 XML 字符串
# 这是一个商品列表,包含两个商品的信息
xml = """
<store>
<item category="tech">
<name>键盘</name>
<price>199</price>
</item>
<item category="book">
<name>算法导论</name>
<price>99</price>
</item>
</store>
"""
# 说明:使用 fromstring() 解析 XML 字符串
# 返回根节点 Element 对象
root = etree.fromstring(xml)
# 说明:访问第一个 item 元素
# 使用索引 [0] 访问第一个子元素(索引从 0 开始)
first_item = root[0]
# 说明:使用 findtext() 方法查找 name 子元素并获取文本
# findtext() 返回第一个匹配的子元素的文本内容
name = first_item.findtext("name")
print("第一个商品名称:", name)
# 说明:获取元素的属性
# attrib 是一个字典,包含元素的所有属性
attributes = first_item.attrib
print("商品属性:", attributes)
# 说明:访问第二个 item 元素
# 使用索引 [1] 访问第二个子元素
second_item = root[1]
# 说明:获取第二个商品的名称和价格
second_name = second_item.findtext("name")
second_price = second_item.findtext("price")
print(f"\n第二个商品:{second_name},价格:{second_price}")
# 说明:遍历所有 item 元素
print("\n所有商品:")
for item in root:
name = item.findtext("name")
price = item.findtext("price")
category = item.get("category") # 使用 get() 方法获取属性值
print(f" 名称:{name},价格:{price},类别:{category}")5.2 修改元素 #
| 方法 | 功能说明 | 常用参数 | 返回值 |
|---|---|---|---|
.find() |
查找第一个匹配的子元素 | tag:标签名 |
Element 对象(找不到返回 None) |
.text |
获取或设置元素的文本内容 | 无(属性),可直接赋值 | 文本字符串 |
etree.SubElement() |
创建并添加子元素 | parent:父元素,tag:标签名,**attrib:属性(可选) |
新创建的 Element 对象 |
etree.tostring() |
将 Element 对象转换为 XML 字符串 | element:Element 对象,encoding:编码,pretty_print:是否格式化 |
XML 字符串 |
# -*- coding: utf-8 -*-
# 说明:演示如何修改 XML 元素
# 说明:导入 lxml 的 etree 模块
from lxml import etree
# 说明:准备一个示例 XML 字符串
xml = """
<store>
<item category="tech">
<name>键盘</name>
<price>199</price>
</item>
<item category="book">
<name>算法导论</name>
<price>99</price>
</item>
</store>
"""
# 说明:解析 XML 字符串
root = etree.fromstring(xml)
# 说明:获取第一个商品元素
first_item = root[0]
# 说明:查找 price 元素
# find() 方法返回第一个匹配的子元素对象
price_node = first_item.find("price")
# 说明:修改价格文本
# text 属性可以直接读取和修改元素的文本内容
price_node.text = "188"
print("价格已修改为:", price_node.text)
# 说明:创建新的子元素
# SubElement() 函数创建新的子元素并添加到父元素中
# 参数:父元素、标签名、属性(可选)
new_node = etree.SubElement(first_item, "stock")
# 说明:设置新元素的文本内容
new_node.text = "有货"
# 说明:将 Element 对象转换为格式化的 XML 字符串
# tostring() 将 Element 对象转换回 XML 字符串
# encoding="unicode" 表示返回 Unicode 字符串(而不是字节)
# pretty_print=True 表示格式化输出(添加缩进和换行)
xml_str = etree.tostring(root, encoding="unicode", pretty_print=True)
# 说明:打印修改后的 XML
print("\n修改后的 XML:")
print(xml_str)5.3 遍历元素 #
| 方法/属性 | 功能说明 | 常用参数 | 返回值 |
|---|---|---|---|
.tag |
获取元素的标签名 | 无(属性) | 标签名字符串 |
.text |
获取元素的文本内容 | 无(属性) | 文本字符串 |
.get() |
获取元素的属性值 | key:属性名 |
属性值字符串(找不到返回 None) |
.findall() |
查找所有匹配的子元素 | tag:标签名 |
Element 对象列表 |
# -*- coding: utf-8 -*-
# 说明:演示如何遍历 XML 元素树
# 说明:导入 lxml 的 etree 模块
from lxml import etree
# 说明:准备一个示例 XML 字符串
xml = """
<store>
<item category="tech">
<name>键盘</name>
<price>199</price>
<stock>10</stock>
</item>
<item category="book">
<name>算法导论</name>
<price>99</price>
<stock>5</stock>
</item>
</store>
"""
# 说明:解析 XML 字符串
root = etree.fromstring(xml)
# 说明:方法1:直接遍历子元素
# 可以直接使用 for 循环遍历元素的直接子元素
print("方法1:直接遍历子元素")
for item in root:
# 说明:tag 属性返回元素的标签名
print(f"标签名:{item.tag}")
# 说明:attrib 返回元素的属性字典
print(f"属性:{item.attrib}")
# 说明:遍历子元素的子元素
for child in item:
# 说明:tag 获取标签名,text 获取文本内容
print(f" {child.tag}:{child.text}")
print()
# 说明:方法2:使用 findall() 查找所有匹配的元素
# findall() 返回所有匹配的子元素列表
print("方法2:使用 findall() 查找所有 price 元素")
prices = root.findall(".//price") # .//price 表示查找所有 price 元素(无论在哪个层级)
for price in prices:
print(f"价格:{price.text}")
# 说明:方法3:递归遍历所有元素
print("\n方法3:递归遍历所有元素")
def print_elements(element, indent=0):
"""
递归打印所有元素
参数:
element: Element 对象
indent: 缩进级别
"""
# 说明:打印当前元素的信息
# " " * indent 创建缩进字符串
print(" " * indent + f"<{element.tag}>")
# 说明:如果有文本内容,打印文本
if element.text and element.text.strip():
print(" " * (indent + 1) + element.text.strip())
# 说明:递归遍历所有子元素
for child in element:
print_elements(child, indent + 1)
# 说明:打印结束标签
print(" " * indent + f"</{element.tag}>")
# 说明:调用递归函数打印所有元素
print_elements(root)6. XPath 查询:强大的元素定位工具 #
XPath 是 lxml 最强大的功能之一,它提供了一种简洁的方式来定位和选择 XML/HTML 元素。掌握 XPath 可以大大提高数据提取的效率。
6.1 XPath 基础语法 #
| 方法 | 功能说明 | 常用参数 | 返回值 |
|---|---|---|---|
.xpath() |
使用 XPath 表达式查找元素 | xpath:XPath 表达式字符串 |
结果列表(元素、文本或属性值) |
# -*- coding: utf-8 -*-
# 说明:演示 XPath 的基本语法和用法
# 说明:导入 lxml 的 etree 模块
from lxml import etree
# 说明:准备一个示例 XML 字符串
xml = """
<library>
<book category="programming">
<title lang="zh">流畅的 Python</title>
<price>79</price>
</book>
<book category="fiction">
<title lang="en">1984</title>
<price>59</price>
</book>
<book category="programming">
<title lang="zh">Python 数据分析</title>
<price>89</price>
</book>
</library>
"""
# 说明:解析 XML 字符串
root = etree.fromstring(xml)
# 说明:XPath 表达式1:查找所有 title 元素的文本
# //title 表示查找所有 title 元素(无论在文档的哪个位置)
# /text() 表示获取元素的文本内容
titles = root.xpath("//title/text()")
print("所有标题:", titles)
# 说明:XPath 表达式2:按属性筛选
# //title[@lang="zh"] 表示查找 lang 属性值为 "zh" 的 title 元素
# @lang 表示 lang 属性,[@lang="zh"] 是属性条件
cn_titles = root.xpath('//title[@lang="zh"]/text()')
print("\n中文标题:", cn_titles)
# 说明:XPath 表达式3:按元素内容筛选
# //book[price>60] 表示查找 price 子元素的值大于 60 的 book 元素
# [price>60] 是条件表达式
expensive = root.xpath('//book[price>60]/title/text()')
print("\n价格>60 的书:", expensive)
# 说明:XPath 表达式4:查找特定位置的元素
# //book[1] 表示查找第一个 book 元素(索引从 1 开始)
first_book = root.xpath("//book[1]/title/text()")
print("\n第一本书:", first_book)
# 说明:XPath 表达式5:查找所有 book 的 category 属性
# //book/@category 表示查找所有 book 元素的 category 属性
categories = root.xpath("//book/@category")
print("\n所有类别:", categories)6.2 常用的 XPath 语法 #
下面是一个更全面的 XPath 示例,展示各种常用的查询方式。
| XPath 表达式 | 功能说明 | 示例 |
|---|---|---|
//tag |
查找所有 tag 标签(无论在哪个位置) | //book 查找所有 book 元素 |
tag |
查找当前元素下的 tag 标签 | title 查找直接子元素 title |
[@attr="value"] |
按属性筛选 | [@id="1"] 查找 id 属性为 "1" 的元素 |
/text() |
获取元素的文本内容 | //title/text() 获取所有 title 的文本 |
[@attr] |
查找有某个属性的元素 | [@class] 查找有 class 属性的元素 |
[condition] |
添加条件 | [price>60] 查找 price 值大于 60 的元素 |
tag[@attr] |
查找有特定属性的标签 | book[@category] 查找有 category 属性的 book |
# -*- coding: utf-8 -*-
# 说明:演示更多 XPath 语法和用法
# 说明:导入 lxml 的 etree 模块
from lxml import etree
# 说明:准备一个更复杂的示例 XML
xml = """
<library>
<section name="编程">
<book id="1" category="programming">
<title lang="zh">流畅的 Python</title>
<price>79</price>
<author>Luciano Ramalho</author>
</book>
<book id="2" category="programming">
<title lang="zh">Python 数据分析</title>
<price>89</price>
<author>Wes McKinney</author>
</book>
</section>
<section name="小说">
<book id="3" category="fiction">
<title lang="en">1984</title>
<price>59</price>
<author>George Orwell</author>
</book>
</section>
</library>
"""
# 说明:解析 XML 字符串
root = etree.fromstring(xml)
# 说明:查找所有有 id 属性的 book 元素
# //book[@id] 表示查找所有有 id 属性的 book 元素
books_with_id = root.xpath("//book[@id]")
print("有 ID 的书数量:", len(books_with_id))
# 说明:查找 id 属性为 "1" 的 book 的标题
# //book[@id="1"] 表示查找 id 属性为 "1" 的 book 元素
book_title = root.xpath('//book[@id="1"]/title/text()')
print("ID 为 1 的书标题:", book_title)
# 说明:查找所有 section 的 name 属性
# //section/@name 表示查找所有 section 元素的 name 属性
section_names = root.xpath("//section/@name")
print("\n所有章节名称:", section_names)
# 说明:查找第一个 section 下的所有 book
# //section[1]/book 表示查找第一个 section 下的 book 子元素
first_section_books = root.xpath("//section[1]/book/title/text()")
print("\n第一个章节的书:", first_section_books)
# 说明:查找价格在 60 到 90 之间的书
# //book[price>=60 and price<=90] 表示查找 price 在指定范围内的 book
# and 是逻辑与运算符
affordable_books = root.xpath('//book[price>=60 and price<=90]/title/text()')
print("\n价格在 60-90 之间的书:", affordable_books)
# 说明:查找有 author 子元素的书
# //book[author] 表示查找有 author 子元素的 book
books_with_author = root.xpath("//book[author]/title/text()")
print("\n有作者信息的书:", books_with_author)7. 创建和生成 XML #
除了解析 XML,lxml 还可以从头创建 XML 文档。这在需要生成配置文件或数据文件时非常有用。
7.1 创建 XML 元素 #
| 方法 | 功能说明 | 常用参数 | 返回值 |
|---|---|---|---|
etree.Element() |
创建根元素 | tag:标签名,**attrib:属性(可选) |
Element 对象 |
etree.SubElement() |
创建并添加子元素 | parent:父元素,tag:标签名,**attrib:属性(可选) |
新创建的 Element 对象 |
.text |
设置元素的文本内容 | 无(属性),可直接赋值 | 无 |
etree.tostring() |
将 Element 对象转换为 XML 字符串 | element:Element 对象,encoding:编码,pretty_print:是否格式化,xml_declaration:是否包含 XML 声明 |
XML 字符串 |
# -*- coding: utf-8 -*-
# 说明:演示如何从零创建 XML 文档
# 说明:导入 lxml 的 etree 模块
from lxml import etree
# 说明:创建根元素
# Element() 函数创建根元素,参数是标签名
catalog = etree.Element("catalog")
# 说明:创建第一个 book 子元素并添加属性
# SubElement() 函数创建子元素并自动添加到父元素中
# 参数:父元素、标签名、属性(可选)
book1 = etree.SubElement(catalog, "book", id="1001")
# 说明:创建 title 子元素
title1 = etree.SubElement(book1, "title")
# 说明:设置 title 元素的文本内容
# text 属性可以直接设置元素的文本
title1.text = "爬虫实战"
# 说明:创建 author 子元素并设置文本
author1 = etree.SubElement(book1, "author")
author1.text = "张三"
# 说明:创建第二个 book 子元素
book2 = etree.SubElement(catalog, "book", id="1002")
title2 = etree.SubElement(book2, "title")
title2.text = "数据分析"
author2 = etree.SubElement(book2, "author")
author2.text = "李四"
# 说明:将 Element 对象转换为格式化的 XML 字符串
# tostring() 函数将 Element 对象转换回 XML 字符串
# encoding="unicode" 表示返回 Unicode 字符串(而不是字节)
# pretty_print=True 表示格式化输出(添加缩进和换行,使 XML 更易读)
# xml_declaration=True 表示在开头添加 XML 声明(<?xml version="1.0"?>)
xml_str = etree.tostring(
catalog,
encoding="unicode",
pretty_print=True,
xml_declaration=True
)
# 说明:打印生成的 XML 字符串
print("生成的 XML:")
print(xml_str)7.2 保存 XML 到文件 #
| 方法 | 功能说明 | 常用参数 | 返回值 |
|---|---|---|---|
etree.ElementTree() |
创建 ElementTree 对象(用于文件操作) | element:根元素 |
ElementTree 对象 |
.write() |
将 XML 写入文件 | file:文件路径或文件对象,encoding:编码,pretty_print:是否格式化,xml_declaration:是否包含 XML 声明 |
无 |
# -*- coding: utf-8 -*-
# 说明:演示如何将 XML 保存到文件
# 说明:导入 lxml 的 etree 模块
from lxml import etree
import os
# 说明:创建 XML 结构
catalog = etree.Element("catalog")
book = etree.SubElement(catalog, "book", id="1001")
title = etree.SubElement(book, "title")
title.text = "爬虫实战"
author = etree.SubElement(book, "author")
author.text = "张三"
# 说明:方法1:使用 tostring() 和文件写入
# 先将 Element 转换为字符串,再写入文件
xml_str = etree.tostring(
catalog,
encoding="unicode",
pretty_print=True,
xml_declaration=True
)
# 说明:打开文件并写入 XML 字符串
# encoding="utf-8" 指定文件编码为 UTF-8
with open("catalog.xml", "w", encoding="utf-8") as f:
f.write(xml_str)
print("XML 已保存到 catalog.xml(方法1)")
# 说明:方法2:使用 ElementTree 的 write() 方法
# ElementTree 是 Element 的包装类,提供了文件操作方法
tree = etree.ElementTree(catalog)
tree.write(
"catalog2.xml",
encoding="utf-8",
pretty_print=True,
xml_declaration=True
)
print("XML 已保存到 catalog2.xml(方法2)")
# 说明:验证文件是否创建成功
if os.path.exists("catalog.xml"):
print("\n✓ catalog.xml 文件创建成功")
# 说明:读取并显示文件内容
with open("catalog.xml", "r", encoding="utf-8") as f:
content = f.read()
print("文件内容:")
print(content)
# 说明:清理临时文件(可选)
if os.path.exists("catalog.xml"):
os.remove("catalog.xml")
if os.path.exists("catalog2.xml"):
os.remove("catalog2.xml")8. HTML 解析与清洗 #
lxml 不仅可以解析 XML,还可以解析 HTML。对于处理网页数据,HTML 解析功能非常有用。
8.1 解析 HTML #
| 方法 | 功能说明 | 常用参数 | 返回值 |
|---|---|---|---|
html.fromstring() |
从字符串解析 HTML | html_text:HTML 字符串 |
Element 对象(根节点) |
.xpath() |
使用 XPath 查询元素 | xpath:XPath 表达式 |
结果列表 |
html.tostring() |
将 Element 对象转换为 HTML 字符串 | element:Element 对象,encoding:编码,pretty_print:是否格式化 |
HTML 字符串 |
# -*- coding: utf-8 -*-
# 说明:演示如何解析 HTML
# 说明:导入 lxml 的 html 模块
from lxml import html
# 说明:准备一个示例 HTML 字符串
# 这是一个简单的网页片段,包含标题、段落和链接
html_text = """
<html>
<body>
<h1>欢迎</h1>
<p class="intro">这是一个示例网页</p>
<ul>
<li><a href="/home">首页</a></li>
<li><a href="/about">关于</a></li>
</ul>
</body>
</html>
"""
# 说明:使用 fromstring() 解析 HTML 字符串
# html.fromstring() 专门用于解析 HTML,可以处理格式不完美的 HTML
doc = html.fromstring(html_text)
# 说明:使用 XPath 查找标题文本
# //h1 表示查找所有 h1 标签,/text() 表示获取文本内容
h1_text = doc.xpath("//h1/text()")
print("标题:", h1_text)
# 说明:使用 XPath 查找所有链接
# //a 表示查找所有 a 标签
links = doc.xpath("//a")
print("\n所有链接:")
for link in links:
# 说明:text 属性获取链接文本
text = link.text
# 说明:get() 方法获取属性值
href = link.get("href")
print(f" 文本:{text},地址:{href}")
# 说明:使用 XPath 查找特定 class 的段落
# //p[@class="intro"] 表示查找 class 属性为 "intro" 的 p 标签
intro_paragraph = doc.xpath('//p[@class="intro"]/text()')
print("\n介绍段落:", intro_paragraph)
# 说明:将 HTML 转换为字符串
html_str = html.tostring(doc, encoding="unicode", pretty_print=True)
print("\n转换后的 HTML:")
print(html_str)8.2 HTML 清洗 #
HTML 清洗可以去除不需要的内容(如脚本、样式),让 HTML 更干净、更安全。
| 方法 | 功能说明 | 常用参数 | 返回值 |
|---|---|---|---|
Cleaner() |
创建 HTML 清洗器 | scripts:是否移除脚本,javascript:是否移除 JavaScript,style:是否移除样式 |
Cleaner 对象 |
.clean_html() |
清洗 HTML 元素 | doc:Element 对象 |
清洗后的 Element 对象 |
# -*- coding: utf-8 -*-
# 说明:演示如何清洗 HTML(移除脚本和样式)
# 说明:导入 lxml 的 html 模块和 Cleaner 类
from lxml import html
from lxml.html.clean import Cleaner
# 说明:准备一个包含脚本和不完整标签的 HTML
# 这个 HTML 包含可能不安全的脚本和格式问题
dirty_html = """
<html>
<body>
<h1>示例网页</h1>
<p>这是一段内容</p>
<script>alert('这是恶意脚本')</script>
<style>body { color: red; }</style>
<p>这是另一段内容
<div>未闭合的 div
</body>
</html>
"""
# 说明:创建 HTML 清洗器
# Cleaner 可以移除 HTML 中的不安全内容和格式问题
cleaner = Cleaner(
scripts=True, # 移除 <script> 标签及其内容
javascript=True, # 移除 JavaScript 代码(包括内联的)
style=True, # 移除 <style> 标签及其内容
comments=True, # 移除 HTML 注释
page_structure=True, # 修复页面结构问题
safe_attrs_only=True # 只保留安全的属性
)
# 说明:先解析 HTML
# fromstring() 可以自动修复一些格式问题
doc = html.fromstring(dirty_html)
# 说明:清洗 HTML
# clean_html() 方法返回清洗后的 Element 对象
clean_doc = cleaner.clean_html(doc)
# 说明:将清洗后的 HTML 转换为字符串并打印
# tostring() 将 Element 对象转换回 HTML 字符串
clean_html_str = html.tostring(clean_doc, encoding="unicode", pretty_print=True)
print("清洗后的 HTML:")
print(clean_html_str)
# 说明:验证脚本和样式是否被移除
# 检查清洗后的 HTML 中是否还有 script 和 style 标签
if "script" not in clean_html_str.lower() and "style" not in clean_html_str.lower():
print("\n✓ 脚本和样式已成功移除")9. 从文件读取 XML #
在实际应用中,XML 数据通常存储在文件中。lxml 提供了多种方式来读取文件。
9.1 读取 XML 文件 #
| 方法 | 功能说明 | 常用参数 | 返回值 |
|---|---|---|---|
etree.parse() |
从文件解析 XML | source:文件路径或文件对象 |
ElementTree 对象 |
.getroot() |
获取根元素 | 无 | Element 对象(根节点) |
# -*- coding: utf-8 -*-
# 说明:演示如何从文件读取和解析 XML
# 说明:导入 lxml 的 etree 模块
from lxml import etree
import os
# 说明:先创建一个示例 XML 文件
xml_content = """<?xml version="1.0" encoding="utf-8"?>
<books>
<book id="1">
<title>Python 入门</title>
<price>39.9</price>
</book>
<book id="2">
<title>数据科学</title>
<price>49.9</price>
</book>
</books>
"""
# 说明:将 XML 内容写入文件
# 创建或覆盖文件并写入 XML 内容
with open("books.xml", "w", encoding="utf-8") as f:
f.write(xml_content)
print("XML 文件已创建:books.xml")
# 说明:方法1:使用 parse() 从文件解析 XML
# parse() 函数从文件路径或文件对象解析 XML
# 返回 ElementTree 对象
tree = etree.parse("books.xml")
# 说明:获取根元素
# getroot() 方法返回根元素对象
root = tree.getroot()
# 说明:提取所有书籍信息
print("\n所有书籍:")
for book in root.findall("book"):
title = book.findtext("title")
price = book.findtext("price")
book_id = book.get("id")
print(f" ID:{book_id},标题:{title},价格:{price}")
# 说明:方法2:先读取文件内容,再用 fromstring() 解析
# 这种方式适合需要预处理文件内容的情况
with open("books.xml", "r", encoding="utf-8") as f:
xml_text = f.read()
# 说明:从字符串解析 XML
root2 = etree.fromstring(xml_text)
# 说明:使用 XPath 提取所有标题
titles = root2.xpath("//title/text()")
print(f"\n所有标题:{titles}")
# 说明:清理临时文件(可选)
if os.path.exists("books.xml"):
os.remove("books.xml")
print("\n临时文件已清理")10. 实战案例:RSS 解析器 #
下面是一个完整的实战案例,封装成一个 RSS 解析器类,可以解析 RSS 格式的订阅源。
| 方法 | 功能说明 | 常用参数 | 返回值 |
|---|---|---|---|
etree.fromstring() |
从字符串解析 XML | text:XML 字符串 |
Element 对象 |
.find() |
查找第一个匹配的子元素 | tag:标签名 |
Element 对象 |
.findtext() |
查找第一个匹配的子元素并返回其文本 | tag:标签名 |
文本字符串 |
.findall() |
查找所有匹配的子元素 | tag:标签名 |
Element 对象列表 |
# -*- coding: utf-8 -*-
# 说明:实战案例 - RSS 解析器
# 封装一个完整的 RSS 解析器类,可以解析 RSS 订阅源
# 说明:导入 lxml 的 etree 模块
from lxml import etree
# 说明:定义 RSS 解析器类
class SimpleRSSParser:
"""
简单的 RSS 解析器
功能:
- 解析 RSS 2.0 格式的订阅源
- 提取频道信息和文章列表
"""
# 说明:初始化方法
def __init__(self):
"""
初始化解析器
"""
# 说明:命名空间字典(用于处理带命名空间的 XML)
# 目前为空,如果遇到带命名空间的 RSS 可以添加
self.ns = {}
# 说明:解析 RSS 字符串的方法
def parse(self, rss_text: str):
"""
解析 RSS 字符串并返回结构化数据
参数:
rss_text (str): RSS 格式的 XML 字符串
返回:
dict: 包含频道信息和文章列表的字典
"""
# 说明:使用 fromstring() 解析 RSS 字符串
# 返回根元素对象
root = etree.fromstring(rss_text)
# 说明:查找 channel 元素
# RSS 2.0 格式中,channel 元素包含所有信息
channel = root.find("channel")
# 说明:提取频道基本信息
# 使用 findtext() 方法查找并提取文本
result = {
"title": channel.findtext("title"), # 频道标题
"link": channel.findtext("link"), # 频道链接
"description": channel.findtext("description"), # 频道描述
"items": [] # 文章列表
}
# 说明:遍历所有 item 元素(文章条目)
# findall() 方法返回所有匹配的子元素列表
for item in channel.findall("item"):
# 说明:提取每篇文章的信息
# 使用 findtext() 提取文本内容
article = {
"title": item.findtext("title"), # 文章标题
"link": item.findtext("link"), # 文章链接
"description": item.findtext("description"), # 文章描述
"pubDate": item.findtext("pubDate") # 发布日期
}
# 说明:将文章信息添加到列表中
result["items"].append(article)
# 说明:返回解析结果
return result
# 说明:获取文章数量的方法
def get_item_count(self, rss_text: str):
"""
获取 RSS 中的文章数量
参数:
rss_text (str): RSS 格式的 XML 字符串
返回:
int: 文章数量
"""
# 说明:解析 RSS
root = etree.fromstring(rss_text)
# 说明:使用 XPath 统计 item 元素的数量
# count() 是 XPath 函数,用于统计元素数量
count = root.xpath("count(//item)")
# 说明:返回整数形式的数量
return int(count)
# 说明:主程序入口,演示如何使用
if __name__ == "__main__":
# 说明:创建解析器实例
parser = SimpleRSSParser()
# 说明:示例 RSS 内容(RSS 2.0 格式)
rss_demo = """<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>示例订阅</title>
<link>https://example.com</link>
<description>这是一个示例 RSS 订阅源</description>
<item>
<title>第一篇文章</title>
<link>https://example.com/post1</link>
<description>这是第一篇文章的内容</description>
<pubDate>Mon, 01 Jan 2024 12:00:00 GMT</pubDate>
</item>
<item>
<title>第二篇文章</title>
<link>https://example.com/post2</link>
<description>这是第二篇文章的内容</description>
<pubDate>Tue, 02 Jan 2024 12:00:00 GMT</pubDate>
</item>
</channel>
</rss>
"""
# 说明:使用解析器解析 RSS
feed = parser.parse(rss_demo)
# 说明:打印频道信息
print("频道信息:")
print(f" 标题:{feed['title']}")
print(f" 链接:{feed['link']}")
print(f" 描述:{feed['description']}")
# 说明:打印文章列表
print(f"\n文章列表(共 {len(feed['items'])} 篇):")
for idx, item in enumerate(feed["items"], 1):
print(f"\n文章 {idx}:")
print(f" 标题:{item['title']}")
print(f" 链接:{item['link']}")
print(f" 描述:{item['description']}")
print(f" 发布日期:{item['pubDate']}")
# 说明:使用 get_item_count() 方法获取文章数量
count = parser.get_item_count(rss_demo)
print(f"\n文章总数:{count}")11. 性能优化:流式解析大型文件 #
当处理大型 XML 文件时,一次性加载整个文件可能会占用大量内存。lxml 提供了流式解析功能,可以逐元素处理,大大降低内存占用。
11.1 流式解析 #
| 方法 | 功能说明 | 常用参数 | 返回值 |
|---|---|---|---|
etree.iterparse() |
流式解析 XML 文件(逐元素处理) | source:文件路径或文件对象,tag:要处理的标签名(可选) |
迭代器,返回 (事件, 元素) 元组 |
.clear() |
清空元素及其子元素(释放内存) | 无 | 无 |
# -*- coding: utf-8 -*-
# 说明:演示如何使用流式解析处理大型 XML 文件
# 说明:导入 lxml 的 etree 模块
from lxml import etree
import os
# 说明:先创建一个示例 XML 文件(模拟大型文件)
# 在实际场景中,这个文件可能包含数千或数万个元素
xml_content = """<?xml version="1.0" encoding="utf-8"?>
<books>
<book>
<title>Python 入门</title>
<price>39.9</price>
</book>
<book>
<title>数据科学</title>
<price>49.9</price>
</book>
<book>
<title>机器学习</title>
<price>59.9</price>
</book>
</books>
"""
# 说明:将 XML 内容写入文件
with open("large_books.xml", "w", encoding="utf-8") as f:
f.write(xml_content)
print("XML 文件已创建:large_books.xml")
# 说明:定义流式解析函数
def stream_books(path: str):
"""
流式解析 XML 文件中的 book 元素
参数:
path (str): XML 文件路径
"""
# 说明:使用 iterparse() 流式解析 XML 文件
# iterparse() 返回一个迭代器,每次返回 (事件, 元素) 元组
# tag="book" 指定只处理 book 元素(提高效率)
# events=("end",) 指定只处理元素结束事件(此时元素已完全解析)
for event, elem in etree.iterparse(path, tag="book", events=("end",)):
# 说明:提取元素信息
# 此时 elem 是一个完整的 book 元素
data = {
"title": elem.findtext("title"), # 查找 title 子元素并获取文本
"price": elem.findtext("price") # 查找 price 子元素并获取文本
}
# 说明:处理数据(这里只是打印,实际使用时可以进行数据库写入等操作)
print("流式读取:", data)
# 说明:清空元素(释放内存)
# clear() 方法清空元素及其所有子元素和属性
# 这对于流式解析非常重要,可以防止内存占用不断增长
elem.clear()
# 说明:调用流式解析函数
print("\n开始流式解析:")
stream_books("large_books.xml")
# 说明:清理临时文件(可选)
if os.path.exists("large_books.xml"):
os.remove("large_books.xml")
print("\n临时文件已清理")12. 常见问题与排查 #
在使用 lxml 时,可能会遇到一些常见问题。本节提供解决方案。
12.1 XML 语法错误处理 #
问题:XML 格式不正确导致解析失败。
解决方案:
| 方法 | 功能说明 | 常用参数 | 返回值 |
|---|---|---|---|
etree.XMLParser() |
创建 XML 解析器 | recover:是否尝试修复错误(True/False) |
XMLParser 对象 |
.parse() |
使用解析器解析文件 | source:文件路径或文件对象,parser:解析器对象 |
ElementTree 对象 |
# -*- coding: utf-8 -*-
# 说明:演示如何处理 XML 语法错误
# 说明:导入 lxml 的 etree 模块
from lxml import etree
# 说明:准备一个有错误的 XML(缺少结束标签)
bad_xml = """
<books>
<book id="1">
<title>Python 入门</title>
<price>39.9</price>
<!-- 缺少 </book> 标签 -->
</books>
"""
# 说明:方法1:使用 recover=True 尝试自动修复
# XMLParser(recover=True) 创建一个会尝试修复错误的解析器
parser = etree.XMLParser(recover=True)
try:
# 说明:使用 recover 解析器解析 XML
# recover=True 会尝试修复一些常见的 XML 错误
root = etree.fromstring(bad_xml, parser=parser)
print("XML 已修复并解析成功")
# 说明:检查是否有错误日志
# error_log 包含解析过程中遇到的错误信息
if parser.error_log:
print("解析过程中发现的错误:")
for error in parser.error_log:
print(f" {error}")
except etree.XMLSyntaxError as e:
# 说明:捕获 XML 语法错误
print(f"XML 语法错误:{e}")
# 说明:方法2:先验证 XML 格式
good_xml = """
<books>
<book id="1">
<title>Python 入门</title>
<price>39.9</price>
</book>
</books>
"""
# 说明:使用标准解析器解析正确的 XML
try:
root = etree.fromstring(good_xml)
print("\n正确的 XML 解析成功")
except etree.XMLSyntaxError as e:
print(f"XML 语法错误:{e}")12.2 编码问题 #
问题:解析 XML/HTML 时出现乱码。
解决方案:
# -*- coding: utf-8 -*-
# 说明:演示如何处理编码问题
# 说明:导入 lxml 的 etree 模块
from lxml import etree
# 说明:准备包含中文的 XML
xml_utf8 = """<?xml version="1.0" encoding="utf-8"?>
<books>
<book>
<title>Python 入门教程</title>
<price>39.9</price>
</book>
</books>
"""
# 说明:方法1:直接解析(lxml 会自动处理编码)
# fromstring() 会自动识别 XML 声明中的编码
root = etree.fromstring(xml_utf8)
title = root.findtext("book/title")
print("标题(方法1):", title)
# 说明:方法2:如果 XML 声明中没有指定编码,可以手动指定
xml_no_encoding = """
<books>
<book>
<title>Python 入门教程</title>
</book>
</books>
"""
# 说明:先解码为字符串,再解析
# 如果是字节串,需要先解码
xml_bytes = xml_no_encoding.encode("utf-8")
root2 = etree.fromstring(xml_bytes.decode("utf-8"))
title2 = root2.findtext("book/title")
print("标题(方法2):", title2)12.3 命名空间处理 #
问题:XML 中包含命名空间,XPath 查询找不到元素。
解决方案:
| 方法 | 功能说明 | 常用参数 | 返回值 |
|---|---|---|---|
.xpath() |
使用 XPath 查询元素 | xpath:XPath 表达式,namespaces:命名空间字典 |
结果列表 |
# -*- coding: utf-8 -*-
# 说明:演示如何处理带命名空间的 XML
# 说明:导入 lxml 的 etree 模块
from lxml import etree
# 说明:准备带命名空间的 XML
# xmlns="http://example.com/ns" 定义了默认命名空间
xml_with_ns = """<?xml version="1.0" encoding="utf-8"?>
<books xmlns="http://example.com/ns">
<book id="1">
<title>Python 入门</title>
<price>39.9</price>
</book>
<book id="2">
<title>数据科学</title>
<price>49.9</price>
</book>
</books>
"""
# 说明:解析 XML
root = etree.fromstring(xml_with_ns)
# 说明:定义命名空间字典
# 键是命名空间前缀(可以是任意名称),值是命名空间 URI
namespaces = {
"bk": "http://example.com/ns" # 定义 bk 前缀指向命名空间 URI
}
# 说明:使用命名空间查询元素
# //bk:book 表示查找命名空间为 "http://example.com/ns" 的 book 元素
# namespaces=namespaces 指定命名空间字典
books = root.xpath("//bk:book", namespaces=namespaces)
print(f"找到 {len(books)} 本书")
# 说明:提取书名
titles = root.xpath("//bk:title/text()", namespaces=namespaces)
print("所有书名:", titles)
# 说明:获取命名空间 URI
# namespace-uri() 是 XPath 函数,用于获取元素的命名空间
ns_uri = root.xpath("namespace-uri(.)")
print(f"\n根元素的命名空间:{ns_uri}")12.4 HTML 动态内容 #
问题:网页内容由 JavaScript 动态生成,lxml 无法获取。
说明:lxml 只能解析静态 HTML,无法执行 JavaScript。如果需要获取动态内容,需要配合 Selenium 或 Playwright 等工具。
# -*- coding: utf-8 -*-
# 说明:说明 HTML 动态内容的问题和解决方案
# 说明:导入 lxml 的 html 模块
from lxml import html
# 说明:示例:静态 HTML(可以直接解析)
static_html = """
<html>
<body>
<div id="content">这是静态内容</div>
</body>
</html>
"""
# 说明:解析静态 HTML
doc = html.fromstring(static_html)
# 说明:提取内容
content = doc.xpath('//div[@id="content"]/text()')
print("静态内容:", content)
# 说明:说明动态内容的问题
print("\n注意:如果网页内容由 JavaScript 动态生成,")
print("lxml 无法获取这些内容,因为 lxml 不执行 JavaScript。")
print("解决方法:使用 Selenium 或 Playwright 先渲染页面,再获取 HTML。")
# 说明:示例代码结构(需要安装 selenium)
print("\n使用 Selenium 的示例代码结构:")
print("""
from selenium import webdriver
from lxml import html
# 启动浏览器并加载页面
driver = webdriver.Chrome()
driver.get("https://example.com")
# 获取渲染后的 HTML
html_text = driver.page_source
# 使用 lxml 解析
doc = html.fromstring(html_text)
# 现在可以提取动态生成的内容了
""")