你是否曾想过如何在几百个文件中,快速找到所有格式为 xxx-xxxx-xxxx 的电话号码?或者从一堆乱七八糟的网页代码中,精准地提取出所有的链接和邮箱地址?
在 Python 中,我们主要通过内置的 re 模块来使用正则表达式。
参考文章:
Python 正则表达式 | 简单一点学习 easyeasy.me
1. 旅程的开始:re.search() 与第一个匹配
学习新东西,最好的方式就是直接上手。我们先来看最常用的一个函数:re.search()。它的作用是在一个字符串中搜索第一个符合规则的匹配项。
import re
# 要搜索的文本
text = "Hello, my number is 123-456-7890. Call me tomorrow!"
# 我们要找的“规则”(正则表达式)
pattern = "number"
# 开始搜索!
match = re.search(pattern, text)
# 如果找到了,match 就不是 None
if match:
print("太棒了,找到了!")
print(f"匹配到的内容是: '{match.group()}'")
print(f"它从索引 {match.start()} 开始,到索引 {match.end()} 结束。")
else:
print("唉,啥也没找到。")
在上面的例子里,我们的 pattern 只是一个普通的字符串 "number"。re.search() 成功地在 text 中找到了它。
如果找到了匹配项,re.search() 会返回一个“匹配对象”(Match Object),我们可以从这个对象里获取很多有用的信息,比如匹配到的具体内容(.group())和它的位置(.start(), .end())。如果找不到,就返回 None。
2. 解锁魔法:元字符(Metacharacters)
如果正则表达式只能查找固定文本,那就太无聊了。它的真正威力在于元字符——一些具有特殊含义的字符,它们能让你的匹配规则变得灵活又强大。
2.1. 匹配单个字符
| 元字符 | 含义 | 举例 |
|
| 匹配除换行符外的任意单个字符 |
|
|
| 匹配任意一个数字 (0-9) |
|
|
| 匹配任意一个非数字 |
|
|
| 匹配单词字符 (字母、数字、下划线) |
|
|
| 匹配非单词字符 |
|
|
| 匹配空白字符 (空格, |
|
|
| 匹配非空白字符 |
|
看个例子,假设我们要找一个三位数的数字:
import re
text = "I have 3 apples, 10 oranges, and 999 pears."
pattern = r"\d\d\d" # r"" 表示这是一个“原始字符串”,可以避免反斜杠的转义问题,写正则时强烈推荐使用!
match = re.search(pattern, text)
if match:
print(f"找到了一个三位数: {match.group()}") # 输出: 找到了一个三位数: 999
2.2. 设定边界:锚点
锚点用来匹配字符串中的特定位置,而不是字符本身。
| 锚点 | 含义 |
|
| 匹配字符串的开头 |
|
| 匹配字符串的结尾 |
|
| 匹配一个单词边界(单词的开头或结尾) |
|
| 匹配一个非单词边界 |
\b 特别有用,可以帮你精确匹配整个单词。
import re
text = "This is a cat, not a catalog."
pattern1 = r"cat"
pattern2 = r"\bcat\b" # \b 表示 cat 这个词的左右两边都得是边界(比如空格或者句首/句尾)
print(f"用 '{pattern1}' 找到的: {re.search(pattern1, text).group()}") # 输出: cat
print(f"用 '{pattern2}' 找到的: {re.search(pattern2, text).group()}") # 输出: cat
# 在 "catalog" 中,'cat' 后面不是边界,所以 \bcat\b 匹配不到
text2 = "This is a catalog."
match = re.search(pattern2, text2)
if not match:
print(f"在 '{text2}' 中找不到独立的单词 'cat'。")
2.3. 控制数量:量词
量词跟在某个字符或分组后面,用来指定它应该出现多少次。
| 量词 | 含义 |
|
| 匹配前一个字符 0 次或多次 |
|
| 匹配前一个字符 1 次或多次 |
|
| 匹配前一个字符 0 次或 1 次 |
|
| 精确匹配 m 次 |
|
| 匹配 m 到 n 次 |
|
| 至少匹配 m 次 |
现在,我们可以把前面的三位数例子写得更优雅:
import re
text = "I have 3 apples, 10 oranges, and 999 pears."
pattern = r"\d{3}" # \d 出现 3 次
match = re.search(pattern, text)
print(f"用 \\d{{3}} 找到的三位数: {match.group()}") # 输出: 999
# 找一个或多个数字
pattern2 = r"\d+"
# re.findall() 是一个很酷的函数,可以找出所有匹配项,我们后面会详细说
matches = re.findall(pattern2, text)
print(f"用 \\d+ 找到的所有数字: {matches}") # 输出: ['3', '10', '999']
贪婪 vs. 非贪婪(懒惰)
默认情况下,量词是贪婪的,它们会尽可能多地匹配字符。比如 .* 会一直匹配到字符串的末尾。
如果你想让它尽可能少地匹配,可以在量词后面加个 ?,把它变成非贪婪(或称懒惰)模式。
import re
text = "<div>content1</div><div>content2</div>"
# 贪婪模式
greedy_pattern = r"<div>.*</div>"
print(f"贪婪模式: {re.search(greedy_pattern, text).group()}")
# 输出: <div>content1</div><div>content2</div> (从第一个 <div> 匹配到了最后一个 </div>)
# 非贪婪模式
lazy_pattern = r"<div>.*?</div>"
print(f"非贪婪模式: {re.search(lazy_pattern, text).group()}")
# 输出: <div>content1</div> (遇到第一个 </div> 就停止了)
3. 组合与选择:分组和分支
3.1. 字符集 []
如果你想匹配的不是某个特定字符,而是“某个范围内的任意一个字符”,可以用方括号 []。
-
[abc]:匹配a、b、c中的任意一个。 -
[0-9]:匹配任意一个数字,等同于\d。 -
[a-zA-Z]:匹配任意一个英文字母。 -
[^abc]:在方括号里用^开头,表示取反,即匹配除了a、b、c之外的任意字符。
import re
text = "I like cats, but she likes dogs."
pattern = r"[cd]ats" # 匹配 'cats' 或者 'dats' (这里没有dats) -> 这里应该是 [cd]ogs
# 修正一下,匹配 cats 或 dogs
pattern_correct = r"\b[cd]ogs\b" # 噢,还是不对,应该是匹配 'cats' 或 'dogs'
pattern_final = r"\b(cats|dogs)\b" # 用 | 更合适,但我们先用字符集演示
# 演示字符集
text_vowel = "how are you?"
pattern_vowel = r"[aeiou]"
print(f"文本中的元音字母: {re.findall(pattern_vowel, text_vowel)}")
# 输出: ['o', 'a', 'e', 'o', 'u']
3.2. 分组 ()
用圆括号 () 可以将一部分正则表达式作为一个整体来对待,这被称为分组。分组有两大作用:
-
对分组使用量词:比如
(ab)+会匹配ab、abab、ababab等。 -
捕获内容:被括号括起来的部分,其匹配到的内容会被“捕获”下来,方便我们后续单独提取。
import re
text = "My phone is 123-456-7890."
# 我们想把区号、前缀、后缀分开提取
pattern = r"(\d{3})-(\d{3})-(\d{4})"
match = re.search(pattern, text)
if match:
print(f"完整匹配: {match.group(0)}") # group(0) 或 group() 总是完整的匹配
print(f"区号: {match.group(1)}") # 第 1 个括号捕获的内容
print(f"前缀: {match.group(2)}") # 第 2 个括号捕获的内容
print(f"后缀: {match.group(3)}") # 第 3 个括号捕获的内容
# .groups() 可以一次性获取所有捕获组
print(f"所有捕获组: {match.groups()}") # 输出: ('123', '456', '7890')
3.3. 分支 |
| 就像是编程里的 or,表示“或”的关系,匹配 | 左边或右边的表达式。
import re
text = "I love cats. I love dogs."
pattern = r"cats|dogs"
print(f"找到的动物: {re.findall(pattern, text)}") # 输出: ['cats', 'dogs']
4. re 模块的常用函数
我们已经见过 re.search() 和 re.findall() 了,re 模块还有其他几个很能打的干将。
| 函数 | 描述 |
|
| 从字符串的开头开始匹配。如果开头不匹配,就失败了,返回 |
|
| 扫描整个字符串,找到第一个匹配项。 |
|
| 找到所有匹配项,并以列表形式返回。 |
|
| 和 |
|
| 替换。找到所有匹配 |
|
| 使用 |
|
| 编译一个正则表达式,如果一个正则要被反复使用,先编译可以提高效率。 |
match vs search
import re
text = "user@example.com"
pattern = r"example"
# search 可以在任何位置找到
print(f"search: {re.search(pattern, text)}") # <re.Match object ...>
# match 必须从头开始,所以这里找不到
print(f"match: {re.match(pattern, text)}") # None
sub 的威力
re.sub 是一个非常强大的替换工具。
import re
text = "Agent Alice gave the secret documents to Agent Bob."
# 把所有 Agent XXX 的名字替换成 [REDACTED]
new_text = re.sub(r"Agent \w+", "[REDACTED]", text)
print(new_text)
# 输出: [REDACTED] gave the secret documents to [REDACTED].
5. 编译与标志位 (Flags)
re.compile()
当你需要多次使用同一个正则表达式时,先用 re.compile() 把它编译成一个模式对象,可以让程序运行得更快。
import re
text = "I have 1 apple, 2 bananas, 3 cherries."
# 编译正则表达式
p = re.compile(r"\d+")
# 多次使用这个编译好的模式
print(p.findall(text)) # ['1', '2', '3']
print(p.search(text)) # <re.Match object ...>
标志位 (Flags)
标志位可以改变正则表达式的匹配行为。
| 标志 | 缩写 | 含义 |
|
|
| 忽略大小写 |
|
|
| 多行模式,让 |
|
|
| 让 |
import re
text = "Python is fun.\npython is powerful."
# 1. 不加 flag,默认区分大小写
print(re.findall(r"python", text)) # ['python']
# 2. 加上 re.I,忽略大小写
print(re.findall(r"python", text, re.IGNORECASE)) # ['Python', 'python']
# 3. 多行模式
text_multi = "hello world\nhello python"
# ^ 默认只匹配字符串开头
print(re.findall(r"^hello", text_multi)) # ['hello']
# re.M 让 ^ 也匹配换行后的开头
print(re.findall(r"^hello", text_multi, re.MULTILINE)) # ['hello', 'hello']

3478

被折叠的 条评论
为什么被折叠?



