一、正则是什么:用「模式」找文字
第 10 篇《字符串》里学过 'abc' in text、text.find('abc') 这类找法。它们只能找一模一样的内容。可真实需求经常是「找一段长得像的东西」:
| 需求 | 不用正则怎么写 | 用正则 |
|---|---|---|
| 找出一段文字里所有的数字 | 循环 + 判断每个字符是不是 0-9 | \d+ |
| 判断这串是不是手机号 | 写一长串 if 判断长度、首位、每一位 | 1[3-9]\d{9} |
把 2026/10/04 改成 2026-10-04 | 切分、拼接、判断格式 | 一条替换搞定 |
| 找出所有邮箱 | 几乎写不出来 | [\w.]+@[\w.]+\.[a-z]{2,} |
正则表达式(regular expression)就是用来描述「一段文字长什么样」的一套小语言。它不解决新问题,但能把「逐字符判断」这种啰嗦活压缩成一行。
import re
text = '点了一杯杨枝甘露,花了 18 元,送到宿舍 3 号楼'
print(re.findall(r'\d+', text))
输出:
['18', '3']
课程思政|科技自立自强 · 看懂模式,才不被模式牵着走
正则是一套描述「文字长什么样」的小语言,手机号校验、日志分析、数据清洗都靠它,如今几乎每个工具都把它当标配。网上一搜就有现成模式,可抄来的东西一旦匹配不上,你往往不知道从哪儿下手改。花点时间把 \d、+、() 这几个基本符号弄懂,这门手艺才算真正长在自己身上。
二、re 的四个主力函数
re 是标准库,跟着 Python 一起装好。日常干活,四个函数覆盖九成场景:
| 函数 | 干什么 | 返回什么 |
|---|---|---|
search(模式, 文本) | 在文本里找第一个符合的 | 匹配对象,找不到是 None |
match(模式, 文本) | 只从开头匹配 | 匹配对象,找不到是 None |
findall(模式, 文本) | 找出全部符合的 | 列表(最常用) |
sub(模式, 新内容, 文本) | 把符合的替换掉 | 替换后的新字符串 |
2.1 search:找第一个
import re
text = 'abc123def'
m = re.search(r'\d+', text)
print(m)
print('取到内容:', m.group())
print('它在哪里:', m.span())
输出:
<re.Match object; span=(3, 6), match='123'>
取到内容: 123
它在哪里: (3, 6)
2.2 match:只认开头——这是最容易搞错的一个
import re
print('search 找 abc123 里的数字:', re.search(r'\d+', 'abc123').group())
print('match 找 abc123 里的数字:', re.match(r'\d+', 'abc123'))
print('match 找 123abc 里的数字:', re.match(r'\d+', '123abc').group())
输出:
search 找 abc123 里的数字: 123
match 找 abc123 里的数字: None
match 找 123abc 里的数字: 123
search 从任意位置开始找,match 只认开头。上面第二个打印 None,就是因为字符串开头是字母而不是数字。
fullmatch(整串完全匹配);判断「里面有没有手机号」才用 search。用错会导致「只要开头像就算通过」这种漏洞。2.3 findall:一次找全部
import re
text = 'a1b22c333'
print(re.findall(r'\d+', text))
print(re.findall(r'[a-z]+', text))
输出:
['1', '22', '333']
['a', 'b', 'c']
返回的是字符串列表——想当成数字算,还要自己转 int()(和 csv 那一篇的坑一样)。
2.4 sub:替换
import re
print(re.sub(r'\d+', '#', 'a1b22c333'))
print(re.sub(r'\d+', '', 'a1b22c333')) # 换成空串 = 删掉
print(re.sub(r'\s+', ' ', '杨枝甘露 18 少冰')) # 多个空白压成一个
输出:
a#b#c#
abc
杨枝甘露 18 少冰
re.sub(r'\s+', ' ', 文本) 是清洗数据时最常用的一条:把连续的空格、制表符统统压成一个空格。做数据整理时几乎每个项目都会用到。三、元字符速查表
正则的「词汇」不多,下面这张表覆盖了绝大多数日常用法。不用背,用的时候回来查。
3.1 表示「某一类字符」
| 写法 | 含义 | 匹配的例子 |
|---|---|---|
. | 任意一个字符(默认不含换行) | a、1、中 |
\d | 一个数字 | 0~9 |
\D | 一个非数字 | a、空格 |
\w | 一个「字」:字母、数字、下划线,以及中文 | a、5、_、中 |
\s | 一个空白字符 | 空格、Tab、换行 |
[abc] | 方括号里的任意一个 | a 或 b 或 c |
[a-z] | a 到 z 里的任意一个 | g |
[^0-9] | 方括号里之外的任意一个(^ 是「除了」) | a、空格 |
3.2 表示「位置」
| 写法 | 含义 | 说明 |
|---|---|---|
^ | 字符串开头 | 在方括号里表示「除了」,位置不同意思不同 |
$ | 字符串结尾 | \d+$ 表示以数字结尾 |
\b | 单词边界 | 找独立的词时用 |
3.3 表示「几个」——量词
| 写法 | 含义 | 例子 |
|---|---|---|
* | 0 个或多个 | a*b 能匹配 b、ab、aaab |
+ | 1 个或多个(至少一个) | a+b 不能匹配 b |
? | 0 个或 1 个 | a?b 能匹配 b、ab |
{3} | 恰好 3 个 | \d{3} 匹配 123 |
{2,5} | 2 到 5 个 | \d{2,5} |
{2,} | 至少 2 个 | \d{2,} |
import re
print('+ 至少要一个:', re.findall(r'a+b', 'b ab aab aaab'))
print('* 可以一个都没有:', re.findall(r'a*b', 'b ab aab aaab'))
print('{3} 恰好三个:', re.findall(r'\d{3}', '12 123 1234'))
输出:
+ 至少要一个: ['ab', 'aab', 'aaab']
* 可以一个都没有: ['b', 'ab', 'aab', 'aaab']
{3} 恰好三个: ['123', '123']
看第三行要留意:1234 里被找出来的也是 123——{3} 是「恰好三个」,多出来的第四个不参与。想「至少三个」得写 \d{3,}。
四、贪婪与懒惰:.* 到底吃多少
量词有个默认脾气:能多吃就多吃,这叫「贪婪」(greedy)。行为差异在下面这个例子里看得最清楚:
import re
text = '<a><b>'
print('贪婪 <.*> :', re.findall(r'<.*>', text))
print('懒惰 <.*?> :', re.findall(r'<.*?>', text))
输出:
贪婪 <.*> : ['<a><b>']
懒惰 <.*?> : ['<a>', '<b>']
两行只差一个 ?,结果完全不同:
| 写法 | 叫法 | 怎么吃 | 这里的结果 |
|---|---|---|---|
<.*> | 贪婪 | 一路吃到最远的那个 > | 一整串 <a><b> |
<.*?> | 懒惰(非贪婪) | 吃到最近的那个就停 | 两段 <a> 和 <b> |
想变懒惰,就是在量词后面再加一个 ?:*?、+?、??、{2,5}? 都成立。
import re
text = '价格:12.5元,折扣价 8.8元'
print('贪婪:', re.findall(r'\d+\.?\d*', text))
print('再看一对引号:')
print(' 贪婪 "(.*)" :', re.findall(r'"(.*)"', '他说"你好",又说"再见"'))
print(' 懒惰 "(.*?)" :', re.findall(r'"(.*?)"', '他说"你好",又说"再见"'))
输出:
贪婪: ['12.5', '8.8']
再看一对引号:
贪婪 "(.*)" : ['你好",又说"再见']
懒惰 "(.*?)" : ['你好', '再见']
? 变懒惰;如果你要的是「从第一个到最后一个整段」,就用默认的贪婪。实际写的时候,见到 .* 先想一下这个问题,能省掉很多「结果为什么是一整坨」的困惑。AI 助手小贴士|元字符是正则的字母表,速查表交给 AI 生成最省事。
打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:把 . \d \w \s + * ? {m,n} 整理成一张表:符号、含义、一个能匹配的例子和一个不匹配的例子
课程思政|工匠精神与严谨求实 · 差一个问号,结果差很远
贪婪的 .* 一路吃到最远的那个符号,懒惰的 .*? 吃到最近的就停,两行只差一个 ?,结果却完全不同。正则最考验的,正是这种「以实际匹配为准」的严谨:别凭感觉说「应该能匹配上」,写一个样例跑一遍再下结论。这份较真用到统计和分析里,就是数据人最该有的基本功。
五、分组:把要的那一段单独拎出来
用圆括号 () 把模式中的一部分括起来,就把这部分单独标了出来,叫一个「组」(group)。
5.1 按编号取
import re
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', '日期:2026-10-04')
print('整段:', m.group(0))
print('第 1 组(年):', m.group(1))
print('第 2 组(月):', m.group(2))
print('第 3 组(日):', m.group(3))
print('一次全拿出来:', m.groups())
输出:
整段: 2026-10-04
第 1 组(年): 2026
第 2 组(月): 10
第 3 组(日): 04
一次全拿出来: ('2026', '10', '04')
group(0) 是整段匹配,group(1) 往后才是各个括号里的内容。这个「0 号是全体」的规矩要记住。
5.2 按名字取(推荐)
import re
m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', '2026-10-04')
print('年:', m.group('year'))
print('一次全拿出来:', m.groupdict())
输出:
年: 2026
一次全拿出来: {'year': '2026', 'month': '10', 'day': '04'}
写法是 (?P<名字>模式)。组一多,用名字取值比数编号靠谱得多——加一个括号就要重数一遍编号,很容易错。
5.3 在替换里用组
import re
print(re.sub(r'(\d{4})/(\d{2})/(\d{2})', r'\1-\2-\3', '日期:2026/10/04'))
输出:
日期:2026-10-04
替换内容里写 \1 \2,就等于「把第 1 组、第 2 组的内容放这里」。注意替换串也写了 r 前缀。
5.4 只想分组、不想占编号:(?:...)
import re
print('带编号: ', re.findall(r'(\d{3})-(\d{4})', '010-1234'))
print('不占编号:(?:...)', re.findall(r'(?:\d{3})-(\d{4})', '010-1234'))
输出:
带编号: [('010', '1234')]
不占编号:(?:...) ['1234']
加了 ?: 的括号只起到「把几个字符圈在一起」的作用,不参与编号、也不出现在结果里。当 findall 遇到多个组时,它返回的是「组的元组」而不是整段——这时候 ?: 就能把不关心的那部分摘掉。
六、实战:四个常用模式
下面四个模式是日常最常用的,读懂它们的构成,比背下来更有用。
6.1 手机号:1[3-9]\d{9}
import re
text = '联系方式:13800138000,备用 13900139000,错误示范 12345678901'
print(re.findall(r'1[3-9]\d{9}', text))
输出:
['13800138000', '13900139000']
拆开看:1 打头 → [3-9] 第二位是 3 到 9 → \d{9} 后面再跟 9 位数字,一共 11 位。12345678901 没被选中,因为第二位是 2。
6.2 邮箱:[\w.]+@[\w.]+\.[a-z]{2,}
import re
text = '注册邮箱 tangyuan@example.com 或 a@b.cn'
print(re.findall(r'[\w.]+@[\w.]+\.[a-z]{2,}', text))
输出:
['tangyuan@example.com', 'a@b.cn']
拆开看:[\w.]+ 用户名(字母数字下划线点)→ @ → [\w.]+ 域名 → \. 一个真正的点 → [a-z]{2,} 结尾至少两位字母。
6.3 日期归一:把 / 换成 -
import re
print(re.sub(r'(\d{4})/(\d{2})/(\d{2})', r'\1-\2-\3', '开学日期:2026/09/01'))
输出:
开学日期:2026-09-01
6.4 提取并计算:把价格加起来
import re
text = '薯片 5.5元,奶茶 12.8元,辣条 3.2元'
prices = [float(x) for x in re.findall(r'\d+\.\d+', text)]
print('价格:', prices)
print('合计:', round(sum(prices), 1))
输出:
价格: [5.5, 12.8, 3.2]
合计: 21.5
这一段把「提取 → 转类型 → 计算」三步串了起来,是最实用的组合:findall 拿到的一定是字符串,要算数就得自己转。
| 要解决的问题 | 模式 | 用法 |
|---|---|---|
| 取所有数字 | \d+ | findall |
| 取所有小数 | \d+\.\d+ | findall + float |
| 验证是不是手机号 | 1[3-9]\d{9} | fullmatch |
| 找所有邮箱 | [\w.]+@[\w.]+\.[a-z]{2,} | findall |
| 多个空白压成一个 | \s+ | sub |
| 去掉首尾空格 | ^\s+|\s+$ | sub(其实 strip() 更简单) |
课程思政|数据安全与法治素养 · 能匹配,不等于能收集
这一节教你怎么用模式,从一大段文本里捞出手机号和邮箱。正则确实是「捞」数据的利器,但技术做得到,和该不该做是两回事。同学的手机号、邮箱都是受法律保护的个人信息,练手时不妨把它们换成隐藏中间四位的脱敏数据,既练了手艺,也守住了分寸——真实业务里,越权采集、违规使用是真要担责任的。
七、re.compile 与常用 flags
7.1 编译成对象,反复用
import re
phone = re.compile(r'1[3-9]\d{9}') # 编译一次
print(phone.findall('联系 13800138000'))
print(phone.search('我的号是 13900139000').group())
print(bool(phone.fullmatch('13800138000')))
输出:
['13800138000']
13900139000
True
同一个模式要用很多次时,compile() 一次、之后重复使用,写起来也更整齐。
re.findall(...) 完全没问题。compile() 的真正好处是可读性:模式起个名字(phone)比在代码里到处撒一串符号清楚得多。7.2 三个常用 flags
| flag | 含义 | 什么时候用 |
|---|---|---|
re.I | 忽略大小写(ignore case) | 找 python 时不想漏掉 Python |
re.M | 多行模式:^ 和 $ 匹配每一行的行首行尾 | 逐行处理一段文本 |
re.S | 让 . 也匹配换行 | 跨行抓内容 |
import re
print('不加 I:', re.findall(r'python', 'Python PYTHON python'))
print('加了 I:', re.findall(r'python', 'Python PYTHON python', re.I))
text = 'abc\n123\nxyz'
print('不加 M:', re.findall(r'^\d+', text))
print('加了 M:', re.findall(r'^\d+', text, re.M))
输出:
不加 I: ['python']
加了 I: ['Python', 'PYTHON', 'python']
不加 M: []
加了 M: ['123']
多个 flag 用竖线连起来:re.I | re.M。
re.M 那一组的对比很值得注意:不加时 ^ 只认整段文字的开头(开头是 abc,所以没匹配到数字);加了 re.M 之后 ^ 变成「每一行的开头」,才把中间那行 123 捞出来。八、常见坑
坑 1:忘了写 r 前缀
正则里的 \d、\s、\b 在普通字符串里是「无效转义」,Python 会警告你。虽然多数情况还能跑,但有些模式会被悄悄改掉意思,非常难查。
import re
print(re.findall('\d+', 'a1b22')) # 少了 r 前缀
输出:
['1', '22']
运行会成功,但解释器会先给一句警告:
demo.py:2: SyntaxWarning: invalid escape sequence '\d'处理办法:所有正则串一律加 r 前缀,写成 r'\d+'。这样反斜杠就是反斜杠本身,不会被 Python 先解释一遍。
坑 2:拿 None 去调 .group()
import re
m = re.search(r'\d+', '这段文字里没有数字')
print(m.group()) # m 是 None
运行后会报错(逐字照抄解释器):
AttributeError: 'NoneType' object has no attribute 'group'
search / match 找不到时返回的是 None,None 当然没有 group()。
处理办法:先判断再取。
import re
m = re.search(r'\d+', '这段文字里没有数字')
if m:
print('找到了:', m.group())
else:
print('没找到')
输出:
没找到
坑 3:把 match 当成 search 用
import re
text = '订单号 A123'
print('match 只认开头:', re.match(r'\d+', text))
print('search 任意位置:', re.search(r'\d+', text).group())
输出:
match 只认开头: None
search 任意位置: 123
处理办法:「这串整体是不是符合格式」用 fullmatch;「里面有没有符合的」用 search;match 只在明确「必须从开头算起」时才用。
坑 4:模式里该转义的没转义
. 在正则里是「任意字符」,想表示真正的点必须写 \.:
import re
print('没转义:', re.findall(r'\d.\d', '1a2 1.2'))
print('转了义:', re.findall(r'\d\.\d', '1a2 1.2'))
输出:
没转义: ['1a2', '1.2']
转了义: ['1.2']
要转义的还有:\. \* \+ \? \( \) \[ \] \{ \} \| \^ \$。找不准时,一个稳妥的判断是:这个符号在正则里有特殊含义吗?有就加反斜杠。
坑 5:findall 带分组后返回的东西变了
import re
print('没有组:', re.findall(r'\d{3}-\d{4}', '010-1234'))
print('有一个组:', re.findall(r'(\d{3})-\d{4}', '010-1234'))
print('两个组:', re.findall(r'(\d{3})-(\d{4})', '010-1234'))
输出:
没有组: ['010-1234']
有一个组: ['010']
两个组: [('010', '1234')]
没有组时返回「整段」;有组之后返回的就变成「组」了——一个组返回字符串,多个组返回元组,整段反而拿不到了。不想要这个效果就用 (?:...)。
坑 6:以为 * 和 + 是一回事
import re
print('a* 匹配 bb:', re.findall(r'a*', 'bb'))
print('a+ 匹配 bb:', re.findall(r'a+', 'bb'))
输出:
a* 匹配 bb: ['', '', '']
a+ 匹配 bb: []
* 允许「一个都没有」,所以在没有 a 的地方也会匹配出空字符串(这里出现了 3 次空匹配)。+ 要求至少一个,没有就什么都匹配不到。要「至少一个」的时候就写 +,别用 *。
九、报错手册
报错原文都是从本机解释器逐字抄下来的,对照着找就行。
| 报错 / 警告原文(最后一行) | 什么情况 | 怎么改 |
|---|---|---|
AttributeError: 'NoneType' object has no attribute 'group' | search / match 没找到(返回 None),却直接调了 .group() | 先 if m: 判断,再取 group() |
re.PatternError: missing ), unterminated subpattern at position 0 | 括号没配对(少写或多写 )) | 从头数一遍括号;长模式分段写、分段测 |
re.PatternError: bad character range | 方括号里的范围写反了,如 [9-0] | 改成 [0-9] |
SyntaxWarning: invalid escape sequence '\d' | 正则串忘了加 r 前缀 | 写成 r'\d+' |
TypeError: expected string or bytes-like object | 把数字或其他类型传给了 re 的函数 | 先 str(值) 转成字符串 |
re.PatternError: nothing to repeat at position 0 | 把量词写在了开头,如 *abc | 量词必须跟在「要重复的东西」后面 |
下面几条是「不报错,但结果不对」的,比报错更值得当心:
| 现象 | 原因 | 怎么改 |
|---|---|---|
| 只匹配到开头,中间明明有 | 用了 match | 改用 search |
| 本来想要好几段,结果全挤成一段 | 用了贪婪的 .* | 加 ? 变懒惰:.*? |
findall 返回的元组少了一截 | 模式里加了分组改变了返回内容 | 不想要的组写成 (?:...) |
fullmatch 判手机号总是不通过 | 字符串带了空格或换行 | 先 .strip() 再判 |
AI 助手小贴士|正则靠实战题养手感。
打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:出 3 道正则题:提取文本里所有金额、校验 18 位身份证号(末位可能是 X)、把各种格式的日期统一成 YYYY-MM-DD。给我测试文本,我写完你判分
十、十个基础案例
下面十个例子都很短,建议每个都亲手敲一遍、跑一遍。
1. 把一句话里的数字全捞出来
import re
print(re.findall(r'\d+', '奶茶 18 元,薯条 12 元,鸡翅 20 元'))
输出:
['18', '12', '20']
2. 判断一串是不是 11 位手机号
import re
def is_phone(s):
return bool(re.fullmatch(r'1[3-9]\d{9}', s))
print(is_phone('13800138000'))
print(is_phone('1380013800'))
print(is_phone('13800138000 '))
输出:
True
False
False
3. 找出所有邮箱
import re
print(re.findall(r'[\w.]+@[\w.]+\.[a-z]{2,}', '注册邮箱 tangyuan@example.com 或 a@b.cn'))
输出:
['tangyuan@example.com', 'a@b.cn']
4. 把日期里的斜杠换成短横
import re
print(re.sub(r'(\d{4})/(\d{2})/(\d{2})', r'\1-\2-\3', '2026/10/04 开学'))
输出:
2026-10-04 开学
5. 多个连续空格压成一个
import re
print(re.sub(r'\s+', ' ', '杨枝甘露 18 少冰'))
输出:
杨枝甘露 18 少冰
6. 隐藏手机号中间四位
import re
print(re.sub(r'(\d{3})\d{4}(\d{4})', r'\1****\2', '13800138000'))
输出:
138****8000
7. 用命名组解析一段「日期+温度」
import re
m = re.search(r'(?P<date>\d{4}-\d{2}-\d{2})\s+(?P<temp>-?\d+℃)', '2026-10-04 19℃')
print(m.groupdict())
输出:
{'date': '2026-10-04', 'temp': '19℃'}
8. 用 findall + float 把价格加总
import re
text = '薯片 5.5元,奶茶 12.8元'
print(round(sum(float(x) for x in re.findall(r'\d+\.\d+', text)), 1))
输出:
18.3
9. 忽略大小写地统计关键词出现次数
import re
text = 'Python 很好用,python 也很好用,PYTHON 最常用'
print('出现了', len(re.findall(r'python', text, re.I)), '次')
输出:
出现了 3 次
10. 只保留中文字符
import re
print(re.findall(r'[\u4e00-\u9fa5]+', 'Name:芋圆 Age:18'))
输出:
['芋圆']
十一、常见问题
Q:正则难吗?要背多少?
不用背。只需要记住三件事:\d 是数字、+ 是「一个或多个」、() 是分组。其余全部现场查第三节的表。写了十几个模式之后,常用的那些自然就记住了。
Q:search、match、fullmatch 怎么选?
一句话:整串必须完全符合 → fullmatch;只要里面出现就算 → search;必须从开头算起 → match。校验类需求(手机号、学号、日期格式)基本都用 fullmatch。
Q:为什么我的 .* 把整段都吃掉了?
因为量词默认贪婪。在量词后加 ? 变成 .*? 就是「吃到最近的那个就停」。判断标准是:你要「每一段分别取」还是「整段一次取」。
Q:正则能不能用来解析 HTML / JSON?
不要。这些结构有嵌套,正则处理不了嵌套(这不是你写得不够好,是正则本身的能力边界)。JSON 用 json 库,表格用 csv 库,HTML 用专门的解析库。正则只适合「从一坨文本里捞固定形状的片段」。
Q:模式写得太长看不懂怎么办?
分段写、分段测。先用 print(re.findall(...)) 把一小段模式调通,再往外套。另外把模式存进变量或 compile() 并起个名字(phone、email),比在代码里直接撒一串符号清楚得多。
Q:这个和「网络数据采集」有什么关系?
关系很大——网页拿回来是一大坨 HTML 文本,从中捞出标题、价格、日期这类固定形状的片段时,正则是最快的工具。学完这一篇再去看数据采集,会顺手很多。
AI 助手小贴士|正则是「写了就忘」的典型,AI 是最好的正则解释器——让它写完逐段讲解,再配测试用例。
打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:写一个匹配中国大陆手机号的正则表达式,逐段解释每个符号的含义,再给 5 个测试用例,其中包含 2 个应该匹配失败的例子
十二、随堂自测(12 题)
点击选项即可判分,答错的题会立刻显示解析。
1re.findall(r'\d+', 'a1b22c333') 的结果是?
- A.
[1, 22, 333] - B.
['1', '22', '333'] - C.
['1', '2', '2', '3', '3', '3'] - D.
6
\d+ 是「一个或多个数字」,所以连续的数字会被当成一整段。findall 返回的永远是字符串列表——要算数得自己 int() 转。答案 A 里的元素是数字类型,那得靠推导式转出来。2re.search() 和 re.match() 的区别是?
- A. 两者完全一样
- B.
search从头找,match从任意位置找 - C.
search从任意位置找,match只从开头匹配 - D.
match只能匹配数字
re.match(r'\d+', 'abc123') 会返回 None,因为开头是字母;re.search 则能找到 123。要「整串完全匹配」应该用 fullmatch。3re.findall(r'<.*>', '<a><b>') 的结果是?
- A.
['<a>', '<b>'] - B.
'<a><b>' - C.
[] - D.
['<a><b>']
.* 默认是贪婪的,会一路吃到文本里最远的那个 >,所以整串被一次匹配完。想分成两段就得加 ? 写成 <.*?>,让它「吃到最近的 > 就停」。4正则模式 r'\d{3}' 在文本 '12 123 1234' 里会匹配到什么?
- A.
['123'] - B.
['123', '123'] - C.
['12', '123', '1234'] - D. 什么都匹配不到
{3} 是「恰好三个数字」。12 不够三个、不匹配;1234 里只有前三个数字组成一次匹配,多出来的第四个不参与。想要「至少三个」得写 \d{3,}。5代码里出现 re.findall('\d+', text)(没有 r 前缀),会有什么后果?
- A. 语法错误,程序根本不能运行
- B. 能跑,但解释器会给一句
SyntaxWarning: invalid escape sequence '\d' - C. 结果和加了 r 完全一样,也没有任何提示
- D. 自动变成
\\d+,结果不同
'\d' 在 Python 里是无效转义,解释器会警告。多数情况结果碰巧一样,但有些模式会被悄悄改掉意思,很难查。处理办法就一条:所有正则串一律加 r 前缀。6re.search(r'\d+', '这段文字里没有数字').group() 会怎样?
- A. 返回空字符串
- B. 返回
None - C. 报
AttributeError: 'NoneType' object has no attribute 'group' - D. 自动返回文本里最近的数字
search 找不到时返回 None,紧接着调 .group() 就报 AttributeError。正确写法是先判断:if m: 再取 m.group();或者直接用 findall(找不到时返回空列表,不会报错)。7用命名组解析日期,取值应该怎么写?
- A.
m.group(7) - B.
m.get('year') - C.
m.year - D.
m.group('year')或m.groupdict()
(?P<名字>模式),取值用 group('名字'),一次全取用 groupdict()。返回值是字符串,要算数还得转 int()。组一多,用名字比数编号可靠得多。8把 2026/10/04 改成 2026-10-04,正确的写法是?
- A.
re.sub(r'(\d{4})/(\d{2})/(\d{2})', r'\1-\2-\3', text) - B.
re.sub(r'(\d{4})/(\d{2})/(\d{2})', '-', text) - C.
re.findall(r'/.*-', text) - D.
re.match(r'\d{4}', text)
\1 \2 \3 把组放回去,就完成了「换分隔符」的操作。B 会把整段日期替换成一个减号,C 和 D 根本不做替换。9re.findall(r'(\d{3})-(\d{4})', '010-1234') 返回什么?
- A.
['010-1234'] - B.
[('010', '1234')] - C.
['010', '1234'] - D.
['1234']
findall 返回的就是组的元组:一个组返回字符串,多个组返回元组,整段反而拿不到了。不想要这个效果,把不关心的组写成 (?:...)。10re.match(r'\d+', 'abc123') 返回 None,想找到里面的 123 应该改成?
- A.
re.match(r'\d+', 'abc123', re.I) - B.
re.match(r'.*\d+', 'abc123'),这样最规范 - C.
re.search(r'\d+', 'abc123') - D.
re.findall(r'\d', 'abc123')
match 只认开头,开头是 a 自然匹配不到;换成 search 就能从任意位置找。B 虽然能匹配上,但 .* 会吞掉前面的内容,取 group() 得到的是 'abc123' 而不是 '123'。11判断用户输入的字符串是不是 11 位手机号,「整串必须完全符合」,应该用哪个函数?
- A.
re.fullmatch - B.
re.match - C.
re.search - D.
re.findall
fullmatch:整串必须完全符合才算通过。search / match 只要一部分匹配上就会返回结果,会出现 '13800138000abc' 也被判为合法这种漏洞。另外记得先 .strip() 去掉首尾空格。12想「忽略大小写」地查找,应该怎么做?
- A. 在模式末尾写
! - B. 只能靠
text.lower()预处理,re 不支持 - C. 把模式整个写成大写
- D. 加上
re.I参数,如re.findall(r'python', text, re.I)
re.I(ignore case)忽略大小写,re.M 让 ^ $ 按行匹配,re.S 让 . 也能匹配换行。多个 flag 用竖线连:re.I | re.M。预处理 lower() 也能用,但不是唯一办法。