第一部分:正则表达式(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 库通常有两种方式:

  1. 函数式:直接调用 re 模块的函数,如 re.search(pattern, string)。适合一次性的操作。
  2. 对象式:先用 re.compile(pattern) 将正则表达式编译成一个模式对象,然后调用该对象的方法,如 pattern.search(string)推荐在需要多次使用同一个正则表达式时使用,因为编译可以节省时间,提高效率。

1.1 re.match(pattern, string, flags=0)

  • 功能:从字符串的起始位置开始匹配。如果起始位置不匹配,则返回 None
  • 返回值:一个匹配对象(Match Object),或者 None
1
2
3
4
5
6
7
8
9
10
import re

text = "hello world"
# 从开头匹配 'hello'
match_obj = re.match(r"hello", text)
print(match_obj) # <re.Match object; span=(0, 5), match='hello'>

# 尝试匹配 'world',因为不在开头,所以失败
match_obj = re.match(r"world", text)
print(match_obj) # None

注意r"..." 是 Python 的原始字符串(raw string),它可以防止反斜杠被解释为转义字符,在写正则表达式时几乎是必需的。

1.2 re.search(pattern, string, flags=0)

  • 功能:扫描整个字符串,找到第一个匹配的子串。
  • 返回值:一个匹配对象(Match Object),或者 None。这是最常用的查找函数。
1
2
3
4
text = "hello world"
# 可以在字符串的任何位置找到 'world'
match_obj = re.search(r"world", text)
print(match_obj) # <re.Match object; span=(6, 11), match='world'>

1.3 re.findall(pattern, string, flags=0)

  • 功能:查找字符串中所有与模式匹配的非重叠子串。
  • 返回值:一个列表。
    • 如果模式中没有捕获组,返回所有匹配的字符串组成的列表。
    • 如果模式中捕获组,返回一个由元组组成的列表,每个元组包含各捕获组匹配的内容。
1
2
3
4
5
6
7
8
text = "I have 2 apples and 3 bananas."
# 无捕获组
numbers = re.findall(r"\d+", text)
print(numbers) # ['2', '3']

# 有捕获组
items = re.findall(r"(\d+) (\w+)", text)
print(items) # [('2', 'apples'), ('3', 'bananas')]

1.4 re.finditer(pattern, string, flags=0)

  • 功能:与 findall 类似,但返回一个迭代器(iterator),迭代器中的每个元素都是一个匹配对象。
  • 优点:当匹配结果非常多时,使用迭代器比一次性生成一个大列表更节省内存。
1
2
3
4
text = "Event at 2023-10-26, and another at 2024-01-01."
iterator = re.finditer(r"(\d{4})-(\d{2})-(\d{2})", text)
for match in iterator:
print(f"Full match: {match.group(0)}, Year: {match.group(1)}, Month: {match.group(2)}")

1.5 re.sub(pattern, repl, string, count=0, flags=0)

  • 功能:替换(Substitute)。查找所有匹配的子串,并用 repl 替换它们。
  • repl 可以是字符串,也可以是一个函数。
  • count 指定最大替换次数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
text = "My phone is 123-456-7890."
# 简单的字符串替换
hidden_phone = re.sub(r"\d", "*", text)
print(hidden_phone) # My phone is ***-***-****.

# 使用捕获组进行高级替换 (\g<name> or \1)
formatted_phone = re.sub(r"(\d{3})-(\d{3})-(\d{4})", r"(\1) \2-\3", text)
print(formatted_phone) # My phone is (123) 456-7890.

# 使用函数进行替换
def add_one(match):
number = int(match.group(0))
return str(number + 1)

text = "The score is 99."
new_text = re.sub(r"\d+", add_one, text)
print(new_text) # The score is 100.

1.6 re.split(pattern, string, maxsplit=0, flags=0)

  • 功能:使用正则表达式作为分隔符来分割字符串。比 str.split() 更强大。
1
2
3
4
text = "apple, pear; orange|banana"
# 可以用多种分隔符分割
parts = re.split(r"[,;|]\s*", text)
print(parts) # ['apple', 'pear', 'orange', 'banana']

1.7 re.compile(pattern, flags=0)

  • 功能:将正则表达式字符串编译成一个模式对象,以便复用。
1
2
3
4
5
6
7
8
9
# 编译模式
email_pattern = re.compile(r"[\w.-]+@[\w.-]+\.\w+")

# 使用模式对象
text1 = "Contact me at test@example.com"
text2 = "Invalid address: user@.com"

print(email_pattern.search(text1)) # <re.Match object ...>
print(email_pattern.search(text2)) # None

1.8 re.fullmatch(pattern, string, flags=0)

  • 功能re.fullmatch 会尝试将正则表达式模式应用到整个字符串上。只有当整个字符串从头到尾都能够与模式完全匹配时,它才会返回一个匹配对象(Match Object),否则返回 None
1.8.1 与 re.matchre.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import re

text = "user@example.com"

# 模式 1: 'user'
pattern1 = r"user"
print(f"Pattern: '{pattern1}'")
print("re.match(): ", re.match(pattern1, text)) # 成功,因为字符串以 'user' 开头
print("re.search(): ", re.search(pattern1, text)) # 成功,因为字符串中包含 'user'
print("re.fullmatch():", re.fullmatch(pattern1, text)) # 失败 (None),因为 'user' 不等于整个字符串

print("-" * 30)

# 模式 2: 一个完整的邮箱模式
pattern2 = r"[\w.-]+@[\w.-]+\.\w+"
print(f"Pattern: '{pattern2}'")
print("re.match(): ", re.match(pattern2, text)) # 成功,因为整个字符串从头开始就是一个邮箱
print("re.search(): ", re.search(pattern2, text)) # 成功,因为整个字符串就是一个邮箱
print("re.fullmatch():", re.fullmatch(pattern2, text)) # 成功,因为整个字符串完美匹配了邮箱模式

print("-" * 30)

# 另一个例子,包含额外字符
text2 = "username: test"

pattern3 = r"username"
print(f"Text: '{text2}', Pattern: '{pattern3}'")
print("re.match(): ", re.match(pattern3, text2)) # 成功
print("re.fullmatch():", re.fullmatch(pattern3, text2)) # 失败 (None),因为字符串末尾还有 ": test"

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
Pattern: 'user'
re.match(): <re.Match object; span=(0, 4), match='user'>
re.search(): <re.Match object; span=(0, 4), match='user'>
re.fullmatch(): None
------------------------------
Pattern: '[\w.-]+@[\w.-]+\.\w+'
re.match(): <re.Match object; span=(0, 16), match='user@example.com'>
re.search(): <re.Match object; span=(0, 16), match='user@example.com'>
re.fullmatch(): <re.Match object; span=(0, 16), match='user@example.com'>
------------------------------
Text: 'username: test', Pattern: 'username'
re.match(): <re.Match object; span=(0, 8), match='username'>
re.fullmatch(): None
1.8.3 主要应用场景

re.fullmatch 的主要用途是数据验证(Validation)

当你需要确保一个输入(比如用户提交的表单字段、配置文件中的一行等)严格地、完整地符合特定格式,不多不少时,re.fullmatch 是最直接、最清晰的选择。

常见用例:

  • 验证用户名:要求用户名只能是 6-12 位的字母和数字组合。

    1
    2
    3
    4
    5
    username = "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
    5
    phone = "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
    5
    id_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
2
3
4
5
6
7
8
9
10
11
text = "Date: 2023-10-26"
match = re.search(r"(\d{4})-(\d{2})-(\d{2})", text)

if match:
print("整个匹配:", match.group(0)) # 2023-10-26
print("第一个捕获组 (年):", match.group(1)) # 2023
print("第二个捕获组 (月):", match.group(2)) # 10
print("所有捕获组:", match.groups()) # ('2023', '10', '26')
print("开始位置:", match.start()) # 6
print("结束位置:", match.end()) # 16
print("起止位置:", match.span()) # (6, 16)

3. 编译标志(Flags)

在调用 re 函数时,可以通过 flags 参数改变正则表达式的行为。

  • re.IGNORECASEre.I: 忽略大小写匹配。
  • re.MULTILINEre.M: 多行模式。^$ 会匹配每一行的开头和结尾,而不仅仅是整个字符串的开头和结尾。
  • re.DOTALLre.S: 使 . 能够匹配包括换行符在内的所有字符。
  • re.VERBOSEre.X: 详细模式。允许你在正则表达式中添加空白和注释,使其更具可读性。
1
2
3
4
5
6
7
8
9
10
11
# VERBOSE 模式示例
pattern = re.compile(r"""
^(\d{4}) # 捕获年份
- # 分隔符
(\d{2}) # 捕获月份
- # 分隔符
(\d{2}) # 捕获日期
$
""", re.VERBOSE)

print(pattern.match("2023-10-26"))

总结

  1. 使用原始字符串:始终使用 r"..." 来定义你的正则表达式模式。
  2. 编译你的模式:如果一个模式需要被多次使用,使用 re.compile() 来提高性能。
  3. 善用工具:使用在线正则表达式测试工具(如 regex101.com, regexr.com)可以极大地帮助你调试和理解正则表达式。这些工具可以实时显示匹配结果,并对你的模式进行详细解释。
  4. 理解贪婪与懒惰:这是许多正则表达式错误的根源。明确你想要的是“最多”匹配还是“最少”匹配。
  5. 优先使用 search:除非你确定你的模式必须从字符串的开头匹配,否则 re.search() 通常比 re.match() 更有用。