
先看一行看起来毫无问题的代码用parse解析01-1993这样的 MM-YYYY 字符串。上个月清洗一份香港公开数据的月度表时它在 9 月底跑出1993-01-28今天同一个文件、同一行代码跑出的是1993-01-01。代码没改过输出跟着日历走——dateutil 把字符串里缺失的「日」悄悄填成了运行当天的日期。这是我用了很多年才发现的坑本文把 dateutil.parser 的三场事故全部复现给你看最后给出一份什么时候能安全使用它的判断清单。顺带一个深搜发现Fedora 已经在 2026 年发起了对它的弃用提案。目录一、事故现场一行代码两种输出二、fuzzyTrue把一句话缝成一个不存在的时刻三、猜测的代价慢 61 倍还会传染 pandas四、深搜发行版已经在准备告别它五、为什么明知有坑还戒不掉六、什么时候还能用四条判断清单一、事故现场一行代码两种输出环境Python 3.13.12 / python-dateutil 2.9.0.post0 / pandas 3.0.5 / macOS。下面的行为全部实测复现你可以直接照着跑。机制说穿了很简单dateutil 的parse有个default参数字符串里没提供的字段全部从 default 里取而你不传 default 时官方文档写明它就是datetime.now()。于是01-1993这种只有月和年的字符串解析结果的「日」完全由你哪天运行决定importdatetimeasdtfromdateutilimportparserasdp pdp.parser()s01-1993# MM-YYYY 格式缺「日」fordin(1,15,28):assertp.parse(s,defaultdt.datetime(2026,10,d)).dayd# 不传 default缺省即 datetime.now()日子来自运行当天todayp.parse(s)assert(today.year,today.month)(1993,1)asserttoday.daydt.datetime.now().dayprint(日子完全来自 default今天跑 ,today.strftime(%Y-%m-%d))这段代码可以直接复用——它同时也是一面照妖镜如果你的解析结果会随运行日期变化一定有字段被填充了。我当时的数据表里有一整列MM-YYYY9 月 26 日之后没改过任何代码10 月重跑一遍整列的「日」全部变了。而这一切不报错、不告警只在 downstream 的月度对账里多出一堆幽灵行。二、fuzzyTrue把一句话缝成一个不存在的时刻第二个坑更隐蔽。处理邮件、公告标题这类「日期混在文字里」的需求时很多人会打开fuzzyTrue。单独喂一个日期它确实好用但遇到一句话里出现两个日期它会把两处的碎片缝合成一个谁也没写过的时刻fromdateutilimportparserasdp pdp.parser()# 三个最小案例逐步加料assertstr(p.parse(updated 2025-12-31,fuzzyTrue))2025-12-31 00:00:00assertstr(p.parse(effective 2026-10-08,fuzzyTrue))2026-10-08 00:00:00mergedp.parse(updated 2025-12-31, effective 2026-10-08,fuzzyTrue)assertstr(merged)2025-12-31 20:26:00-08:00print(fuzzy 缝合结果,merged)看清这个输出日期取了第一处2025-12-31时间20:26来自第二处的年份2026时区偏移-08:00是另一段残片。原文里没有任何一个时刻长这样但它不报错、类型正确、直接入库。我后来给自己立了条规矩fuzzy只允许用在「先人工看过样例、且解析结果可枚举校验」的文本上其余一律先拆句再解析。把四类输入和四种解析方式放进同一张矩阵格局就很清楚了绿色是「结果正确」黄色是「不报错但语义被改写」——黄色区永远比红色区危险红色你至少会停下来排查。三、猜测的代价慢 61 倍还会传染 pandas猜格式是个昂贵的动作。同样是解析MM-YYYY10 万行的实测耗时dateutil.parse3.978 秒、strptime0.696 秒慢 5.7 倍、pandas.to_datetime显式给出%m-%Y这个format只要 0.065 秒——dateutil 比显式 format 慢 61 倍。更值得警惕的是to_datetime不传format时走的是同一种猜测世界观直接喂01-1993同样返回1993-01-01日子照样被填成运行当天。你换成 pandas 并不会安全只会把坑换个姓氏。importdatetimeasdt,timefromdateutilimportparserasdp rows[01-1993]*20000pdp.parser()t0time.perf_counter()[p.parse(s,defaultdt.datetime(1900,1,1))forsinrows]t_dateutiltime.perf_counter()-t0 t0time.perf_counter()[dt.datetime.strptime(s,%m-%Y)forsinrows]t_strptimetime.perf_counter()-t0assertt_dateutilt_strptime# 反直觉小知识dateutil 反而解析不了 202108strptime 可以assertdt.datetime.strptime(202108,%Y%m)dt.datetime(2021,8,1)print(f2 万行dateutil{t_dateutil:.2f}s vs strptime{t_strptime:.2f}s)顺带说一句202108dateutil 直接抛ParserErrorstrptime配%Y%m反而一次解析成 2021-08-01。「什么都能猜」的库遇到干净的定长格式反而不如笨办法——这大概是这篇吐槽里最讽刺的一行。四、深搜发行版已经在准备告别它写这篇之前我去查了下这个库的现状结果比坑本身更有信息量Fedora 在 2026 年发起的官方变更提案《Deprecate python-dateutil》目标 Fedora 452026 年 6 月还在更新里写得直白——upstream 处于无人维护状态且可能存在未处理的安全问题计划逐步说服下游项目迁移而提案页直接依赖它的 Fedora 包清单列了整整一页。PyPI 侧的数据同样夸张月下载约 11.9 亿次全站第 16 名最后一次发版是 2024 年 3 月的 2.9.0.post0到本文写作已经两年半。被下载几十亿次、又被发行版点名弃用——一个库能同时活在两个极端里。五、为什么明知有坑还戒不掉因为它在 90% 的场景里真的好用。日志时间戳、用户输入、爬下来的英文页面——这些格式的确没法提前枚举parse一行顶十行。问题在于剩下 10% 的场景里它的失败方式是「给出一个错误的答案」而不是「报错」错误答案会顺着管线一路走到报表里等你发现时污染已经有了时间戳、有了类型、甚至有了下游引用。我后来把「同一段解析代码隔天重跑」加进了自己数据管线的验收步骤——输出变了就说明有字段被填充这条习惯抓到过不止一次静默改写。顺便说一句标准库这边也在进步3.11 起fromisoformat接受的 ISO 变体多了不少但它依然拒绝猜测——这条边界画得很对。六、什么时候还能用四条判断清单收藏这份清单遇到日期字段直接对照格式已知→datetime.strptime或pd.to_datetime(format...)这是唯一不猜的路线也最快标准 ISO 字符串→date.fromisoformat它对01-1993这种输入直接抛ValueError——拒绝猜测本身是优点确实要兜底猜→ 必须defaultdatetime(1900,1,1)把「日」钉死让污染至少变成确定性的fuzzy默认禁用验收动作→ 同一段代码隔天重跑一次输出变了就说明有字段被填充。边界也说清楚本文只吐槽parser的猜测行为。dateutil 里的relativedelta、rrule、时区工具至今仍是标准库之外难替代的部分我自己的项目也在继续用——吐槽一个库不等于把整个工具箱扔掉。这段判断清单和复现代码可以直接搬走收藏备用如果这篇帮你省下一次凌晨排查收藏点赞。你有没有被某个「太聪明」的解析器坑过——日期、编码还是数字格式评论区聊聊你的事故现场。这类开源库的实测复盘会持续更新关注不迷路。参考链接https://dateutil.readthedocs.io/en/stable/parser.htmlhttps://fedoraproject.org/wiki/Changes/DeprecatePython-dateutilhttps://pypi.org/project/python-dateutil/