正则表达式与模式匹配
第一部分:正则表达式(Regular Expression)详解
1. 什么是正则表达式?
正则表达式是一种描述字符模式的对象。可以把它想象成一种极其强大的“查找和替换”工具,它不是用固定的字符(如 “abc”)去查找,而是用一种描述性的语言(如 “查找三个数字”)去匹配一系列符合某个句法规则的字符串。
它的核心用途包括:
- 数据验证:检查输入的数据是否符合某种格式(如邮箱、手机号、身份证号)。
- 文本搜索与定位:在大量文本中快速找到符合特定模式的内容。
- 文本提取:从一段文本中抽取出需要的信息(如网页爬虫中提取标题和链接)。
- 文本替换:将符合模式的文本替换成其他内容。
2. 核心组成元素(元字符 Metacharacters)
正则表达式的威力来自于它的特殊字符——元字符。普通字符(如 a, b, 1, 2)在正则中就是匹配它们自身,而元字符则有特殊的含义。
2.1 基础字符与预定义字符集
| 元字符 | 描述 | 示例 |
|---|---|---|
. |
匹配除换行符 \n 之外的任意单个字符 |
a.c 匹配 “abc”, “a_c”, “a2c” |
\d |
匹配任意一个数字(等价于 [0-9]) |
\d{3} 匹配 “123” |
\D |
匹配任意一个非数字字符 | \D 匹配 “a”, “!”, “ “ |
\w |
匹配任意一个字母、数字或下划线(等价于 [a-zA-Z0-9_]) |
\w+ 匹配 “hello_123” |
\W |
匹配任意一个非字母、数字或下划线 | \W 匹配 “ “, “!”, “?” |
\s |
匹配任意一个空白字符(包括空格、制表符\t、换行符\n等) |
hello\sworld 匹配 “hello world” |
\S |
匹配任意一个非空白字符 | \S+ 匹配 “non-space” |
2.2 量词(Quantifiers)
量词用来指定一个模式需要重复出现的次数。
| 元字符 | 描述 | 示例 |
|---|---|---|
* |
匹配前面的元素 0 次或多次(任意次数) | go*gle 匹配 “ggle”, “google”, “goooogle” |
+ |
匹配前面的元素 1 次或多次(至少一次) | go+gle 匹配 “google”, “goooogle”, 但不匹配 “ggle” |
? |
匹配前面的元素 0 次或 1 次(最多一次) | colou?r 匹配 “color” 和 “colour” |
{n} |
匹配前面的元素恰好 n 次 | \d{5} 匹配一个五位数的邮编 “10086” |
{n,} |
匹配前面的元素至少 n 次 | \d{5,} 匹配五位或更长的数字 |
{n,m} |
匹配前面的元素 n 到 m 次 | \d{5,8} 匹配五到八位的数字 |
贪婪模式 vs. 懒惰模式
默认情况下,量词是贪婪的(Greedy),即尽可能多地匹配字符。在量词后面加上一个 ? 可以使其变为懒惰的(Lazy),即尽可能少地匹配字符。
- 示例:对于字符串
"<h1>Title</h1>"- 贪婪模式:
<.*>会匹配整个字符串"<h1>Title</h1>"。 - 懒惰模式:
<.*?>会先匹配到"<h1>",因为它找到了第一个>就停止了,满足了“尽可能少”的原则。
- 贪婪模式:
2.3 边界匹配(Anchors)
边界匹配符用于定位模式在字符串中的位置,它们不匹配任何字符,只匹配位置。
| 元字符 | 描述 | 示例 |
|---|---|---|
^ |
匹配字符串的开始位置 | ^Hello 匹配以 “Hello” 开头的字符串 |
$ |
匹配字符串的结束位置 | world$ 匹配以 “world” 结尾的字符串 |
\b |
匹配一个单词边界(单词的开始或结束位置) | \bcat\b 匹配独立的单词 “cat”,但不匹配 “category” 中的 “cat” |
\B |
匹配一个非单词边界 | \Bcat\B 匹配 “category” 中的 “cat” |
2.4 分组与捕获(Grouping and Capturing)
| 元字符 | 描述 | 示例 |
|---|---|---|
(...) |
1. 分组:将多个字符作为一个单元,可以对这个单元使用量词。 2. 捕获:匹配的内容会被捕获,以便后续引用或提取。 |
(ab)+ 匹配 “ab”, “abab”, “ababab”(\d{4})-(\d{2})-(\d{2}) 捕获年、月、日 |
(?:...) |
非捕获分组:只分组,不捕获匹配的内容。效率略高。 | (?:https?://) 匹配 “http://“ 或 “https://“ 但不捕获它 |
| |
或(OR):匹配 ` | ` 左边或右边的表达式。 |
[...] |
字符集:匹配方括号中的任意一个字符。 | [aeiou] 匹配任意一个元音字母 |
[^...] |
排除型字符集:匹配不在方括号中的任意一个字符。 | [^0-9] 匹配任意非数字字符 |
[a-z] |
范围:匹配指定范围内的任意一个字符。 | [a-zA-Z0-9] 匹配任意字母或数字 |
2.5 高级概念:断言(Lookaround)
断言(也叫环视或零宽断言)是一种特殊的边界匹配,它只进行位置匹配,不消耗任何字符。
| 元字符 | 类型 | 描述 | 示例 |
|---|---|---|---|
(?=...) |
正向先行断言 | 匹配…之前的位置,要求该位置后面能匹配… | `windows(?=95 |
(?!...) |
负向先行断言 | 匹配…之前的位置,要求该位置后面不能匹配… | \d{3}(?!\d) 匹配一个三位数,但前提是它后面没有跟着另一个数字 |
(?<=...) |
正向后行断言 | 匹配…之后的位置,要求该位置前面能匹配… | (?<=\$)\d+ 匹配一个数字,但仅当其前面是”$”符号时(如 “$100” 中的 “100”) |
(?<!...) |
负向后行断言 | 匹配…之后的位置,要求该位置前面不能匹配… | (?<!-)\d+ 匹配一个数字,但仅当其前面不是连字符”-“时 |
2.6 转义字符
如果你想匹配元字符本身(如 .、*、(),你需要在它前面加上反斜杠 \ 进行转义。
\.匹配一个真正的点号.。\*匹配一个真正的星号*。\\匹配一个真正的反斜杠\。
第二部分:Python 的 re 库用法
Python 通过内置的 re 模块来支持正则表达式。
1. 核心函数与方法
使用 re 库通常有两种方式:
- 函数式:直接调用
re模块的函数,如re.search(pattern, string)。适合一次性的操作。 - 对象式:先用
re.compile(pattern)将正则表达式编译成一个模式对象,然后调用该对象的方法,如pattern.search(string)。推荐在需要多次使用同一个正则表达式时使用,因为编译可以节省时间,提高效率。
1.1 re.match(pattern, string, flags=0)
- 功能:从字符串的起始位置开始匹配。如果起始位置不匹配,则返回
None。 - 返回值:一个匹配对象(Match Object),或者
None。
1 | import re |
注意:
r"..."是 Python 的原始字符串(raw string),它可以防止反斜杠被解释为转义字符,在写正则表达式时几乎是必需的。
1.2 re.search(pattern, string, flags=0)
- 功能:扫描整个字符串,找到第一个匹配的子串。
- 返回值:一个匹配对象(Match Object),或者
None。这是最常用的查找函数。
1 | text = "hello world" |
1.3 re.findall(pattern, string, flags=0)
- 功能:查找字符串中所有与模式匹配的非重叠子串。
- 返回值:一个列表。
- 如果模式中没有捕获组,返回所有匹配的字符串组成的列表。
- 如果模式中有捕获组,返回一个由元组组成的列表,每个元组包含各捕获组匹配的内容。
1 | text = "I have 2 apples and 3 bananas." |
1.4 re.finditer(pattern, string, flags=0)
- 功能:与
findall类似,但返回一个迭代器(iterator),迭代器中的每个元素都是一个匹配对象。 - 优点:当匹配结果非常多时,使用迭代器比一次性生成一个大列表更节省内存。
1 | text = "Event at 2023-10-26, and another at 2024-01-01." |
1.5 re.sub(pattern, repl, string, count=0, flags=0)
- 功能:替换(Substitute)。查找所有匹配的子串,并用
repl替换它们。 repl可以是字符串,也可以是一个函数。count指定最大替换次数。
1 | text = "My phone is 123-456-7890." |
1.6 re.split(pattern, string, maxsplit=0, flags=0)
- 功能:使用正则表达式作为分隔符来分割字符串。比
str.split()更强大。
1 | text = "apple, pear; orange|banana" |
1.7 re.compile(pattern, flags=0)
- 功能:将正则表达式字符串编译成一个模式对象,以便复用。
1 | # 编译模式 |
1.8 re.fullmatch(pattern, string, flags=0)
- 功能:
re.fullmatch会尝试将正则表达式模式应用到整个字符串上。只有当整个字符串从头到尾都能够与模式完全匹配时,它才会返回一个匹配对象(Match Object),否则返回None。
1.8.1 与 re.match 和 re.search 的关键区别
| 函数 | 匹配行为 | 描述 |
|---|---|---|
re.match() |
从字符串开头匹配 | 只要字符串的开头部分能匹配模式即可,不关心字符串的剩余部分。 |
re.search() |
在字符串中任意位置匹配(找到第一个) | 扫描整个字符串,只要找到任何一个子串能匹配模式就立即返回。 |
re.fullmatch() |
必须匹配整个字符串 | 整个字符串,从第一个字符到最后一个字符,都必须能匹配模式。 |
一个直观的类比:
re.match(pattern, string)相当于re.search(f'^{pattern}', string)re.fullmatch(pattern, string)相当于re.search(f'^{pattern}$', string)或者re.match(f'{pattern}$', string)
这个类比清晰地展示了 fullmatch 的本质:它隐式地包含了开头 ^ 和结尾 $ 的锚定效果。
1.8.2 代码示例对比
让我们用同一个字符串和不同的模式来感受它们的差异。
1 | import re |
输出结果:
1 | Pattern: 'user' |
1.8.3 主要应用场景
re.fullmatch 的主要用途是数据验证(Validation)。
当你需要确保一个输入(比如用户提交的表单字段、配置文件中的一行等)严格地、完整地符合特定格式,不多不少时,re.fullmatch 是最直接、最清晰的选择。
常见用例:
验证用户名:要求用户名只能是 6-12 位的字母和数字组合。
1
2
3
4
5username = "my_user123"
if re.fullmatch(r"[a-zA-Z0-9]{6,12}", username):
print("Username is valid.")
else:
print("Username is invalid.")如果用
re.match,"my_user123!!!"也会被认为是合法的,因为它只检查了开头部分。验证手机号码:
1
2
3
4
5phone = "18812345678"
if re.fullmatch(r"1[3-9]\d{9}", phone):
print("Phone number is valid.")
else:
print("Phone number is invalid.")验证身份证号:
1
2
3
4
5id_card = "44010119900101123X"
if re.fullmatch(r"\d{17}[\dXx]", id_card):
print("ID card format is valid.")
else:
print("ID card format is invalid.")
在 re.fullmatch 出现之前,为了实现同样的效果,开发者通常需要手动在模式的开头和结尾加上 ^ 和 $,然后使用 re.match,例如 re.match(r"^{pattern}$", string)。re.fullmatch 提供了更语义化、更易读的写法。
2. 匹配对象(Match Object)
当 match() 或 search() 成功时,它们返回一个匹配对象。这个对象包含了匹配的详细信息,非常有用。
match.group(0)或match.group(): 返回整个匹配的字符串。match.group(n): 返回第 n 个捕获组匹配的字符串。match.groups(): 返回一个包含所有捕获组匹配内容的元组。match.start(): 返回匹配的开始位置索引。match.end(): 返回匹配的结束位置索引。match.span(): 返回一个包含(start, end)的元组。
1 | text = "Date: 2023-10-26" |
3. 编译标志(Flags)
在调用 re 函数时,可以通过 flags 参数改变正则表达式的行为。
re.IGNORECASE或re.I: 忽略大小写匹配。re.MULTILINE或re.M: 多行模式。^和$会匹配每一行的开头和结尾,而不仅仅是整个字符串的开头和结尾。re.DOTALL或re.S: 使.能够匹配包括换行符在内的所有字符。re.VERBOSE或re.X: 详细模式。允许你在正则表达式中添加空白和注释,使其更具可读性。
1 | # VERBOSE 模式示例 |
总结
- 使用原始字符串:始终使用
r"..."来定义你的正则表达式模式。 - 编译你的模式:如果一个模式需要被多次使用,使用
re.compile()来提高性能。 - 善用工具:使用在线正则表达式测试工具(如 regex101.com, regexr.com)可以极大地帮助你调试和理解正则表达式。这些工具可以实时显示匹配结果,并对你的模式进行详细解释。
- 理解贪婪与懒惰:这是许多正则表达式错误的根源。明确你想要的是“最多”匹配还是“最少”匹配。
- 优先使用
search:除非你确定你的模式必须从字符串的开头匹配,否则re.search()通常比re.match()更有用。











