Python实用工具:pyparsing 从入门到实战,轻松实现自定义语法解析

一、pyparsing 库概述

pyparsing 是一款纯 Python 编写的递归下降解析库,无需编写复杂正则表达式,即可实现文本、自定义语法、配置文件、日志等内容的结构化解析。其核心原理是通过组合基础解析元素构建语法规则,自上而下完成文本解析。该库轻量无依赖、上手简单,适合非专业解析器开发者快速实现解析需求;缺点是解析大型复杂语法时性能弱于专用解析器。采用 MIT License,可自由商用与修改。

二、pyparsing 基础安装与快速入门

2.1 安装 pyparsing

pyparsing 不依赖第三方库,直接通过 pip 即可完成安装,命令如下:

pip install pyparsing

安装完成后,在 Python 脚本中导入即可使用:

import pyparsing as pp

2.2 基础解析元素介绍

pyparsing 提供了大量基础解析单元,通过组合这些单元就能实现复杂解析逻辑,常用基础元素:

  • pp.Word:匹配连续字符(字母、数字、自定义字符集)
  • pp.Suppress:匹配内容但不加入解析结果,用于过滤符号
  • pp.OneOrMore:匹配一次或多次指定规则
  • pp.ZeroOrMore:匹配零次或多次指定规则
  • pp.Optional:匹配可选内容
  • pp.Group:将解析结果分组,提升结构可读性

2.3 最简解析示例

以最简单的「匹配数字」为例,快速体验 pyparsing 解析流程:

import pyparsing as pp

# 定义解析规则:匹配连续数字
number = pp.Word(pp.nums)

# 待解析文本
text = "20250630"

# 执行解析
result = number.parseString(text)

# 输出解析结果
print("解析结果:", result)
print("结果类型:", type(result))
print("结果取值:", result[0])

代码说明

  1. 使用 pp.nums 定义数字字符集,pp.Word 匹配连续数字
  2. parseString 是核心解析方法,传入待解析文本即可执行
  3. 解析结果为 ParseResults 类型,可像列表一样取值

运行结果:

解析结果: ['20250630']
结果类型: <class 'pyparsing.ParseResults'>
结果取值: 20250630

三、pyparsing 核心语法与进阶用法

3.1 处理带符号的文本(过滤无用字符)

实际解析中常出现括号、逗号、空格等符号,使用 pp.Suppress 可自动过滤:

import pyparsing as pp

# 定义规则:解析 (123,456) 格式,过滤括号和逗号
left = pp.Suppress("(")
right = pp.Suppress(")")
comma = pp.Suppress(",")
number = pp.Word(pp.nums)

# 组合规则:左括号 + 数字 + 逗号 + 数字 + 右括号
coord = left + number + comma + number + right

# 解析坐标文本
text = "(100,200)"
result = coord.parseString(text)

print("解析后的坐标:", result)
print("X坐标:", result[0])
print("Y坐标:", result[1])

代码说明
Suppress 仅匹配不保留,解析结果自动剔除符号,直接获取有效数据。

运行结果:

解析后的坐标: ['100', '200']
X坐标: 100
Y坐标: 200

3.2 解析带名称的键值对

解析配置文件常用的「名称=值」格式,可通过命名结果直接按键取值:

import pyparsing as pp

# 定义规则:变量名 = 数值
var_name = pp.Word(pp.alphas)  # 字母开头的变量名
equal = pp.Suppress("=")
value = pp.Word(pp.nums + pp.alphas + ".")  # 支持字母、数字、小数点

# 组合规则并命名
expr = var_name("key") + equal + value("value")

# 解析配置行
text = "version=3.10.4"
result = expr.parseString(text)

print("完整结果:", result)
print("键名:", result.key)
print("值:", result.value)

代码说明
在解析元素后加 ("名称") 可命名结果,直接通过属性名取值,比索引更直观。

运行结果:

完整结果: ['version', '3.10.4']
键名: version
值: 3.10.4

3.3 分组解析复杂结构

当解析多层结构时,使用 pp.Group 对结果分组,避免结果扁平化:

import pyparsing as pp

# 基础规则
number = pp.Word(pp.nums)
colon = pp.Suppress(":")
comma = pp.Suppress(",")

# 定义学生信息:姓名:成绩,成绩,成绩
score = number
name = pp.Word(pp.alphas)
student = pp.Group(name + colon + pp.OneOrMore(score + pp.Optional(comma)))

# 解析多个学生信息
text = "Tom:90,85,95 Jerry:88,92,90"
result = pp.OneOrMore(student).parseString(text)

print("完整解析结果:", result)
print("第一个学生:", result[0])
print("第一个学生姓名:", result[0][0])
print("第一个学生成绩:", result[0][1:])

代码说明
Group 会将内部解析结果打包为子列表,多层结构清晰可辨。

运行结果:

完整解析结果: [['Tom', '90', '85', '95'], ['Jerry', '88', '92', '90']]
第一个学生: ['Tom', '90', '85', '95']
第一个学生姓名: Tom
第一个学生成绩: ['90', '85', '95']

3.4 自定义解析动作(转换数据类型)

pyparsing 默认解析结果为字符串,可通过 setParseAction 自定义处理函数,自动转换类型:

import pyparsing as pp

# 定义转换函数:将字符串转为整数
def to_int(tokens):
    return int(tokens[0])

# 定义数字规则,并绑定转换动作
number = pp.Word(pp.nums).setParseAction(to_int)

# 解析并查看类型
text = "12345"
result = number.parseString(text)

print("解析结果:", result)
print("结果类型:", type(result[0]))

代码说明
setParseAction 接收函数,解析完成后自动执行函数处理结果,实现类型转换、校验、格式化等功能。

运行结果:

解析结果: [12345]
结果类型: <class 'int'>

四、pyparsing 实战案例:简易日志解析器

4.1 案例需求

解析程序日志,提取时间、日志级别、模块、信息,输出结构化字典。
日志格式:

[2025-06-30 10:00:00] [INFO] [Database] 连接成功
[2025-06-30 10:05:00] [ERROR] [Network] 请求超时

4.2 解析代码实现

import pyparsing as pp

# 过滤括号
lbracket = pp.Suppress("[")
rbracket = pp.Suppress("]")

# 定义各部分规则
time_str = pp.Word(pp.nums + "-: ")  # 时间:2025-06-30 10:00:00
level = pp.Word(pp.alphas)          # 日志级别:INFO/ERROR
module = pp.Word(pp.alphas)         # 模块名
message = pp.restOfLine()           # 剩余所有内容作为日志信息

# 组合完整规则并命名
log_expr = (
    lbracket + time_str("time") + rbracket
    + lbracket + level("level") + rbracket
    + lbracket + module("module") + rbracket
    + message("message")
)

# 测试日志
log1 = "[2025-06-30 10:00:00] [INFO] [Database] 连接成功"
log2 = "[2025-06-30 10:05:00] [ERROR] [Network] 请求超时"

# 解析并转为字典
def parse_log(log_text):
    res = log_expr.parseString(log_text)
    return {
        "time": res.time,
        "level": res.level,
        "module": res.module,
        "message": res.message.strip()
    }

# 输出结果
print(parse_log(log1))
print(parse_log(log2))

代码说明

  1. restOfLine() 匹配行剩余所有内容,适合解析末尾不定长信息
  2. 封装解析函数,直接返回结构化字典,方便后续数据处理
  3. 命名规则让代码可读性极高,无需记忆索引位置

运行结果:

{'time': '2025-06-30 10:00:00', 'level': 'INFO', 'module': 'Database', 'message': '连接成功'}
{'time': '2025-06-30 10:05:00', 'level': 'ERROR', 'module': 'Network', 'message': '请求超时'}

五、pyparsing 实战案例:自定义数学表达式解析器

5.1 案例需求

实现支持加减乘除、括号、多位数的简易表达式解析,并计算结果。
支持格式:

1+2*3
(10+5)*2
8/4+6

5.2 解析与计算代码

import pyparsing as pp

# 数字转换为整数
number = pp.Word(pp.nums).setParseAction(lambda t: int(t[0]))

# 定义运算符
plus = pp.Literal("+")
minus = pp.Literal("-")
mult = pp.Literal("*")
div = pp.Literal("/")

# 定义括号
lpar = pp.Suppress("(")
rpar = pp.Suppress(")")

# 定义表达式优先级:括号 > 乘除 > 加减
expr = pp.infixNotation(
    number,
    [
        (pp.oneOf("* /"), 2, pp.opAssoc.LEFT),  # 乘除,左结合
        (pp.oneOf("+ -"), 2, pp.opAssoc.LEFT),  # 加减,左结合
    ]
)

# 计算函数
def calculate(expression):
    return eval(str(expr.parseString(expression)[0]))

# 测试
expr1 = "1+2*3"
expr2 = "(10+5)*2"
expr3 = "8/4+6"

print(f"{expr1} = {calculate(expr1)}")
print(f"{expr2} = {calculate(expr2)}")
print(f"{expr3} = {calculate(expr3)}")

代码说明

  1. infixNotation 专门处理中缀表达式,自动处理优先级与结合性
  2. 无需手动编写复杂递归逻辑,pyparsing 自动完成语法树构建
  3. 结合 eval 实现计算,也可自定义函数实现安全计算

运行结果:

1+2*3 = 7
(10+5)*2 = 30
8/4+6 = 8.0

六、pyparsing 实战案例:配置文件解析器

6.1 案例需求

解析类 INI 简易配置文件,提取节、键值对,生成嵌套字典。
配置文件内容:

[User]
name=test
age=20
[Database]
host=127.0.0.1
port=3306

6.2 解析代码

import pyparsing as pp

# 规则定义
lbracket = pp.Suppress("[")
rbracket = pp.Suppress("]")
equal = pp.Suppress("=")

# 节名:[User]
section = pp.Group(lbracket + pp.Word(pp.alphas)("section") + rbracket)

# 键值对:key=value
key = pp.Word(pp.alphas)
value = pp.restOfLine()
kv = pp.Group(key + equal + value)

# 配置文件:节 + 多个键值对
config = pp.Dict(pp.OneOrMore(section | kv))

# 测试配置文本
conf_text = """
[User]
name=test
age=20
[Database]
host=127.0.0.1
port=3306
"""

# 解析
result = config.parseString(conf_text)

# 输出嵌套字典
print("User 配置:", result.User)
print("Database 配置:", result.Database)
print("用户名:", result.User.name)
print("数据库端口:", result.Database.port)

代码说明

  1. Dict 可将解析结果自动转为字典结构,支持链式取值
  2. 支持多节配置,结构清晰,适合解析自定义配置文件

运行结果:

User 配置: {'name': 'test', 'age': '20'}
Database 配置: {'host': '127.0.0.1', 'port': '3306'}
用户名: test
数据库端口: 3306

七、相关资源

  • Pypi地址:https://pypi.org/project/pyparsing/
  • Github地址:https://github.com/pyparsing/pyparsing
  • 官方文档地址:https://pyparsing-docs.readthedocs.io/

关注我,每天分享一个实用的Python自动化工具。