一、问题背景:一个棘手的 Bug

最近,在开发过程中遇到了一个棘手的程序报错。经过数小时的调试,最终才定位到问题根源:Python 不同版本之间,正则表达式引擎存在着微妙的行为差异。

问题的起因是下面这段用于匹配被星号(*...*)包裹内容的正则表达式:

1
2
# 原始的、引发问题的正则表达式
r'\\\*(\S(?:(?s).*?\S)?)\\\*'

二、排查与解决方案

这段表达式在旧项目中一直运行正常,但在新项目中复用时却意外地抛出了错误。起初这让我百思不得其解,因为表达式的逻辑本身看起来毫无问题。

经过反复排查和尝试,我最终找到了解决方案。首先,将正则表达式本身进行修改,移除其中的内联标志 (?s)

1
2
# 修改后的正则表达式
r'\\\*(\S(?:.*?\S)?)\\\*'

然后,在调用 re 模块的相关函数(如 re.searchre.findall)时,显式地设置 flags 参数:

1
2
3
4
5
import re

text = r"这是一个\*包含换行符\n的内容\*的例子"
# 在调用时,通过 flags 参数启用 DOTALL 模式
match = re.search(r'\\\*(\S(?:.*?\S)?)\\\*', text, flags=re.DOTALL)

完成这两步修改后,问题才得以解决。

三、根源剖析:摇摆不定的 (?s)

那么,问题的症结究竟在哪里?正是在于内联标志(inline flag)(?s) 的摆放位置。这个标志的作用等同于 re.DOTALL,即让 . 特殊字符可以匹配包括换行符在内的任意字符。

经过在不同环境下的测试,我发现该标志的合法性在 Python 的不同版本中飘忽不定:

  • Python 3.9: 在非捕获组 (?:...) 内部使用 (?s)非法 的,会直接导致 re.error
  • Python 3.10: 在同样的位置,(?s) 的使用又变得 合法,程序可以正常运行。
  • Python 3.12: (?s) 在此处再次被认定为 非法,行为又回到了 3.9 的状态。

四、总结与反思

正是这种不易察觉的版本间“行为漂移”,让我结结实实地“踩了一坑”。

我的建议是:

优先使用 flags 参数,而不是内联标志。

相比于在表达式内部使用 (?s), (?i), (?m) 等内联标志,在函数调用时明确传递 flags 参数(如 re.DOTALL, re.IGNORECASE, re.MULTILINE)是一个更安全、更具可移植性的选择。它能确保代码在不同 Python 版本下行为的一致性,有效避免类似的意外发生。

将此文分享出来,希望能让大家引以为戒,避免重蹈覆辙。