系列文章
Python 从入门到精通
第 20 / 36 篇
从环境安装和基础语法出发,逐步学习工程实践、自动化、数据处理与 Web 开发。
-
01
Python 怎么装、怎么用 -
02
变量、数字和字符串 -
03
列表:把一组数据放在一起 -
04
元组、集合和字典 -
05
条件判断:让程序做选择 -
06
循环:重复的事交给程序 -
07
函数:把代码整理成可复用的块 -
08
模块与包:拆分你的程序 -
09
输入、输出与字符串格式化 -
10
文件读写:保存程序的数据 -
11
异常处理:程序出错时怎么办 -
12
基础阶段练习:命令行记账本 -
13
类和对象:面向对象入门 -
14
继承、组合与特殊方法 -
15
迭代器与生成器 -
16
列表推导式与生成器表达式 -
17
装饰器:给函数增加能力 -
18
上下文管理器与 with -
19
类型标注与 dataclass -
20
正则表达式:从文本中找规律正在阅读 -
21
日期、时间与时区 -
22
日志与调试 -
23
虚拟环境与依赖管理 -
24
测试:让修改不再提心吊胆 -
25
网络请求:用 Python 调用 API -
26
网页解析与合规采集 -
27
操作 Excel、CSV 与批量文件 -
28
SQLite:给程序加一个数据库 -
29
数据分析入门:NumPy 与 Pandas -
30
画图:把数据变得直观 -
31
Flask 入门:做一个小网站 -
32
异步编程:同时处理多项任务 -
33
线程、进程与并发选择 -
34
性能分析与优化 -
35
项目结构、配置与发布 -
36
综合项目:从需求到上线
处理文本时,最头疼的需求往往是”找规律”:从一堆日志里找出所有 IP、从网页源码里提取链接、验证手机号格式对不对。用字符串方法硬抠会写到怀疑人生。正则表达式(regex)就是干这个的——用一套”模式”语言描述你要找的规律。
第一个正则
Python 的正则在 re 模块里。最简单的用法:re.findall 找出所有匹配:
import re
text = "我的电话是 138-1234-5678,办公室是 010-8888-6666"
phones = re.findall(r"d{3}-d{4}-d{4}", text)
print(phones) # ['138-1234-5678', '010-8888-6666']
注意字符串前面的 r:原始字符串(raw string),告诉 Python”反斜杠别转义”,写正则必须加。
核心语法速查
记住这张表,覆盖 80% 场景:
# 字符类
d # 数字,等价 [0-9]
w # 单词字符:字母、数字、下划线
s # 空白:空格、tab、换行
. # 任意字符(换行除外)
[abc] # a、b、c 中任意一个
[0-9] # 数字区间
[^0-9] # 非数字
# 量词
* # 0 次或多次
+ # 1 次或多次
? # 0 次或 1 次
{3} # 恰好 3 次
{2,4} # 2 到 4 次
# 位置和分组
^ # 字符串开头
$ # 字符串结尾
| # 或(如 cat|dog)
() # 分组,同时能捕获内容
b # 单词边界
import re
text = "南山小站 2026 年开张,有 12 篇文章,3 个系列"
# d+:连续数字
print(re.findall(r"d+", text)) # ['2026', '12', '3']
# d{4}:恰好 4 位数字
print(re.findall(r"d{4}", text)) # ['2026']
# [南小]:任意一个字符
print(re.findall(r"[南小]", text)) # ['南', '小']
# w+:连续单词
print(re.findall(r"w+", text)) # ['南山小站', '2026', ...]
search vs match vs findall
三个最常用的函数,别搞混:
text = "我的邮箱是 hello@example.com,备用是 hi@test.org"
# findall:找所有,返回列表
emails = re.findall(r"[w.]+@[w.]+.w+", text)
print(emails) # ['hello@example.com', 'hi@test.org']
# search:找第一个,返回 Match 对象(找不到返回 None)
m = re.search(r"[w.]+@[w.]+.w+", text)
print(m.group()) # hello@example.com(匹配到的内容)
# match:必须从开头匹配(注意区别!)
m2 = re.match(r"d+", "2026年是南山小站开张年")
print(m2.group()) # 2026(开头就有数字)
m3 = re.match(r"d+", "南山小站2026年开张")
print(m3) # None(开头不是数字,匹配失败)
记法:findall 找全部,search 找第一个(任意位置),match 从开头找。
分组:提取关键部分
用括号分组,能只取需要的部分:
text = "张三 138-1234-5678,李四 139-9999-8888"
pattern = r"(w+) (d{3}-d{4}-d{4})" # 两个分组:名字、电话
for m in re.finditer(pattern, text): # finditer:逐个 Match
name, phone = m.groups() # 取所有分组
print(f"{name}: {phone}")
# 张三: 138-1234-5678
# 李四: 139-9999-8888
# 分组加名字(?P<名字>),大项目可读性好
pattern = r"(?P<name>w+) (?P<phone>d{3}-d{4}-d{4})"
m = re.search(pattern, text)
print(m.group("name")) # 张三
print(m.group("phone")) # 138-1234-5678
sub:替换
text = "联系我:138-1234-5678"
# 手机号打码
masked = re.sub(r"d{3}-d{4}-d{4}", "***-****-****", text)
print(masked) # 联系我:***-****-****
# 去掉所有非数字字符
clean = re.sub(r"D", "", text) # D 非数字
print(clean) # 13812345678
贪婪 vs 非贪婪
默认量词是贪婪的:尽可能多匹配。加 ? 变非贪婪:
text = "<b>加粗</b>和<b>更粗</b>"
# 贪婪:从第一个
print(re.findall(r"<b>.*</b>", text))
# ['<b>加粗</b>和<b>更粗</b>'] —— 一次吃到底!
# 非贪婪:.*? 尽量少匹配
print(re.findall(r"<b>.*?</b>", text))
# ['<b>加粗</b>', '<b>更粗</b>'] —— 这才是想要的
.*? 是最常用的非贪婪组合,提取成对标签时几乎必用。
实战:验证输入 + 提取 URL
import re
# 验证手机号(简单版:1 开头 11 位)
def is_phone(s):
return bool(re.fullmatch(r"1d{10}", s)) # fullmatch:整个字符串都要匹配
print(is_phone("13812345678")) # True
print(is_phone("23812345678")) # False(2 开头)
print(is_phone("1381234567")) # False(10 位)
# 提取所有 URL
text = "官网 https://www.nsn0704y.cn 和文档 http://docs.example.com"
urls = re.findall(r"https?://[w./-]+", text)
print(urls) # ['https://www.nsn0704y.cn', 'http://docs.example.com']
新手坑
坑 1:忘加 r 前缀。"d" 里 d 会被 Python 当转义符处理(d 不是合法转义,旧版本直接报错或行为诡异)。写正则一律 r"..."。
坑 2:用正则解析 HTML/JSON。HTML 不是正则语言,嵌套标签正则搞不定。解析 HTML 用 BeautifulSoup(后面网页解析那篇),解析 JSON 用 json 模块。
坑 3:贪婪匹配吃多了。该用 .*? 用了 .*,结果全匹配成一坨。匹配成对结构时想想非贪婪。
小结
- 正则 = 描述文本规律的模式语言,用
re模块,字符串加r前缀 - 核心语法:
d w s . []字符、* + ? {}量词、^ $ | ()位置和分组 findall找全部、search找第一个、match从头找、fullmatch整串匹配- 分组提取用
()+m.groups();替换用re.sub - 默认贪婪,
.*?非贪婪;别用正则解析 HTML/JSON
练习
- 写正则从 “南山小站成立于2026年8月,位于吉林省敦化市” 里提取所有数字
- 写一个函数
extract_emails(text),返回文本里所有邮箱地址 - 思考:
re.findall(r"<.*>", "<a>b</a>")会返回什么?如果换成<.*?>呢?为什么?