正则表达式适合处理有明确局部模式的文本,例如日志字段或简单输入格式。遇到嵌套结构、复杂语法和需要解释上下文的内容,专门的解析器通常更合适。下面从常用模式讲起,再讨论可读性、边界条件和性能。
前置阅读:建议先读 Linux + Python 环境基础。配合 数据采集与爬虫 食用更佳。
核心语法:十分钟速通
正则只有四类构件,记住这张表就有了 80%:
| 构件 | 例子 | 含义 |
|---|---|---|
| 字符类 | \d \w \s [0-9a-f] [^x] | 数字/单词字符/空白/自定义集/取反 |
| 量词 | * + ? {3} {2,5} | 0+/1+/0-1/恰好 n/n 到 m 次 |
| 锚点 | ^ $ \b | 行首/行尾/单词边界 |
| 分组 | (abc) (?:abc) (?P<name>...) | 捕获/不捕获/命名捕获 |
Python 侧三个函数分工:
import re
re.search(r"\d{4}-\d{2}-\d{2}", text) # 找第一个匹配,返回 Match 或 None
re.findall(r"\d+", text) # 所有匹配,返回列表
re.sub(r"\s+", " ", text) # 替换:多空格压成一个
m = re.search(r"(?P<ip>\d+\.\d+\.\d+\.\d+)", log)
m.group("ip") # 命名分组按名取值
纪律:pattern 一律用原始字符串 r"..."——否则 \d 要先过 Python 字符串转义一关,写成 "\\d",双反斜杠地狱就是这么来的。
贪婪与懒惰:最多人栽的坑
量词默认贪婪——能吞多少吞多少,再回溯吐出来。解析 HTML 时的经典事故:
html = "<p>第一段</p><p>第二段</p>"
re.findall(r"<p>.*</p>", html)
# ['<p>第一段</p><p>第二段</p>'] ← .* 一路吞到行尾再回溯
re.findall(r"<p>.*?</p>", html)
# ['<p>第一段</p>', '<p>第二段</p>'] ← ? 让量词变懒惰,能少则少
口诀:匹配成对包裹的内容(引号、标签、括号),量词后加 ?。这个 .*? 组合是实战使用率第一的模式。
分组捕获与替换:数据清洗的主力
# 日志行提取:IP、时间、请求路径
pattern = r'(?P<ip>\S+) - - \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+)'
for m in re.finditer(pattern, log_text):
records.append(m.groupdict())
# 后向引用:把 "2026/08/30" 统一成 "2026-08-30"
re.sub(r"(\d{4})/(\d{2})/(\d{2})", r"\1-\2-\3", text)
# 手机号为脱敏:138****5678
re.sub(r"(\d{3})\d{4}(\d{4})", r"\1****\2", phone_text)
\1 \2 引用前面捕获组的内容——查找和替换联动,清洗类需求的核心技巧。
三大实战场景
场景一:日志清洗进 DataFrame
import pandas as pd
def parse_nginx_log(path):
pattern = re.compile(
r'(?P<ip>\S+) - - \[(?P<time>[^\]]+)\] '
r'"(?P<method>\S+) (?P<path>\S+) [^"]*" '
r'(?P<status>\d{3}) (?P<size>\d+)')
with open(path) as f:
rows = [m.groupdict() for line in f if (m := pattern.match(line))]
return pd.DataFrame(rows)
df = parse_nginx_log("access.log")
df["status"].value_counts() # 秒出状态码分布
要点:先 compile 复用 pattern(循环百万次时编译一次省 30%+);用 match 锚定行首,不匹配的脏行自动跳过——脏数据现实存在,让正则以「匹配不上就丢弃」的方式天然容错。
场景二:爬虫数据提取
# 从 HTML 片段抽价格和标题(简单页面正则够用,复杂的用 BeautifulSoup)
price = re.search(r'¥\s*(\d+(?:\.\d{2})?)', html).group(1)
# 提取所有链接中的商品 ID
ids = re.findall(r'/item/(\d+)\.html', html)
爬虫篇的忠告再重复一次:正则适合「格式机械、结构简单」的提取;嵌套 HTML 请上解析器——正则解析 HTML 是圈内著名的梗,因为 HTML 不是正则语言,嵌套结构正则表达不了。
场景三:输入校验与文本归一化
# 校验(够用版,别追求 RFC 完美)
EMAIL = r"^[\w.+-]+@[\w-]+\.[\w.]+$"
PHONE_CN = r"^1[3-9]\d{9}$"
# 归一化:全角转半角、压缩空白、去零宽字符
def normalize(text):
text = re.sub(r"[]", "", text) # 零宽字符,数据污染的隐形杀手
return re.sub(r"\s+", " ", text).strip()
零宽字符那个坑值得单独说:从网页/Word 复制的文本常夹带 U+200B 等不可见字符,导致「看起来一样的两个字符串不相等」,排查能排一下午。清洗管道里加这一条,一劳永逸。
进阶四件套
- 预编译:
re.compile()循环外一次编译。 - 非捕获组
(?:...):只需要分组逻辑(如|分支)不需要捕获时用它,快且干净。 - 零宽断言:
(?<=¥)\d+匹配 ¥ 后面的数字但不含 ¥(后行断言);\d+(?=元)匹配「元」前的数字(先行断言)——取内容不带定界符的优雅方式。 - verbose 模式:长 pattern 加注释和换行,可读性救星:
pattern = re.compile(r"""
(?P<year>\d{4}) # 年
-
(?P<month>\d{2}) # 月
-
(?P<day>\d{2}) # 日
""", re.VERBOSE)
什么时候放下正则
- 嵌套结构(HTML/JSON/代码)→ 用解析器(BeautifulSoup/json/ast)
- 正则嵌套五层还表达不清 → 写成分步的多行代码,可读性优先
- 性能敏感的超高频调用 → 简单字符串方法(
in、startswith、str.split)比正则快一个量级,先试简单方法 - 灾难性回溯:
(a+)+$这类嵌套量词遇上长不匹配串会指数级爆炸——线上曾因此打满 CPU。警惕「量词套量词」。
踩坑排查
| 现象 | 原因 | 解法 |
|---|---|---|
| 匹配结果比预期长 | 贪婪量词 | 量词后加 ? 变懒惰 |
pattern 里有 \d 却不匹配数字 | 忘了 r"" 前缀 | 原始字符串 |
| 中文内容匹配不上 | \w 默认含中文(Python3),但 \b 对中文失效 | 中文边界用断言手动构造 |
| findall 返回元组不是字符串 | pattern 里有捕获组 | 改非捕获组 (?:…) 或取 groupdict |
| 某些行匹配不到被静默丢弃 | 字段含换行/格式变异 | . 不匹配换行需 re.S;抽样检查未匹配行 |
| 线上 CPU 被正则打满 | 灾难性回溯 | 消除嵌套量词,加超时或换字符串方法 |
练习
- 写正则提取「2026年8月30日」「2026-08-30」「08/30/2026」三种日期并统一成 ISO 格式。
- 解析一段模拟 Nginx 日志 1 万行进 DataFrame,统计 top10 请求路径和 404 率。
- 实现手机号/邮箱/身份证三合一校验器,各准备 5 个正反用例测试。
- 写一个会触发灾难性回溯的 pattern,用
time测量并感受指数爆炸,然后修复它。
面试常问
Q:贪婪和懒惰匹配的区别?
贪婪量词(*、+)先尽量多匹配再回溯;加 ? 变懒惰,先尽量少匹配再逐步扩展。默认贪婪是因为 NFA 引擎的实现顺序。工程影响:解析成对定界符时必须用懒惰或排除字符类([^>]*),否则跨越边界误吞。
Q:\b 是什么?为什么对中文不好使?
单词边界——\w 与 \W 之间的零宽位置。Python3 里中文字符属于 \w,所以「今天天气好」整句是一个「单词」,中间没有边界。中文分词边界要靠显式构造(如用标点/空白做断言),或者上 jieba 分词而不是正则。
Q:正则的性能陷阱有哪些?
① 灾难性回溯:嵌套量词在失败路径上指数爆炸((a+)+b 对 aaaa...a);② 超长文本上无锚点全文扫描;③ 循环内重复编译。防御:消除嵌套量词、用占有量词/原子组(Python3.11+ 支持 *+)、预编译、必要时设超时(regex 三方库支持 timeout)。
Q:findall 在有捕获组时行为?
无捕获组返回整个匹配的列表;有一个捕获组返回该组内容列表;多个捕获组返回元组列表。这是 findall 最让人困惑的设计——不想要捕获就用 (?:...),要完整信息用 finditer + groupdict()。
Q:什么时候用字符串方法代替正则?
固定子串判断(in)、前缀后缀(startswith/endswith)、按固定分隔符切分(split)——这些场景字符串方法快 5~10 倍且零心智负担。正则的价值在「模式」而非「固定串」:变长、可选、字符类、重复。
相关阅读
- 数据采集与爬虫实战——正则的第一战场
- Python 与 NumPy 性能工程——向量化文本处理(str.extract)提速
- SQL 与数据库实战——入库后的模式匹配(LIKE/REGEXP)
- NLP 综合指南——正则搞不定时,语言模型接手
正则是典型的「二十分钟入门,用十年精进」的工具。掌握本篇的模式库 + 知道何时收手,你就是团队里那个「清洗文本最快的人」。
💬 评论
这套方案在哪一步卡住了?也欢迎补充你的实现方式或踩坑经验。