part03-自然语言处理-正则表达式
part03-自然语言处理-正则表达式
学完本讲,你应该能够:
- 说清楚正则表达式由哪两类字符组成,知道 Python 用哪个库来跑正则。
- 分清 re 六大函数(match、search、findall、sub、finditer、split)各自干什么、返回什么。
- 读懂量词、字符类、锚点边界、特殊字符四类元字符,能看懂别人写的正则。
- 记住 22 条常用正则搭配,能判断"校验"和"抽取"该用哪一种。
- 独立完成五个实战任务:提取括号数字、提取邮箱、提取地名与邮编、网页标签取文本、过滤非中文字符。
课件链接:.
1.正则表达式是什么:普通字符与元字符
前言
上一讲我们解决了"米"的问题——语料从哪来。这一讲解决"淘米"的问题:爬来的语料里混着 HTML 标签、网址、表情、乱码,必须先把脏东西洗掉才能喂给模型 👈
正则表达式(Regular Expression,常简写为 regex / regexp),就是用一串特殊符号写成的"文本筛选规则"。你告诉计算机"我要找长什么样的文字",它就按这条规则在文本里帮你找、帮你提、帮你换。
一句话理解:正则就是给文本写的"寻物启事"——写清楚特征,计算机按图索骥。
正则表达式:用普通字符和元字符组合成的规则字符串,用来描述"要匹配什么样的文本"。例:相当于海关的"违禁品清单"——写清楚哪些特征要拦、哪些放行,计算机拿着清单逐条比对。
1 组成元素:普通字符 + 元字符
正则表达式由两类字符拼起来:
| 类型 | 含义 | 举例 | 在 a*b 里 |
|---|---|---|---|
| 普通字符 | 字母、数字、普通符号,只代表它自己 | a、Z、5、_、@ | a 和 b 是普通字符 |
| 元字符 | 有特殊含义的符号,代表一类字符或一种规则 | *、+、?、[]、\d、^、.、|、() | * 是元字符(表示前面的 a 可出现 0 次或多次) |
元字符:在正则里有特殊职责、不能当普通字符看待的符号。例:相当于棋谱里的"车马炮"——普通棋子走一步是一步,车马炮有各自的特殊走法规则。
判断诀窍:看到 * + ? [] () ^ $ . \| \ 这类符号,先想它是不是在"发号施令",而不是在当普通字符。
2 Python 的 re 库
Python 用标准库 re 提供正则引擎接口,能把正则编译成模式对象,完成四大类操作:
| 能做的事 | 对应函数 | 典型场景 |
|---|---|---|
| 匹配(从开头验证格式) | re.match() | 判断手机号、邮箱的格式是否合法 |
| 查找(找第一个 / 找全部) | re.search()、re.findall()、re.finditer() | 从长文本里抽手机号、抽邮箱 |
| 替换(清洗主力) | re.sub() | 删掉所有非中文字符、脱敏手机号 |
| 分割 | re.split() | 按多种标点切分句子 |
import re # re 是标准库,无需 pip 安装,直接 import 就能用
text = '自然语言处理是研究能实现人与计算机之间用自然语言进行有效通信的各种理论和方法。'
m = re.search(r'自然语言', text)
print(m.group()) # 自然语言re 库:Python 内置的正则表达式模块,把正则规则交给它执行。例:相当于把"寻物启事"交给专门的搜查队去执行。
3 为什么 NLP 必须学正则
| 用处 | 说明 | 例子 |
|---|---|---|
| 清洗 | 把噪声字符统一删掉 | 去掉 HTML 标签、表情、多余空格 |
| 抽取 | 从大段文本里批量捞结构化信息 | 批量提取手机号、日期、价格 |
| 校验 | 判断数据格式是否合法 | 用户输入的是不是合法邮箱 |
| 切分 | 按规则把文本切成小单元 | 按中英文标点切句子 |
配套课件:互动讲义第 1 关 · 字符分类(普通字符 vs 元字符)
自检
- 正则表达式主要由哪两类字符组成?A.关键字与标识符 B.普通字符与元字符 C.字母与数字 D.运算符与操作数
答案:B.由普通字符(字母、数字、符号)和元字符(有特殊含义的字符)组成 - 在表达式
a*b中,属于元字符的是?A.a B.b C.*D.以上都是
答案:C.*表示前一个字符可出现 0 次或多次,有特殊含义;a、b 只代表自己 - Python 中提供正则表达式引擎的标准库是?A.
regexB.reC.stringD.os
答案:B.import re之后即可调用 re.match() 等函数 - 下列符号中属于普通字符(无特殊含义)的是?A.
*B.?C.5D.[]
答案:C.* ? []都是元字符,数字 5 只代表自身 - 使用 re 库的函数之前必须先做什么?A.
pip install reB.import reC.re.compile()D.声明编码
答案:B.re 是 Python 标准库,无需安装,import 即可
2.re 六大函数:谁干谁活
前言
规则写好了,得交给函数去执行。六个函数名字不同,干的活也不同:有的只看开头,有的扫全串;有的只找一个,有的全找出来;有的替换,有的切分 👇
先看签名(括号里写什么),再看返回值(拿到手的是什么)。
1 六大函数速查
| 函数 | 语法 | 作用 | 返回值 |
|---|---|---|---|
match | re.match(pattern, string, flags=0) | 只从起始位置匹配 | Match 对象 或 None |
search | re.search(pattern, string, flags=0) | 扫描全串,找第一个匹配 | Match 对象 或 None |
findall | re.findall(pattern, string, flags=0) | 找所有非重叠匹配 | 字符串列表(有分组时为元组列表) |
sub | re.sub(pattern, repl, string, count=0, flags=0) | 把匹配到的内容替换成 repl | 替换后的新字符串 |
finditer | re.finditer(pattern, string, flags=0) | 找所有匹配,但逐个给 | 迭代器(每个元素是 Match 对象) |
split | re.split(pattern, string, maxsplit=0, flags=0) | 按匹配位置切分 | 切分后的字符串列表 |
Match 对象:匹配成功后返回的一个"结果包裹",里面装着匹配到的内容、位置等信息。例:相当于快递包裹——包裹本身不是东西,要用
group()拆开才能拿到里面的货。
2 match vs search:起点之争
这是最容易错的一组:
import re
text = 'NLP 2026 自然语言处理'
print(re.match(r'\d', text)) # None ← 开头是 N,不是数字,直接放弃
print(re.search(r'\d', text)) # <Match object> ← 扫全串,找到 2026 的 2
print(re.search(r'\d', text).group()) # 2| 对比项 | match | search |
|---|---|---|
| 从哪里开始 | 必须是字符串第一个字符 | 从任意位置扫描 |
| 典型用途 | 校验整串格式对不对 | 查找文本里有没有某类内容 |
| 失败时 | 返回 None | 返回 None |
口诀:"验格式用 match,找内容用 search" 👈
3 findall vs finditer:列表 vs 迭代器
两者都能找出所有匹配,区别在"拿到手是什么":
s = '123abc456'
print(re.findall(r'\d', s)) # ['1', '2', '3', '4', '5', '6'] 直接给列表
for m in re.finditer(r'\d', s): # 逐个给 Match 对象
print(m.group(), m.start()) # 0 1 / 1 2 / 2 3 ... 还能拿到位置迭代器(iterator):"按需生产、一次给一个"的对象,要用 for 循环逐个取。例:相当于自助餐的传送带——菜是一盘一盘过来的,不是一次性全堆在你盘子里,所以处理大文本更省内存。
只要内容 → findall;还要每个匹配的位置 → finditer。
4 sub 与 split:改和切
# sub:替换(清洗就靠它)
re.sub(r'\d', 'X', '123abc456') # 'XXXabcXXX'
# split:按规则切分
re.split(r',', 'a,b,c') # ['a', 'b', 'c']
re.split(r'[,,.。]', '你好,世界。再见') # ['你好', '世界', '再见']5 group():拆包裹取货
search / match / finditer 返回的是 Match 对象,要用 group() 取出内容:
m = re.search(r'(\d{4})-(\d{2})', '今天是 2026-09')
print(m.group()) # 2026-09 第 0 组 = 整体
print(m.group(1)) # 2026 第 1 组 = 第一个括号
print(m.group(2)) # 09 第 2 组 = 第二个括号分组捕获:正则里的一对
()会把匹配到的内容单独"存一份",用group(1)、group(2)依次取出。例:相当于快递分拣时把"省/市/区"分别装进三个小格子,之后能单独取用。
配套课件:互动讲义第 2 关 · re 函数实验室
自检
- 下列哪个函数只从字符串起始位置开始匹配,失败返回 None?A.
re.match()B.re.search()C.re.findall()D.re.sub()
答案:A.match 从起始位置匹配,起始不匹配就返回 None - 哪个函数返回字符串中所有非重叠匹配项组成的列表?A.
re.search()B.re.findall()C.re.finditer()D.re.split()
答案:B.findall 返回列表;finditer 返回迭代器 - 想把文本里所有数字替换成
X,应使用?A.re.match()B.re.split()C.re.sub()D.re.finditer()
答案:C.re.sub(r'\d','X',s),count=0 表示全部替换 re.search()匹配成功后,用哪个方法取出匹配到的子串?A.span()B.group()C.start()D.find()
答案:B.Match 对象提供 group() 获取匹配子串finditer与findall的核心区别是?A.finditer 更快 B.finditer 返回 Match 对象迭代器,findall 返回字符串列表 C.findall 支持分组 D.两者完全一样
答案:B.需要每个匹配的位置信息时用 finditer,只要内容时用 findall
3.量词元字符:到底重复几次
前言
元字符的第一大类:量词(quantifier)——规定"前一个字符可以出现几次"。它是正则里用得最频繁、也最容易写错的一类。
1 六个量词
| 元字符 | 含义 | 示例 | 能匹配 | 不能匹配 |
|---|---|---|---|---|
* | 前一个字符出现 0 次、1 次或多次 | a*b | b、ab、aab | — |
+ | 前一个字符出现 1 次或多次 | a+b | ab、aab、aaab | b |
? | 前一个字符出现 0 次或 1 次 | a?b | b、ab | aab |
{n} | 前一个字符恰好出现 n 次 | a{3}b | aaab | aab、aaaab |
{n,} | 前一个字符至少出现 n 次 | a{2,}b | aab、aaab、aaaab | ab |
{n,m} | 前一个字符出现 n 到 m 次 | a{2,4}b | aab、aaab、aaaab | ab、aaaaab |
记忆口诀:星号可有可无,加号至少一个,问号最多一个,花括号精确控量 👈
2 常见写法对照
| 想匹配 | 量词写法 |
|---|---|
| 任意位数的数字 | \d+ |
| 恰好 11 位手机号 | 1\d{10} |
| 6 位邮政编码 | \d{6} |
| 2~4 个汉字 | [\u4e00-\u9fa5]{2,4} |
3 贪婪与非贪婪
量词默认是贪婪的:能多匹配就多匹配。在量词后面加一个 ? 就变成非贪婪(懒惰):能少匹配就少匹配。
s = '<b>标题</b>和<b>正文</b>'
re.findall(r'<b>.*</b>', s) # 贪婪:['<b>标题</b>和<b>正文</b>'] 一口气吞到底
re.findall(r'<b>.*?</b>', s) # 非贪婪:['<b>标题</b>', '<b>正文</b>'] 见好就收贪婪匹配:量词默认尽可能多地"吃"字符,直到吃不动为止。例:相当于夹自助餐——手伸到最长,能夹多少夹多少;加
?之后变成"只夹够吃的那一块"。
自检
a+b不能匹配下列哪个字符串?A.abB.aabC.bD.aaab
答案:C.+要求前一个字符至少出现 1 次,所以必须有 aa{2,4}b不能匹配的是?A.aabB.aaabC.aaaabD.aaaaab
答案:D.{2,4}限定 a 出现 2~4 次,5 个 a 超出上限a?b可以匹配下列哪组字符串?A.只有abB.只有bC.b和abD.b、ab、aab
答案:C.?表示 0 次或 1 次,a 可有可无,但不能有 2 个- 要匹配"恰好 3 位数字",正确的量词写法是?A.
\d{1,3}B.\d{3,}C.\d{3}D.\d?
答案:C.{n}表示前一个字符恰好出现 n 次 - 关于
*与+的区别,正确的是?A.两者完全相同 B.*允许 0 次,+要求至少 1 次 C.+允许 0 次 D.*只能匹配数字
答案:B.a*b可匹配b,a+b不能匹配b
4.字符类·锚点边界·特殊字符
前言
元字符的第二、三、四大类。这一关信息量大,但规律很强:大写字母就是小写字母的反义(\d 数字 / \D 非数字) 👇
1 字符类元字符:代表"某一类字符"
| 元字符 | 含义 | 等价写法 / 示例 |
|---|---|---|
[] | 匹配方括号中的任意一个字符 | [abc] 可匹配 a、b、c 中任意一个 |
[^] | 匹配不在方括号中的任意字符 | [^abc] 匹配除 a、b、c 以外的字符 |
- | 表示范围 | [a-z] 任意小写字母;[0-9] 任意数字 |
\d | 任意数字 | 等价于 [0-9] |
\D | 任意非数字 | 等价于 [^0-9] |
\w | 任意字母、数字、下划线 | 等价于 [a-zA-Z0-9_] |
\W | 任意非字母数字下划线 | 等价于 [^a-zA-Z0-9_] |
\s | 任意空白字符(空格、制表符、换行) | 匹配 Hello World! 中的空格 |
\S | 任意非空白字符 | 匹配 Hello World! 中的 Hello、World! |
字符类:用一个符号代表"一整类字符",不用把每个字符都列出来。例:相当于超市货架的标签牌——写"饮料区"就不用把可乐、雪碧、矿泉水一个个写出来。
2 锚点与边界:规定"位置"
| 元字符 | 含义 | 示例 |
|---|---|---|
^ | 匹配字符串的开始位置 | ^[A-Z] 匹配以大写字母开头的字符串 |
$ | 匹配字符串的结束位置 | a$ 匹配以字母 a 结尾的字符串 |
\b | 匹配单词边界(单词前后的位置) | \babc\b 能匹配 abc,但不能匹配 abcd 或 aabc |
锚点:只规定"匹配发生在哪",本身不消耗任何字符。例:相当于答题卡上的"从第 1 题开始、到第 10 题结束"的框线,框线自己不是答案。
3 特殊字符:转义、通配、或、分组
| 元字符 | 含义 | 示例 |
|---|---|---|
\ | 转义字符,把元字符变回普通字符 | \. 匹配真正的点号 . |
. | 匹配任意一个字符(除换行 \n) | a.b 可匹配 a1b、a_b、a b、aXb |
| | 逻辑或 | a|b 匹配 a 或 b |
() | 分组匹配 | (abc)+ 可匹配 abc、abcabc |
4 原始字符串:为什么前面要加 r
写正则时强烈建议在字符串前加 r(raw string,原始字符串),这样字符串里的 \ 不会被 Python 当成转义符吃掉。
# 不加 r:'\d' 里的 \d 会被 Python 转义处理,正则拿到的不是你写的东西
re.findall('\d', 'a1b2') # 能跑,但写法危险、容易被转义坑
# 加 r:所见即所得,正则原样收到 \d
re.findall(r'\d', 'a1b2') # ['1', '2'] 推荐写法原始字符串(raw string):在字符串前加
r,让里面的反斜杠\不再当转义符。例:相当于给字符串套了个"免翻译罩"——写什么就传什么,正则才能收到完整的\d。
配套课件:互动讲义第 4 关 · 元字符速查台
自检
\d等价于下列哪个字符类?A.[a-z]B.[0-9]C.[^0-9]D.\w
答案:B.\d匹配任意数字;\D才是[^0-9]\W表示匹配什么?A.任意数字 B.任意空白字符 C.任意非字母/数字/下划线的字符 D.任意字母
答案:C.\W是\w的反集,\w=[a-zA-Z0-9_]- 要匹配"以大写英文字母开头"的字符串,模式开头应写?A.
[A-Z]$B.^[A-Z]C.\b[A-Z]D.[A-Z]*
答案:B.^锚定字符串开始位置 \babc\b不能匹配下列哪个?A.abcB.a abc bC.abcdD.abc.
答案:C.\b是单词边界,abcd 中 abc 后紧跟字母 d,不构成边界- 想匹配真正的点号
.本身(不是"任意字符"),应写?A..B.\.C.[.]以外都不行 D.|^.
答案:B..是元字符,加\转义后才表示点号本身
5.常用正则表达式搭配 22 条
前言
真实项目里没人从零写正则,都是站在现成模式上改。这 22 条是 PPT 汇总的高频搭配,按用途分成四张表。
关键心法:先判断你是要"校验"还是要"抽取"——校验加 ^ $,抽取去掉 ^ $ 👈
1 校验类:整串必须完全符合(带 ^ $)
| 任务 | 正则表达式 | 说明 |
|---|---|---|
| 匹配整数 | ^\d+$ | 全是数字,至少 1 位 |
| 匹配浮点数 | ^\d+\.\d+$ | 必须有小数点,如 3.14 |
| 匹配手机号码 | ^1\d{10}$ | 1 开头 + 10 位数字 = 11 位 |
| 匹配身份号码 | ^\d{17}[\dXx]$ | 17 位数字 + 最后一位可以是 X |
| 匹配日期 | ^(\d{4})-(\d{2})-(\d{2})$ | 2026-09-12,三个括号方便分组取年月日 |
| 匹配邮政编码 | ^\d{6}$ | 固定 6 位 |
| 匹配年份 | ^(19|20)\d{2}$ | 19 或 20 开头的 4 位数 |
| 匹配 4 位验证码 | ^\d{4}$ | 固定 4 位数字 |
| 匹配非负整数 | ^\d+$ | 与"匹配整数"同款 |
| 匹配 QQ 号码 | ^[1-9]\d{4,10}$ | 首位非 0,总长 5~11 位 |
| 匹配含字母和数字的密码 | ^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d]+$ | 必须同时含字母和数字 |
预查(lookahead):
(?=...)表示"后面必须存在某种内容",但它自己不消耗字符。例:相当于安检口——先确认你"带了身份证",再放你过去,身份证本身不收走。本讲只要求认识,不要求独立编写。
2 网络与格式类
| 任务 | 正则表达式 |
|---|---|
| 匹配电子邮箱地址 | ^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$ |
| 匹配 URL | ^(http|https)://[^\s]+$ |
| 匹配 IP 地址 | ^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$ |
| 匹配域名 | ^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$ |
| 匹配 MAC 地址 | ^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$ |
| 匹配十六进制颜色代码 | ^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$ |
| 匹配邮件主题 | ^Subject: .*$ |
3 标签与中文类
| 任务 | 正则表达式 | 说明 |
|---|---|---|
| 匹配 HTML 标签 | <[^>]+> | 一对尖括号及其中的内容 |
| 匹配 XML 标签 | <([a-zA-Z]+)[^>]*>.*?</\1> | \1 反向引用前面的标签名 |
| 匹配中文字符 | [\u4e00-\u9fa5] | 常用汉字的 Unicode 区间 |
| 匹配连续重复的字符 | (.)\1+ | 如 哈哈哈、good 里的 oo |
| 匹配单词边界 | \bword\b | 只匹配独立的 word |
| 匹配替代方案 | \b(replace|substitute)\b | 两个词任一 |
| 匹配句子中的叹号 | ! | 单独一个叹号 |
反向引用:
\1表示"再来一份和第 1 个括号里一模一样的内容"。例:相当于找双胞胎——先记住哥哥长什么样((. )),再要求后面跟着一个跟他一模一样的(\1)。本讲只要求认识。
4 怎么用:校验 vs 抽取
| 场景 | 写法要点 | 例子 |
|---|---|---|
| 校验(判断用户输入合不合法) | 必须带 ^ 和 $ | 验证单个手机号:^1\d{10}$ |
| 抽取(从长文本里批量捞) | 去掉 ^ 和 $ | 从一段话里捞所有手机号:1\d{10} |
5 实操案例
案例①:匹配中国大陆手机号
需求:匹配中国大陆手机号(1 开头共 11 位)。
思路:1 开头 + 10 位数字 = 11 位 → ^1\d{10}$。
import re
s = '13800138000'
m = re.match(r'^1\d{10}$', s)
print(m) # <re.Match object; span=(0, 11), match='13800138000'>| 要点 | 说明 |
|---|
自检
- 匹配中国大陆手机号(1 开头共 11 位)的模式是?A.
^\d{11}$B.^1\d{10}$C.^1\d{11}$D.1\d{10}
答案:B.首位固定为 1,其余 10 位数字,^ $保证整串只有手机号 - 匹配 6 位邮政编码的模式是?A.
^\d{5}$B.^\d{6}$C.\d{6,}D.^[0-9]{6,}$
答案:B.邮政编码固定 6 位数字 - 模式
(.)\1+用来匹配什么?A.任意单个字符 B.连续重复的字符 C.数字开头 D.空白符
答案:B.(.)捕获任意字符,\1反向引用它,要求再出现 1 次以上 - 匹配中文字符的模式是?A.
[\w]B.[\u4e00-\u9fa5]C.[a-zA-Z]D.\s
答案:B.用 Unicode 范围表示常用汉字区间 - 在"提取一段文本中的多个邮箱"时,为什么要去掉
^和$?A.加快匹配速度 B.^ $要求整串完全是一个邮箱,加上就只能匹配单独一个邮箱的整串文本 C.语法不允许 D.防止贪婪匹配
答案:B.校验单个邮箱时用锚点,从长文本里抽取多个邮箱要去掉锚点
6.八大战役:真实文本实战
前言
规则看再多不如真刀真枪打五场。每一场都按"需求 → 正则 → 函数 → 输出"四步走,这四步也是你以后写任何正则的通用套路 👇
1 战役①:提取括号中的数字
需求:从 (888)555-1234 中把括号里的 888 取出来。
思路:888 被夹在 ( 和 ) 之间 → 就匹配"左括号 + 数字 + 右括号",并把数字用括号捕获下来。
import re
s = '(888)555-1234'
m = re.search(r'\((\d+)\)', s)
print(m.group(1)) # 888| 要点 | 说明 |
|---|---|
\( 和 \) | 括号在正则里是元字符(分组),要匹配它本身必须转义 |
(\d+) | 外层 () 是捕获分组,里层 \d+ 是"1 位以上数字" |
用 search 不用 match | 目标不在字符串开头,match 会直接返回 None |
group(1) | 取第 1 个捕获分组的内容 |
2 战役②:提取所有电子邮箱
需求:从 11111@qq.com abcdefg@126.com abc123@163.com 中提取全部邮箱。
import re
text = '11111@qq.com abcdefg@126.com abc123@163.com'
pat = r'\w+(?:[-+.]\w+)*@\w+(?:[-.]\w+)*\.\w+(?:[-.]\w+)*'
print(re.findall(pat, text))
# ['11111@qq.com', 'abcdefg@126.com', 'abc123@163.com']关键点:这里去掉了 ^ 和 $——因为要从一段文本里抽取多个,不是校验单个邮箱是否合法。用 findall 一次拿全部。
3 战役③:提取地名与邮编
需求:从 广州:510000 深圳:518000 佛山:528000 中提取地名和对应邮编。
import re
text = '广州:510000 深圳:518000 佛山:528000 珠海:519000 东莞:523000'
pairs = re.findall(r'([\u4e00-\u9fa5]+):(\d{6})', text)
print(pairs)
# [('广州', '510000'), ('深圳', '518000'), ('佛山', '528000'), ('珠海', '519000'), ('东莞', '523000')]
d = dict(zip([p[0] for p in pairs], [p[1] for p in pairs]))
print(d) # {'广州': '510000', '深圳': '518000, ...}| 要点 | 说明 |
|---|---|
| 两个括号 | findall 有 2 个分组时,返回元组列表 (地名, 邮编) |
[\u4e00-\u9fa5]+ | 1 个以上汉字(地名) |
\d{6} | 恰好 6 位数字(邮编) |
zip | 把两个列表一一配对成字典 |
zip:Python 内置函数,把多个列表"拉链式"一一配对。例:相当于给两排学生牵手配对——第一排第一个牵第二排第一个,依次往后。
4 战役④:提取网页标签中的文本
需求:从 HTML 片段中取出正文文本,剥掉标签。
import re
html = '<div id="content"><h2>科技动态</h2><p>最新研究显示,<a href="/ai">人工智能</a>' \
'在<span class="highlight">医疗诊断</span>领域的准确率已达92%。<br/><img src="pic.jpg" alt="AI应用"/></p></div>'
# 做法一:把所有标签替换成空 → 得到纯文本
text = re.sub(r'<[^>]+>', '', html)
print(text)
# 科技动态最新研究显示,人工智能在医疗诊断领域的准确率已达92%。
# 做法二:只取标签之间的文本(不要标签外的空白)
parts = re.findall(r'>([^<>]+)<', html)
print(parts)
# ['科技动态', '人工智能', '医疗诊断']| 做法 | 正则 | 结果 |
|---|---|---|
| 去标签留全文 | re.sub(r'<[^>]+>', '', html) | 一整段纯文本 |
| 只取标签里的文字 | re.findall(r'>([^<>]+)<', html) | 文字片段列表 |
说明:PPT 上这一页的正则留空,上面两种是工程里最常用的写法。真实项目更推荐用 BeautifulSoup 解析 HTML,正则适合快速处理简单片段。
5 战役⑤:过滤非中文字符
需求:《三国志》文本里混着标点、英文字母、阿拉伯数字,只保留纯中文。
import re
raw = '建安五年(200年),曹操与袁绍战于官渡。Cao Cao vs Yuan Shao.'
pure = re.sub(r'[^\u4e00-\u9fa5]', '', raw)
print(pure)
# 建安五年年曹操与袁绍战于官渡要点:[^...] 是取反——匹配"不是中文的所有字符",再 sub 替换成空字符串,等于只留中文。
6 战役⑥:编写正则表达式来识别和提取网页开发中使用的十六进制颜色代码。这种代码以井号开头,后面跟随 3 位或 6 位的十六进制数字(0‑9,A‑F),用于表示 RGB 颜色值。3 位格式是 6 位格式的简写形式。
输入字符串:
input_text = [
"#ff0000",
"#f00",
"#ABC123",
"#ggg",
"#12345",
"red",
"#FFFFFF",
"#abc"
]
预期输出:`['#ff0000', '#f00', '#ABC123', '#FFFFFF', '#abc']` import re
input_text = [
"#ff0000",
"#f00",
"#ABC123",
"#ggg",
"#12345",
"red",
"#FFFFFF",
"#abc"
]
# ^# 以#开头;[0-9A‑Fa‑f]{3} 3位十六进制;[0-9A‑Fa‑f]{6} 6位十六进制;$字符串结束
pattern = r'^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$'
valid_color = []
for s in input_text:
# fullmatch完整匹配整个字符串
m = re.fullmatch(pattern, s)
if m:
valid_color.append(s)
print(valid_color)7 战役⑦:匹配网络设备 MAC 地址
任务描述:编写正则表达式来验证网络设备的物理地址(MAC 地址)。MAC 地址由 6 组两位的十六进制数组成,每组之间用冒号或连字符分隔。
输入字符串:
input_text = "00:1A:2B:3C:4D:5E 00-1A-2B-3C-4D-5E 00:1A:2B:3C:4D 00:1A:2B:3C:4D:5E:6F GH:IJ:KL:MN:OP:QR"
预期输出:`['00:1A:2B:3C:4D:5E', '00-1A-2B-3C-4D-5E']`import re
input_text = "00:1A:2B:3C:4D:5E 00-1A-2B-3C-4D-5E 00:1A:2B:3C:4D 00:1A:2B:3C:4D:5E:6F GH:IJ:KL:MN:OP:QR"
# [0‑9A‑Fa‑f]{2}两位十六进制;([:-])捕获分隔符;\1引用前面捕获的分隔符,统一冒号或者统一短横线
pattern = r'[0-9A-Fa-f]{2}([:-])[0-9A-Fa-f]{2}\1[0-9A-Fa-f]{2}\1[0-9A-Fa-f]{2}\1[0-9A-Fa-f]{2}\1[0-9A-Fa-f]{2}'
mac_result = []
# finditer迭代全部匹配结果
for item in re.finditer(pattern, input_text):
mac_result.append(item.group())
print(mac_result)8 战役⑧:识别短信验证码
任务描述:编写正则表达式来提取短信或消息末尾的 4 位数字验证码。这种验证码通常位于文本字符串的末尾位置,且恰好为 4 位数字。
输入字符串:
input_text = "1234 5678 9012 123 12345 ABCD 12 34 12.34 0000"
预期输出:`['1234', '5678', '9012', '0000']`import re
input_text = "1234 5678 9012 123 12345 ABCD 12 34 12.34 0000"
# \d{4}匹配4位数字
pattern = r'\d{4}'
result = re.findall(pattern, input_text)
print(result)常见报错排查
| 报错 / 现象 | 原因 | 解决办法 |
|---|---|---|
AttributeError: 'NoneType' object has no attribute 'group' | search / match 没匹配上,返回了 None | 先用 if m: 判断;检查是不是该用 search 而不是 match |
| 匹配到的内容比预期多得多 | 量词默认是贪婪的 | 在量词后加 ? 改非贪婪,如 .*? |
\d 之类没生效,匹配结果为空 | 字符串前忘了加 r,反斜杠被 Python 转义吃掉 | 一律写成 r'\d' 原始字符串 |
想匹配 ( . + 却匹配不上 | 这些是元字符,被当成规则用了 | 加 \ 转义:\( \. \+ |
| 中文匹配不到 | 忘了用 Unicode 区间 | 用 [\u4e00-\u9fa5] |
自检
- 从
"(888)555-1234"中提取括号里的888,正确做法是?A.re.findall()直接取 B.re.search(r'\((\d+)\)',s).group(1)C.re.split()D.re.sub()
答案:B.括号是元字符需转义;(\d+)是捕获分组,group(1) 取第一组 - 从一段文本中提取所有邮箱地址,应选?A.
re.match()B.re.search()C.re.findall()D.re.sub()
答案:C.找全部用 findall,返回邮箱字符串列表 - 用
r'([\u4e00-\u9fa5]+):(\d{6})'配合 findall 提取"广州:510000",返回结果是?A.两个独立列表 B.元组列表[('广州','510000'), ...]C.一个字符串 D.字典
答案:B.模式中有 2 个分组时 findall 返回元组列表,再用 zip 配对成字典 - 把《三国志》文本中所有非中文字符删掉、只留纯中文,应写?A.
re.findall(r'[\u4e00-\u9fa5]',t)B.re.sub(r'[^\u4e00-\u9fa5]','',t)C.re.split(r'[\u4e00-\u9fa5]',t)D.re.match(r'[\u4e00-\u9fa5]',t)
答案:B.[^...]取反,把所有非中文替换成空字符串 - 从网页 HTML 中去掉所有标签、只留正文文本,常用写法是?A.
re.findall(r'<[^>]+>',h)B.re.sub(r'<[^>]+>','',h)C.re.split(r'<',h)D.re.match(r'<.*>',h)
答案:B.<[^>]+>匹配一对尖括号及内容,sub 替换为空即剥离标签
