Chapter 01 / 10

正则表达式入门

理解正则表达式的本质、引擎原理、元字符基础与量词使用

什么是正则表达式

正则表达式(Regular Expression,简称 regex 或 regexp)是一种用于描述字符串模式的形式化语言。它让你可以用一个简短的"模式字符串"来匹配、搜索、替换、分割满足该模式的文本。

正则表达式(Regular Expression)
由普通字符和特殊元字符组成的模式字符串,描述满足某种规律的字符串集合。例如 \d+ 描述"一个或多个数字",可以匹配 4200712345 等任何数字序列。
模式(Pattern)
正则表达式本身。每个模式定义了一个规则集合,用于描述某类字符串应具备的结构特征。
匹配(Match)
当一个字符串满足某个正则模式的规则时,称该字符串与模式匹配。匹配可以是整个字符串,也可以是字符串中的某个子串。
元字符(Metacharacter)
在正则表达式中具有特殊含义的字符(如 .*+?^$[]{}()|\),不表示字面字符。

正则表达式能做什么

正则表达式的用途贯穿日常开发的方方面面:

文本搜索与匹配

  • 验证邮箱地址格式
  • 验证手机号、身份证号
  • 检测字符串是否符合日期格式
  • 在日志文件中搜索特定错误

文本提取与替换

  • 从 HTML 中提取链接、图片地址
  • 从日志中提取 IP 地址、时间戳
  • 批量重命名变量(代码重构)
  • 模板渲染(替换占位符)

正则引擎的工作原理

理解正则引擎的工作方式,是避免写出低效或错误正则的基础。

NFA 引擎(Non-deterministic Finite Automaton)
绝大多数现代正则引擎(Python re、JavaScript、Java、.NET、PCRE)采用 NFA 引擎。NFA 引擎是"以正则为主导"的:引擎从模式的第一个元素开始,尝试在文本中找到匹配;当某条路径失败时,引擎回溯(backtrack)并尝试其他路径。NFA 支持捕获组、反向引用、前瞻断言等高级特性,但复杂的模式可能触发指数级回溯。
DFA 引擎(Deterministic Finite Automaton)
DFA 引擎是"以文本为主导"的:同时追踪所有可能的匹配路径,保证线性时间复杂度(不会回溯)。DFA 用于某些命令行工具(如 grep 的默认模式)和正则库(如 Go 的 regexp 包、RE2)。DFA 不支持捕获组和反向引用,但永远不会出现灾难性回溯。

NFA 引擎的匹配过程可以用以下伪代码理解:

# 简化的 NFA 匹配示意(以 /ab*c/ 匹配 "abbc" 为例)
# 模式:a  b*  c
# 文本:a  b  b  c
#
# 步骤1:模式 'a' → 匹配文本[0]='a'  ✓ 前进
# 步骤2:模式 'b*' → 贪婪,吃掉 'bb' → 指针到文本[3]='c'
# 步骤3:模式 'c' → 匹配文本[3]='c'  ✓ 成功!
#
# 如果步骤3失败,引擎会回溯:b* 退还一个 'b',再次尝试

字面字符(Literal Characters)

除了元字符以外,正则中的字符都是字面字符——匹配自身。

/hello/
say hello to the world
字面字符串,精确匹配 "hello"
import re

# 字面匹配,区分大小写
re.search(r'hello', 'say hello to the world')  # 匹配
re.search(r'hello', 'say Hello to the world')  # None!H 大写

# 忽略大小写需要加标志位
re.search(r'hello', 'say Hello to the world', re.IGNORECASE)  # 匹配

点号(.)——最宽泛的元字符

. 匹配除换行符(\n)以外的任意单个字符

/c.t/
cat cut c3t c t ct(不匹配,需要一个字符)
. 匹配 a、u、3、空格等任意字符,但需要恰好一个
# . 匹配任意单个字符(除换行)
re.findall(r'c.t', 'cat cut c3t c\nt')
# → ['cat', 'cut', 'c3t'](c\nt 不匹配,因为 . 不匹配换行)

# 加 re.DOTALL 标志,让 . 也匹配换行
re.findall(r'c.t', 'cat cut c3t c\nt', re.DOTALL)
# → ['cat', 'cut', 'c3t', 'c\nt']
WARNING(点号的常见误用)点号是"任意字符",不是"任意字母"或"任意数字"。/192.168.1.1/ 会匹配 "192X168Y1Z1",因为 . 在这里不是字面点,而是任意字符。要匹配字面点号,需要转义:/192\.168\.1\.1/

元字符与转义

当你需要匹配元字符本身时,在它前面加反斜杠 \ 进行转义:

元字符字面含义匹配示例
.\.3\.14 匹配 "3.14"
*\*a\*b 匹配 "a*b"
+\+a\+b 匹配 "a+b"
?\?done\? 匹配 "done?"
^\^\^abc 匹配 "^abc"
$\$\$100 匹配 "$100"
( )\( \)\(a\) 匹配 "(a)"
[ ]\[ \]\[x\] 匹配 "[x]"
{ }\{ \}\{n\} 匹配 "{n}"
|\|a\|b 匹配 "a|b"
\\\C:\\Users 匹配 "C:\Users"
TIP(Python 原始字符串)在 Python 中,正则表达式的反斜杠会与字符串的反斜杠解析产生冲突。始终使用原始字符串 r"..." 来写正则:r"\d+" 而非 "\\d+"。前者更直观,避免双重转义的混乱。

量词(Quantifiers)

量词指定前面的元素(字符、字符类或分组)重复出现的次数

*(零次或多次)
匹配前面的元素零次或任意多次。ab*c 可以匹配 "ac"(b 出现0次)、"abc"(1次)、"abbc"(2次)等。
+(一次或多次)
匹配前面的元素至少一次。ab+c 只能匹配 "abc"、"abbc",不能匹配 "ac"(b 至少出现1次)。
?(零次或一次)
匹配前面的元素可选地出现一次。colou?r 匹配 "color" 和 "colour"(u 可以有也可以没有)。
{n}(恰好 n 次)
精确匹配 n 次。\d{4} 匹配恰好4位数字,如 "2026",但不匹配 "202" 或 "20261"。
{n,}(至少 n 次)
匹配 n 次或更多次。\d{3,} 匹配3位或更多位数字。
{n,m}(n 到 m 次)
匹配 n 到 m 次(包含两端)。\d{3,5} 匹配3位到5位数字。
量词等价写法含义示例
*{0,}0次或多次go*gle → "ggle", "gogle", "google"
+{1,}1次或多次\d+ → "1", "42", "999"
?{0,1}0或1次(可选)https? → "http" 或 "https"
{3}恰好3次[A-Z]{3} → "USA", "ABC"
{2,4}2到4次\w{2,4} → "hi", "hey", "hello"
# 量词示例
re.findall(r'\d+', '123 abc 45 6789')
# → ['123', '45', '6789']

re.findall(r'\d{3}', '123 4567 89')
# → ['123', '456']  (注意:7 单独不够3位,89 也不够)

re.findall(r'\d{2,3}', '1 22 333 4444')
# → ['22', '333', '444']  (4444 取前3位 '444',贪婪)

# 量词 ? 的典型用法:可选部分
re.findall(r'https?://\S+', 'http://a.com and https://b.com')
# → ['http://a.com', 'https://b.com']

选择(Alternation):| 符号

| 是"或"操作符,匹配左边或右边的模式:

/cat|dog|bird/g
I have a cat and a dog. No bird.
| 的优先级最低,整个左右两侧都是选项
# | 匹配左边或右边
re.findall(r'cat|dog', 'I have a cat and a dog')
# → ['cat', 'dog']

# | 优先级最低——整个左右都是选项
re.findall(r'gray|grey', 'gray or grey?')
# → ['gray', 'grey']

# 用括号限定 | 的作用范围
re.findall(r'gr(a|e)y', 'gray or grey?')
# → ['a', 'e']  (返回的是捕获组内容!)

# 用非捕获组 (?:...) 限定范围但不捕获
re.findall(r'gr(?:a|e)y', 'gray or grey?')
# → ['gray', 'grey']  (返回整个匹配)

锚点(Anchors):位置而非字符

锚点不匹配任何字符,而是匹配字符串中的特定位置

^(行首/字符串开头)
默认匹配整个字符串的开头。开启多行模式(re.MULTILINE)后,匹配每行行首。注意:在字符类 [...] 中,^ 表示"取反",不是锚点。
$(行尾/字符串结尾)
默认匹配整个字符串的结尾(在可能的换行符之前)。开启多行模式后,匹配每行行尾。
\b(单词边界)
匹配单词字符(\w)与非单词字符(\W)之间的位置,或字符串的开头/结尾与单词字符之间的位置。用于精确匹配完整单词,避免匹配到单词的一部分。
\B(非单词边界)
\b 的反义,匹配不在单词边界的位置。例如 \Bcat\B 匹配 "concatenate" 中间的 "cat",但不匹配单独的 "cat"。
/\bcat\b/g
I have a cat. My concatenate function works.
\b 确保只匹配完整的单词 "cat",不匹配 "concatenate" 中的 "cat"
# ^ 和 $ 锚点
re.findall(r'^\d+', '123 abc')     # → ['123'](从开头匹配数字)
re.findall(r'\d+$', 'abc 456')     # → ['456'](到结尾匹配数字)
re.fullmatch(r'\d+', '12345')       # 匹配(整串都是数字)
re.fullmatch(r'\d+', '123abc')      # None(含非数字)

# \b 单词边界
re.findall(r'\bcat\b', 'cat cats concatenate')
# → ['cat'](只有独立的 'cat' 匹配)

re.findall(r'\bcat', 'cat cats concatenate')
# → ['cat', 'cat']('cat' 和 'cats' 中都以单词边界开头)

第一个完整示例:验证日期格式

综合运用字面字符、量词和锚点,写一个日期格式验证器:

import re

# 目标:验证 YYYY-MM-DD 格式
# YYYY:4位数字
# MM:01-12
# DD:01-31
DATE_PATTERN = re.compile(
    r'^\d{4}'            # 年份:4位数字,从开头
    r'-(0[1-9]|1[0-2])'  # 月份:01-09 或 10-12
    r'-(0[1-9]|[12]\d|3[01])'  # 日期:01-09, 10-29, 30-31
    r'$'                 # 到结尾
)

def is_valid_date(s):
    return bool(DATE_PATTERN.fullmatch(s))

is_valid_date('2026-03-26')   # True
is_valid_date('2026-13-01')   # False(月份超范围)
is_valid_date('2026-03-32')   # False(日期超范围)
is_valid_date('26-3-1')       # False(格式不对)
is_valid_date('2026-03-26 extra')  # False(后面有多余内容)
INFO(正则的局限性)正则表达式验证格式,但不验证语义。上面的模式会接受 "2026-02-31"(2月没有31天),但这种语义验证需要代码逻辑处理。正则表达式是字符串模式工具,不是日期解析器。

Python 与 JavaScript 语法对比

特性PythonJavaScript
字面量语法无(必须用字符串)/pattern/flags
使用字符串r"pattern"(推荐)new RegExp("pattern", "flags")
预编译re.compile(r"pattern")/pattern/(自动)
标志位re.I | re.M 等常量末尾字母 gigim
搜索首个re.search()str.match()(无 g)
搜索所有re.findall()str.match(/re/g)
替换re.sub()str.replace()
反向引用\1\g<name>$1$<name>

小结

本章要点