正则描述「长什么样」,不描述「是什么」。
\d\d\d 说的是「三个数字」,不管那三个数字代表年份还是价格。
四个函数覆盖九成用法:search 全文找第一个、match 只认开头、findall 找全部、
sub 替换。
写正则一律加 r 前缀。r'\d+' 和 '\\d+' 是同一个字符串,但前者一眼看出是正则。
上一篇抓图片时用了三条正则,那只是「能用」。这一篇把它讲清楚。 教材第 11 章中间一节是【规则表达式】,篇幅不小——它是这一章唯一的理论部分。 正则的学习曲线很陡: 看别人的能读懂,自己写就卡住。 破解办法只有一个——把那张元字符表背下来, 然后一个字符一个字符对着念。 本篇最后一条结论值得先记住:正则不是解析 HTML 的正确工具。 能凑合的地方凑合, 复杂页面还是用第 6 章那套建树的办法。
一、四个函数,覆盖九成用法
| 函数 | 干什么 | 给什么 |
|---|---|---|
search |
全文找第一个 | 匹配对象或 None |
match |
只从开头试 | 匹配对象或 None |
findall |
找全部 | 字符串列表 |
finditer |
找全部 | 匹配对象序列 |
sub |
替换 | 新字符串 |
演示第一段那句原文是「订单 PO-20261102-001 金额 328.50 元,订单 PO-20261102-002…」。
search('PO-\d+-\d+') 拿到第一个单号;findall 拿到两个;sub 把金额换成了 ###.##。
match 只从第 0 个字符开始试。 所以 match('订单', 原文) 能中,
match('PO', 原文) 中不了——虽然原文里确实有 PO。
这条差别坑过很多人。
要全文找一律用 search,
match 只在「我确定要从头匹配」时用(比如校验一个输入框里整串是不是手机号)。
findall 和 finditer 的区别:前者直接给你字符串列表,后者给你匹配对象。
要分组信息就用 finditer,只要文本就用 findall。
二、元字符表:正则的全部基础
| 写法 | 意思 | 例子 |
|---|---|---|
\d |
一个数字 | \d\d\d 匹配 001 |
\w |
一个字母数字下划线 | \w+ 匹配 abc_1 |
\s |
一个空白 | a\sb 匹配 a b |
[abc] |
这三个里任意一个 | [abc]+ 匹配 cab |
[^abc] |
不是这三个的任意一个 | [^\d]+ 匹配字母 |
* |
前面那个重复 0 次或多次 | \d* 连空的都算 |
+ |
前面那个重复 1 次或多次 | \d+ 至少要有一位 |
? |
前面那个出现 0 或 1 次 | colou?r 匹配 color |
{2,4} |
重复 2 到 4 次 | \d{2,4} 匹配 2026 |
. |
任意一个字符(默认不含换行) | a.c 匹配 abc |
这一张表就是正则的全部基础,剩下的都是把它们拼起来。
看不懂别人的正则时,一个字符一个字符对这张表念,基本都能读懂。
比如 \d{4}-\d{2}-\d{2} 念出来就是「四位数字、横杠、两位数字、横杠、两位数字」,那就是日期。
反过来写的时候也一样:先在心里把「我要的东西长什么样」念一遍,再一段一段对着表写。
别一上来就写整条。
那个「默认不含换行」要记住。. 匹配不了换行符,要跨行得加 re.S 标志,或者写 [\s\S]。
这一点在抓多行 HTML 时最容易忘。
三、分组:把要的那一截单独留出来
圆括号的意思是「我要这一截」。
PO-(\d{8})-(\d{3}) 能匹配 PO-20261102-001,同时把日期和序号单独留出来。
取值用 group(0)(整段匹配)、group(1)、group(2)。注意编号从 1 开始,0 是全部。
分组一多,数编号就容易错。
用命名分组 (?P<名字>...) 更好,取值写 group('date'),一眼知道拿的是什么。
groupdict() 直接给你一个字典,写报表、填模板的时候很省事。
还有一个不占编号的分组写法 (?:...)——只用来分组,不要这一截。
它的作用是让你在需要括号但不需要捕获的地方(比如 (?:ab)+)不出多余的编号。
四、贪婪和非贪婪
原文是 <a href="/a">1</a><a href="/b">2</a>。
<a href="(.*)"> 抓出来是 ['/a">1</a><a href="/b']——一行的开头配到了最后一行的位置。
<a href="(.*?)"> 抓出来才是 ['/a', '/b']。
.* 会尽量往后吃,吃到最后一个还能匹配的位置为止;.*? 则是「能少就少」。
抓 HTML 时基本都要加问号,否则整段都被吞掉。
这也是「正则不该用来解析 HTML」的直接证据——它在跟一个嵌套结构较劲,
而正则根本不知道「嵌套」是什么。
结构简单的页面可以凑合,复杂页面还是老老实实用第 6 章那套 html.parser 建树。
树知道层级,正则只知道字符。
五、一个真用得上的组合
演示第五段解析一行表格文本:2026-11-02 | 六角螺栓 M8 | 120 | 1.50。
正则写成 (\d{4}-\d{2}-\d{2})\s*\|\s*(.+?)\s*\|\s*(\d+)\s*\|\s*([\d.]+)。
里面那个 \s*\|\s* 的意思是「竖线两边不管有没有空格」。
这种松散写法比 split('|') 稳——split 出来会带着前后空格,还得再 strip 一遍。
但也要说实话:格式固定、字段不多的时候,split 更简单也更快。
正则的价值在「格式有点乱但模式明确」的地方:日志行、各种不统一的报表、别人手工填的表格。
别为了炫技把简单事写复杂。
六、三个最常见的坑
| 坑 | 错误写法 | 原文 | 结果 |
|---|---|---|---|
| 忘了转义点 | 1.5 |
135 |
能匹配(点当成了任意字符) |
| 忘了非贪婪 | <b>.*</b> |
<b>a</b><b>b</b> |
['<b>a</b><b>b</b>'] |
| 忘了原始字符串 | '\\d+' |
abc123 |
能匹配,但要数反斜杠 |
第一条尤其常见。 想匹配字面上的点,要写 \.。
不加反斜杠,那个点表示「任意字符」,于是 1.5 会把 135 也匹配上。
第二条是抓 HTML 必踩的,上一节讲过。
第三条不是「错」,是「不好读」。
r'\d+' 和 '\\d+' 在 Python 里是同一个字符串,但前者一眼就能看出「这是正则」,后者要数反斜杠。
所以规矩是:永远写 r。 没有例外。正则里反斜杠太多了,不写 r 迟早出错。
七、动手 Demo:正则:描述「长什么样」
先用一句原文把五个函数各跑一遍,看清 search 和 match 的差别。 然后试分组、命名分组、贪婪与非贪婪。 最后跑三个常见的坑,看它们各自错在哪。
import re
TEXT = '订单 PO-20261102-001 金额 328.50 元,订单 PO-20261102-002 金额 1780.00 元'
print('① 四个函数各干什么')
print(' 原文 ' + TEXT)
print(" search → %r" % re.search(r'PO-\d+-\d+', TEXT).group(0))
print(" match → %r(只从开头试)" % re.match(r'订单', TEXT).group(0))
print(" match PO → %r" % re.match(r'PO', TEXT))
print(" findall → %r" % re.findall(r'PO-\d+-\d+', TEXT))
print(' finditer 个数 %d' % len(list(re.finditer(r'PO-\d+-\d+', TEXT))))
print(" sub → %s" % re.sub(r'\d+\.\d+', '###.##', TEXT))
print('② 方括号、星号、问号、反斜杠')
print('| 写法 | 意思是 | 例子 |')
print('|---|---|---|')
for r_, d_, e_ in (('\\d', '一个数字', '\\d\\d\\d 匹配 001'),
('\\w', '一个字母数字下划线', '\\w+ 匹配 abc_1'),
('\\s', '一个空白', 'a\\sb 匹配 a b'),
('[abc]', '这三个里任意一个', '[abc]+ 匹配 cab'),
('[^abc]', '不是这三个的任意一个', '[^\\d]+ 匹配字母'),
('*', '前面那个重复 0 次或多次', '\\d* 连空的都算'),
('+', '前面那个重复 1 次或多次', '\\d+ 至少要有一位'),
('?', '前面那个出现 0 或 1 次', 'colou?r 匹配 color'),
('{2,4}', '重复 2 到 4 次', '\\d{2,4} 匹配 2026'),
('.', '任意一个字符(默认不含换行)', 'a.c 匹配 abc')):
print('| `%s` | %s | `%s` |' % (r_, d_, e_))
print('③ 分组:把要的那截单独留出来')
m = re.search(r'(?P<prefix>[A-Z]+)-(?P<date>\d{8})-(?P<seq>\d{3})', TEXT)
print(' 整体 %s' % m.group(0))
print(' 第1组 %s 第2组 %s 第3组 %s' % (m.group(1), m.group(2), m.group(3)))
print(' 命名分组 %s' % {k: v for k, v in m.groupdict().items() if k != 'seq'})
print('④ 贪婪和非贪婪')
H = '<a href="/a">1</a><a href="/b">2</a>'
print(' 原文 %s' % H)
print(' 贪婪 <a href="(.*)"> → %r' % re.findall(r'<a href="(.*)">', H))
print(' 非贪婪 <a href="(.*?)"> → %r' % re.findall(r'<a href="(.*?)">', H))
print(' .* 会尽量往后吃,吃到最后一个能匹配的位置为止;.*? 则是「能少就少」。')
print('⑤ 一个真用得上的组合')
LINE = '2026-11-02 | 六角螺栓 M8 | 120 | 1.50'
m = re.search(r'(\d{4}-\d{2}-\d{2})\s*\|\s*(.+?)\s*\|\s*(\d+)\s*\|\s*([\d.]+)', LINE)
print(' 原文 %s' % LINE)
print(' 解析出:日期 %s,品名 %s,数量 %s,单价 %s' % m.groups())
print(' \\s*\\|\\s* 的意思是「竖线两边不管有没有空格」。')
print('⑥ 三个最常见的坑')
print(' 忘了转义点 %r %r → %r' % ('1.5', '135', re.findall(r'1.5', '135')))
print(' 忘了非贪婪 %r %r → %r'
% ('<b>.*</b>', '<b>a</b><b>b</b>', re.findall(r'<b>.*</b>', '<b>a</b><b>b</b>')))
print(' 忘了加原始字符串 %r %r → %r'
% (r'\d+', 'abc123', re.findall('\\d+', 'abc123')))
print(' 第一条尤其常见:想匹配字面上的点,要写 \\.;第二条是抓 HTML 必踩的;')
print(' 第三条是写正则一律加 r 前缀的原因。')
实跑输出:
--------------------------------------------------------------------
正则:描述「长什么样」
--------------------------------------------------------------------
① 四个函数各干什么
--------------------------------------------------------------------
原文 订单 PO-20261102-001 金额 328.50 元,订单 PO-20261102-002 金额 1780.00 元
search → 'PO-20261102-001'
match → '订单'(只从开头试)
findall → ['PO-20261102-001', 'PO-20261102-002']
finditer 个数 2
sub → 金额 ###.##
search 全文找第一个,match 只认开头,findall 找全部,sub 替换。这四个覆盖了九成用法。
match 只从第 0 个字符开始试,所以 re.match('订单', ...) 能中,
re.match('PO', ...) 就中不了——要全文找一律用 search。
② 方括号、星号、问号、反斜杠
--------------------------------------------------------------------
| 写法 | 意思是 | 例子 |
|---|---|---|
| \d | 一个数字 | \d\d\d 匹配 001 |
| \w | 一个字母数字下划线 | \w+ 匹配 abc_1 |
| \s | 一个空白 | a\sb 匹配 a b |
| [abc] | 这三个里任意一个 | [abc]+ 匹配 cab |
| [^abc] | 不是这三个的任意一个 | [^\d]+ 匹配字母 |
| * | 前面那个重复 0 次或多次 | \d* 连空的都算 |
| + | 前面那个重复 1 次或多次 | \d+ 至少要有一位 |
| ? | 前面那个出现 0 或 1 次 | colou?r 匹配 color |
| {2,4} | 重复 2 到 4 次 | \d{2,4} 匹配 2026 |
| . | 任意一个字符(默认不含换行) | a.c 匹配 abc |
这一张表就是正则的全部基础。剩下的都是把它们拼起来。
看不懂别人的正则时,一个字符一个字符对这张表念,基本都能读懂。
③ 分组:把要的那截单独留出来
--------------------------------------------------------------------
整体 PO-20261102-001
第1组 PO 第2组 20261102 第3组 001
命名分组 {'prefix': 'PO', 'date': '20261102'}
圆括号就是「我要这一截」。group(0) 是整段匹配,group(1) 起是括号里的内容。
分组一多,用 ?P<名字> 起名比数编号靠谱。groupdict() 直接给你一个字典,写报表的时候很省事。
④ 贪婪和非贪婪
--------------------------------------------------------------------
原文 <a href="/a">1</a><a href="/b">2</a>
贪婪 <a href="(.*)"> → ['/a">1</a><a href="/b']
非贪婪 <a href="(.*?)"> → ['/a', '/b']
.* 会尽量往后吃,吃到最后一个能匹配的位置为止;.*? 则是「能少就少」。
抓 HTML 时基本都要加问号,否则一行的开头会配上最后一行。
这也说明正则不是解析 HTML 的正确工具。
结构简单的可以凑合,复杂页面还是老老实实用第 6 章那套 html.parser 建树。
⑤ 一个真用得上的组合
--------------------------------------------------------------------
解析出:日期 2026-11-02,品名 六角螺栓 M8,数量 120,单价 1.50
\s*\|\s* 的意思是「竖线两边不管有没有空格」。
这种松散写法比 split('|') 稳——split 出来会带着前后空格,还得再 strip 一遍。
但也要说实话:格式固定、字段不多的时候,split 更简单也更快。
正则的价值在「格式有点乱但模式明确」的地方,别为了炫技把简单事写复杂。
⑥ 三个最常见的坑
--------------------------------------------------------------------
忘了转义点 '1.5' '135' → ['135']
忘了非贪婪 '<b>.*</b>' '<b>a</b><b>b</b>' → ['<b>a</b><b>b</b>']
忘了加原始字符串 '\\d+' 'abc123' → ['123']
第一条尤其常见:想匹配字面上的点,要写 \.;第二条是抓 HTML 必踩的;
第三条是写正则一律加 r 前缀的原因。
r'\d+' 和 '\\d+' 在 Python 里是同一个字符串,但前者一眼就能看出「这是正则」,后者要数反斜杠。
永远写 r。
第一段那个 match PO 打印出来是 None,这就是「只从开头试」的意思。 原文里确实有 PO,但它不在第 0 个位置。 第二段那张表是整篇的基础。看不懂正则的时候,一个字符一个字符对这张表念,基本都能读懂。 第三段那个 (?P<名字>...) 是命名分组的写法,取值写 group('date') 比 group(2) 清楚得多。 第四段那个贪婪结果最能说明问题:它把一行的开头配到了最后一行的位置,中间的两段全被吞进第一组去了。 第六段第一条最阴:想匹配字面上的点得写反斜杠加点,不写的话那个点表示「任意字符」, 于是 1.5 会把 135 也匹配上。
30 秒实验
- 把演示第一段的
re.match(r'订单', TEXT)改成re.search(r'订单', TEXT),看结果一样不一样。 - 把演示第四段的
(.*?)改成([^\"]+),看结果跟非贪婪是不是一样,再想想哪种写法更不容易出错。 - 写一条正则匹配
2026-11-02这种日期,要求月份只能是 01 到 12 之间,提示: 月份那两位写成(0[1-9]|1[0-2])。
常见误区
- 「
match和search差不多。」match只从第 0 个字符试。 原文里有你要的东西,但不在开头,match就给None。 全文找一律用search。 - 「
.匹配任何字符。」默认不匹配换行。要跨行得加re.S,或者写[\s\S]。 - 「正则能解析 HTML。」能凑合简单的,复杂页面必翻车。 正则不知道「嵌套」是什么,一遇到嵌套结构就会贪到底。复杂页面用建树的方式。
自测三问(点开对答案)
1. `re.match(r'PO', 原文)` 返回 `None`,但原文里明明有 `PO`,为什么? —— 因为 `match` 只从第 0 个字符开始尝试。 原文的开头是「订单」两个字,不是 `PO`,所以直接不匹配。要全文找得用 `search`。 2. `` 为什么会把整段 HTML 吞掉?怎么改? —— 因为 `.*` 是贪婪的,它会尽量往后吃, 一直吃到最后一个还能匹配的位置——也就是最后一个 `">` 之前。 改成 `(.*?)` 让它非贪婪,「能少就少」,就会在第一个 `">` 前停下。 3. 为什么写正则一律要加 `r` 前缀? —— 因为正则里反斜杠特别多。 不加 r 的话,`'\d+'` 里的 `\d` 要先经过 Python 字符串转义, 得写成 `'\\d+'` 才是你要的那个意思。 加了 r,反斜杠原样传给正则引擎,**而且一眼能看出这是个正则。**下一篇让程序一心多用:多线程下载,还有定时任务怎么算下一次。
- ☐ (本篇完,下一篇 11.3 多线程与定时运行)