平时在做数据清洗或者处理复杂的业务日志时,提取非标准格式的文本绝对是个让人头疼的体力活。
很多朋友的第一反应是手写正则表达式。对于简单的字符串匹配,正则确实好用;但一旦遇到嵌套结构、或者是包含多种状态的复杂文本,正则表达式就会迅速膨胀成一段只有上帝和写代码那天你自己才能看懂的“天书”。后期需求稍微变一点,改正则简直像在拆炸弹。
如果你也正被复杂的字符串解析折磨,今天推荐的这个 Python 库——parsy,绝对能让你重拾写代码的乐趣。
什么是 Parsy?
Parsy 是一个用 Python 实现的“解析器组合子”(Parser Combinator)库。听起来名字很学术,但它的核心理念极其直观:不要试图用一个庞大复杂的规则去搞定整个文本,而是写一堆极其简单的微型解析器,然后像拼乐高积木一样把它们组合起来。
我们可以用一个简单的数据流转图来看看它的核心工作流:
[基础解析器 A: 提取字母] [基础解析器 B: 提取数字] [基础解析器 C: 忽略空格] │ │ │ └───────────────┬───────────┴───────────────┬───────────┘ │ │ ▼ ▼ (使用 >> 或 << 算子进行链式组合) │ ▼ [复合解析器: 提取特定的键值对] │ ▼ (使用 @generate 装饰器组织高级逻辑) │ ▼ [最终输出: 结构化的 Python 字典或列表]
下面我们直接上代码,看看它在实战中是如何降维打击传统写法的。
实战一:告别正则的晦涩,用自然语义解析文本
假设我们需要解析一种常见的坐标数据格式,比如 Point(125, 45)。如果用正则,你需要去捕获括号、处理逗号前后的空格。而在 parsy 里,一切都是那么顺理成章。
from parsy import string, regex, seq# 1. 定义最基础的元素解析器# regex 负责匹配模式,map(int) 直接在解析时就把字符串转成了整数number = regex(r'-?\d+').map(int)# 忽略逗号及其前后的空格comma = regex(r'\s*,\s*')# 2. 像搭积木一样组合它们# seq 会按顺序执行解析器,并把结果打包成列表返回# << 和 >> 算子用来指明保留哪一边的数据(尖角指向要保留的那边)point_parser = ( string("Point(") >> seq( number, # 提取第一个数字 (x) comma >> number # 遇到逗号后,跳过逗号,提取第二个数字 (y) ) << string(")"))# 测试解析raw_text = "Point( 125 , 45)"result = point_parser.parse(raw_text)print(f"解析结果: {result}")print(f"X坐标: {result[0]}, Y坐标: {result[1]}")
运行这段代码,它会非常稳定地输出 [125, 45],并且直接就是整数类型。代码读起来就跟自然语言一样,毫无心智负担。
实战二:灵魂特性 @generate,把复杂逻辑变平铺
如果说基础算子只是开胃菜,那么 @generate 装饰器才是 parsy 真正的灵魂。它允许你用 Python 原生的 yield 关键字来按顺序定义解析步骤。
假设我们要处理一段复杂的仪器设备日志,格式如下:
[2026-08-07] ERROR - Sensor_01: Temperature overload at 85.5C
我们需要把它拆解成结构化的字典。用 @generate 写出来,代码逻辑清晰得令人发指:
from parsy import generate, string, regex@generatedef log_parser(): # 提取日期:忽略左括号,捕获日期,忽略右括号和空格 yield string("[") date = yield regex(r'\d{4}-\d{2}-\d{2}') yield string("] ") # 提取日志等级 level = yield regex(r'[A-Z]+') yield string(" - ") # 提取设备名称 device = yield regex(r'\w+') yield string(": ") # 提取具体的报警信息和温度数值 message = yield regex(r'[a-zA-Z ]+') yield string(" at ") temp = yield regex(r'\d+\.\d+').map(float) yield string("C") # 最后,直接组装成你想要的 Python 数据结构并返回 return { "date": date, "level": level, "device": device, "alert_message": message.strip(), "temperature": temp }# 喂入脏数据进行测试log_line = "[2026-08-07] ERROR - Sensor_01: Temperature overload at 85.5C"parsed_data = log_parser.parse(log_line)print("日志解析成功,结构化数据如下:")for key, value in parsed_data.items(): print(f"{key}: {value} (类型: {type(value).__name__})")
实战三:处理重复与嵌套数据
在数据清洗时,我们经常会遇到一条记录里包含不定数量的标签。比如 Tags: raw, clean, validated。在 parsy 中,处理这种动态长度的列表极为优雅,自带的 .sep_by() 方法简直是这类数据的克星。
from parsy import string, regex, generate# 定义单个标签的解析规则(纯字母)tag = regex(r'[a-zA-Z]+')# 定义分隔符(逗号加任意空格)separator = regex(r',\s*')# 只要一行代码,就能解析以逗号分隔的不定长标签列表tags_list = tag.sep_by(separator)@generatedef metadata_parser(): yield string("Tags: ") # 直接 yield 那个列表解析器 tags = yield tags_list return tagstext = "Tags: raw, clean, validated, export"print(f"提取出的标签列表: {metadata_parser.parse(text)}")
写在最后
其实在很多数据处理的场景下,我们并不需要动用专门的语法解析工具(比如 Antlr 或者 Lex/Yacc),但用纯正则表达式又容易把自己绕晕。
Parsy 恰好填补了这中间的空白。它让你不仅能够从容应对复杂的字符串拆解,还能在几个月后重新阅读代码时,依然能一眼看懂当初自己写了什么逻辑。下次遇到棘手的文本解析任务,不妨试试这种“搭积木”的快乐。
编辑:余文彬
审校:余雨馨