问号的两种身份
问号的第一重身份是量词:前面的元素出现零次或一次,也就是可选。第二重身份是修饰符:紧跟在其他量词后面,把它从贪婪改成非贪婪。同一个符号在两种位置上的含义完全不同,判断依据是它前面有没有别的量词。
与分组的纠葛
问号还参与构成特殊分组,例如圆括号里以问号开头表示非捕获、断言或命名分组。所以看到括号内问号时,它读作语法前缀而不是量词,这也是模式里问号最难一眼读懂的原因。
基本信息
| 量词含义 | 前一元素出现 0 到 1 次 |
|---|---|
| 修饰含义 | 紧跟量词之后表示非贪婪版本,如星号问号、加号问号 |
| 分组前缀 | 圆括号内以问号开头时表示特殊分组构造 |
| 等价写法 | 可选字符用花括号写 0 到 1 次完全等价 |
| 默认风格 | 作为量词时依然贪婪,会优先尝试吃掉一个字符 |
典型使用场景
- 匹配可选的协议头,例如网址中可能出现的安全传输前缀
- 处理英式与美式拼写差异,把可选的字母用问号包起来
- 匹配可能带正负号的数值,符号部分设为可选
- 把贪婪量词改成非贪婪,限定提取范围
- 匹配带可选尾缀的标识,例如可能出现的单位或后缀
可选与非贪婪的写法
可选与非贪婪的写法
import re
re.findall(r'colou?r', 'color colour') # 可选字母:两种拼写都命中
re.findall(r'-?\d+', 'a-12 b34') # 可选负号,匹配带符号数字
re.search(r'https?://', 'http://x') # 协议头中可选的 s
re.search(r'<i>(.*?)</i>', '<i>a</i>') # 问号在这里是把星号变非贪婪
re.search(r'\d{0,1}', '7') # 与问号完全等价的区间写法
re.search(r'(?:ab)?', 'x ab') # 非捕获分组整体可选问号位置的含义
| 出现位置 | 读作 | 示例效果 |
|---|---|---|
| 字符或类之后 | 量词,出现零或一次 | 可选字母、可选符号 |
| 另一个量词之后 | 非贪婪修饰 | 改最短匹配策略 |
| 左括号之后 | 特殊分组前缀 | 非捕获、断言或命名分组 |
| 字符类内部 | 普通字符 | 不再有量词含义 |
非贪婪不是「尽量少」的绝对承诺:它只是改变尝试顺序,如果最短方案会导致后面匹配失败,仍然会退回取更长的内容。要严格控制长度,还得靠明确的区间量词或断言。
可选修饰符可以叠加在花括号量词上,例如限定 1 到 3 次再改成非贪婪。写这种组合时建议在模式里保留注释或改写成自由间距模式,避免后续维护者误读。