Python正则表达式介绍与实践
1. 正则表达式简介
正则表达式是一种用来描述或匹配字符串的模式。它广泛应用于字符串搜索、替换和解析。
2. 导入re模块
Python中的正则表达式功能主要由re模块提供。你可以通过以下方式导入它:
import re
3. 正则表达式基本语法
3.1 匹配单个字符
.匹配除换行符外的任意字符。\w匹配字母、数字或下划线或汉字。\W匹配非字母、数字、下划线。\d匹配数字。\D匹配非数字。\s匹配空白字符(包括空格、制表符、换行符等)。\S匹配非空白字符。
3.2 字符类
[abc]匹配字符a、b或c。[^abc]匹配除a、b、c之外的任意字符。[a-z]匹配任意小写字母。[A-Z]匹配任意大写字母。[aeiou]匹配元音字母。[0-9]匹配任意数字,等价于\d。[a-z0-9A-Z]等价于\w(如果只考虑英文的话)。[.?!]匹配标点符号(.或?或!)
3.3 量词
*匹配前一个字符0次或多次。+匹配前一个字符1次或多次。?匹配前一个字符0次或1次。{n}匹配前一个字符n次。{n,}匹配前一个字符至少n次。{n,m}匹配前一个字符n到m次。
3.4 边界匹配
-
^匹配字符串的开头。 -
$匹配字符串的结尾。 -
\b匹配单词边界(开始或结束)。 -
\B匹配非单词边界。
3.5 分支条件
正则表达式中的分支条件语法使用竖线 | 来表示多个选择中的一个。例如,pattern1|pattern2 表示匹配 pattern1 或 pattern2。具体用法如下:
a|b匹配a或b。(cat|dog)匹配cat或dog。
当多个条件中有一个匹配成功时,整个表达式就匹配成功。这在处理复杂模式匹配时非常有用。例如,正则表达式 Jan|Feb|Mar 匹配 “Jan”、“Feb” 或 “Mar”。
在使用正则表达式的分支条件时,有以下注意事项:
- 匹配顺序:从左到右匹配,一旦找到匹配项,就不会继续尝试其他分支。
- 优先级:用小括号
()可以明确分支的优先级。例如,(a|b)c匹配ac或bc,而a|bc匹配a或bc。 - 效率问题:分支条件过多时,匹配效率可能会下降,建议优化正则表达式以提高性能。
- 贪婪匹配:默认情况下,正则表达式是贪婪的,尽可能多地匹配字符。
3.6 分组
分组在正则表达式中用于捕获子模式,并用圆括号 () 来表示。
-
捕获组:
(pattern),将pattern作为一个组,例如,(abc)匹配并捕获 “abc”。 -
非捕获组:
(?:pattern),匹配pattern但不捕获,例如,(?:abc)只匹配 “abc” 而不保存。 -
命名组:
(?P<name>pattern),匹配并捕获到名为name的组,例如,(?P<word>\w+)匹配并捕获一个单词。注意:在 Python 的正则表达式库
re中,命名捕获组的语法与其他一些语言(如 JavaScript 或 .NET)有所不同。Python 使用的是(?P<name>pattern)语法,而不是(?<name>pattern)。 -
引用分组:
\number或\g<name>,引用之前捕获的组,例如,(\d+)-\1匹配重复的数字。
示例:
-
(a|b)c匹配 “ac” 或 “bc”。 -
(?P<digit>\d)捕获一个数字并命名为 “digit”。
使用正则表达式进行IPv4地址匹配:
\b(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}\b\b(?:\d{1,3}\.){3}\d{1,3}\b
上面的pattern1仅仅会匹配到正确的IPv4地址,下面的pattern2虽然可以匹配到正确的IPv4地址,但是像256.300.888.999这种不正确的地址也会匹配。
3.7 后向引用
正则表达式中的后向引用(backreference)允许你在正则表达式中引用之前捕获的子表达式。
后向引用的语法是使用 \ 后跟数字 n,其中 n 是一个从 1 开始的正整数,表示捕获组的索引。捕获组是用圆括号 () 包围的正则表达式。
例如我们要匹配一个重复的单词,如 apple apple,可以使用后向引用来实现:
- 正则表达式:
(\w+)\s+\1 - 解释:
(\w+)匹配并捕获第一个单词apple。\s+匹配一个或多个空白字符。\1是后向引用,引用第一个捕获组(\w+)中匹配到的内容,这里就是apple。
这个正则表达式将匹配 apple apple 中的重复单词。
后向引用在需要确保重复的内容一致性时非常有用,例如在查找 HTML 标签的开头和结尾是否匹配时,或者查找重复的词语等情况下。
3.8 零宽断言
零宽断言(zero-width assertions)是正则表达式中非常强大的特性,它允许你指定一个位置,而不是字符,来匹配一个模式。零宽断言不消耗字符,也就是说,它们是零宽度的,只是检查位置是否符合条件。
以下是零宽断言的四种类型及其对比示例:
| 类型 | 语法 | 描述 | 示例正则表达式 | 示例匹配文本 | 匹配结果 |
|---|---|---|---|---|---|
| 正向零宽断言 | (?=pattern) | 断言后面的内容要匹配 pattern | \d+(?= years) | 25 years old | 25 |
| 负向零宽断言 | (?!pattern) | 断言后面的内容不匹配 pattern | \d+(?! years) | 25 months old | 25 |
| 正向零宽断言(逆向) | (?<=pattern) | 断言前面的内容要匹配 pattern | (?<=\$)\d+ | $25 | 25 |
| 负向零宽断言(逆向) | (?<!pattern) | 断言前面的内容不匹配 pattern | (?<!\$)\d+ | 25 dollars | 25 |
3.9 注释
使用 (?#comment) 语法在正则表达式内部添加注释。
| 注释类型 | 语法 | 作用 | 示例 |
|---|---|---|---|
| 内联注释 | (?#comment) | 在正则表达式内部添加注释 | \d{3}(?# Area Code)-\d{4} |
除了上述的语法,也可以使用处理选项re.VERBOSE添加注释。
小括号()相关语法汇总
分组,引用,捕获,零宽断言,注释都用到了小括号的语法,每个特性在正则表达式中都有其特定的用途和语法。
| 特性 | 语法 | 作用 | 示例 | 说明 |
|---|---|---|---|---|
| 分组 | (pattern) | 将多个字符或子模式组合在一起 | (abc)+ | 匹配一个或多个 abc |
| 捕获 | (pattern) | 捕获并存储匹配的内容 | (\d{3})-(\d{4}) | 捕获三个数字和四个数字 |
| 引用 | \n 或 \k<name> | 引用前面捕获的内容 | (\w+)\s+\1 | 匹配重复的单词 |
| 非捕获组 | (?:pattern) | 分组但不捕获内容 | (?:abc)+ | 匹配一个或多个 abc 不捕获 |
| 命名组 | (?<name>pattern) | 为捕获的分组指定一个名称 | (?\d{4})-(?\d{2})-(?\d{2}) | 捕获日期字符串中的年份,月份和日志,并使用命名组引用 |
| 正向零宽断言 | (?=pattern) | 断言当前位置后面的内容要匹配 pattern | \d+(?= years) | 后面紧跟 years 的数字 |
| 负向零宽断言 | (?!pattern) | 断言当前位置后面的内容不匹配 pattern | \d+(?! years) | 后面不能跟 years 的数字 |
| 正向零宽断言(逆向) | (?<=pattern) | 断言当前位置前面的内容要匹配 pattern | (?<=\$)\d+ | 前面紧跟 $ 的数字 |
| 负向零宽断言(逆向) | (?<!pattern) | 断言当前位置前面的内容不匹配 pattern | (?<!\$)\d+ | 前面不能跟 $ 的数字 |
| 注释 | (?#comment) | 添加注释以提高可读性 | \d{3}(?# Area Code)-\d{4} | 区号注释 |
3.10 贪婪与懒惰
在正则表达式中,贪婪(greedy)和懒惰(lazy,也称为非贪婪或者勉强的)是描述匹配模式如何工作的两种不同方式。
-
贪婪匹配:
- 贪婪模式尽可能匹配更多的字符。
- 贪婪量词默认是贪婪的,例如
*,+,?,{n,}等。 - 例如,正则表达式
a.*b会匹配尽可能长的以a开始、以b结尾的字符串。
-
懒惰匹配(也称为非贪婪或勉强的):
- 懒惰模式会尽可能少的匹配字符。
- 在贪婪量词后面加上
?可以将其转换为懒惰模式。 - 例如,
a.*?b会匹配尽可能短的以a开始、以b结尾的字符串。
下面是一些常见的贪婪量词和它们的懒惰版本:
*:贪婪*,懒惰*?+:贪婪+,懒惰+??:贪婪?,懒惰??{n,}:贪婪{n,},懒惰{n,}?{n,m}:贪婪{n,m},懒惰{n,m}?
贪婪模式是默认的,如果你想要一个懒惰的模式,你需要在量词后面加上 ? 符号。
| 区别比 | 贪婪模式 | 懒惰模式 |
|---|---|---|
| 定义 | 尽可能多地匹配字符。 | 尽可能少地匹配字符。 |
| 量词 | 默认是贪婪的,例如 *, +, {n,} 等。 | 在贪婪量词后面加上 ? 来表示懒惰,如 *?, +?, {n,}?。 |
| 示例1 | a.*c 匹配 abbbcabc 中的整个字符串。 | a.*?c 匹配 abbc 中的 abbc。 |
| 示例2 | <div>.*</div> 匹配 <div>first</div><div>second</div> 整个字符串。 | <div>.*?</div> 分别匹配 <div>first</div> 和 <div>second</div>。 |
3.11 处理选项
正则表达式处理选项(也称为标志或修饰符)用于改变正则表达式的行为。
| 处理选项 | 简写 | 描述 | 示例 |
|---|---|---|---|
re.IGNORECASE | re.I | 忽略大小写匹配 | re.search(r"hello", "Hello", re.I) |
re.MULTILINE | re.M | 多行模式,^ 和 $ 匹配每行 | re.search(r"^hello", "hello\nworld", re.M) |
re.DOTALL | re.S | 使 . 匹配包括换行符在内的所有字符 | re.search(r"hello.*world", "hello\nworld", re.S) |
re.VERBOSE | re.X | 允许使用空白符和注释以提高可读性 | re.search(r"(?x) \d{3} # 区号\n-\d{4} # 本地号码", "123-4567") |
re.ASCII | re.A | 使 \w, \b, \s 等仅匹配 ASCII 字符 | re.search(r"\w+", "café", re.A) |
re.LOCALE | re.L | 依赖当前的区域设置进行匹配 | re.search(r"\w+", "café", re.L) |
re.UNICODE | re.U | 使 \w, \b, \s 等匹配所有 Unicode 字符 | re.search(r"\w+", "café", re.U) |
re.DEBUG | 无 | 输出编译后的正则表达式字节码 | re.compile(r"\d+", re.DEBUG) |
4. 常用函数
4.1 re.match()
在字符串的开头尝试匹配一个模式。如果匹配成功,返回一个匹配对象,否则返回None。
import re
pattern = r'hello'
text = 'hello world'
match = re.match(pattern, text)
if match:
print("匹配成功:", match.group())
else:
print("匹配失败")
4.2 re.search()
扫描整个字符串并返回第一个成功的匹配。
import re
pattern = r'world'
text = 'hello world'
match = re.search(pattern, text)
if match:
print("匹配成功:", match.group())
else:
print("匹配失败")
4.3 re.findall()
返回所有与模式匹配的子串列表。
import re
pattern = r'\d+'
text = 'There are 2 apples and 5 oranges'
matches = re.findall(pattern, text)
print("匹配结果:", matches)
4.4 re.sub()
替换字符串中所有匹配正则表达式的子串。
import re
pattern = r'apples'
replacement = 'bananas'
text = 'I like apples'
new_text = re.sub(pattern, replacement, text)
print("替换结果:", new_text)
4.5 re.split()
用于根据匹配的正则表达式模式拆分字符串。
import re
# 定义一个字符串
text = 'apple,banana;cherry orange:grape'
# 使用正则表达式匹配逗号、分号、冒号和空格
pattern = r'[;,\s:]+'
# 使用 re.split() 拆分字符串
result = re.split(pattern, text)
print(result) # 输出 ['apple', 'banana', 'cherry', 'orange', 'grape']
4.5 re.compile()
将正则表达式模式编译为正则表达式对象,提高匹配效率。
import re
pattern = re.compile(r'\d+')
result = pattern.findall('123abc456')
print(result) # 输出 ['123', '456']
当我们在Python中使用正则表达式时,re模块内部会干两件事情:
-
编译正则表达式,如果正则表达式的字符串本身不合法,会报错;
-
用编译后的正则表达式去匹配字符串。
如果一个正则表达式要重复使用几千次,出于效率的考虑,我们可以预编译该正则表达式,接下来重复使用时就不需要编译这个步骤了,直接匹配。
7. 代码示例
1. 捕获组/非捕获组
捕获组允许你提取子字符串。
import re
pattern = r'(\d{3})-(\d{2})-(\d{4})'
text = '123-45-6789'
match = re.match(pattern, text)
if match:
print("完整匹配:", match.group(0))
print("第一组:", match.group(1))
print("第二组:", match.group(2))
print("第三组:", match.group(3))
用 (?:...) 定义非捕获组,匹配内容但不捕获。
import re
pattern = r'(?:\d{3})-(\d{2})-(\d{4})'
text = '123-45-6789'
match = re.match(pattern, text)
if match:
print("完整匹配:", match.group(0))
print("第二组:", match.group(1))
print("第三组:", match.group(2)) # 注意: 这里的组数是从1开始的,因为非捕获组不算在内
2. 注释相关的示例代码
匹配一个包含可选国家代码的国际电话号码
import re
phone_pattern = re.compile(r'''
(\+\d{1,3})? # 可选的国际区号,如 +1, +44
\s? # 可选的空格
\(\d{1,4}\) # 区号在括号内
\s? # 可选的空格
\d{1,4} # 前缀(如前面的三位数)
[-\s]? # 可选的短横线或空格
\d{1,4} # 后缀(如后面的四位数)
([-\s]\d{1,4})* # 可选的其他数字段(适用于多个国家)
''', re.VERBOSE)
match = phone_pattern.search('+1 (123) 456-7890')
print(match.group()) # 输出: +1 (123) 456-7890
3. 提取邮件地址中的名字
以下示例展示了如何提取名字,假设名字可能在尖括号内或为电子邮件的本地部分。
import re
def extract_name(text):
# 尝试匹配尖括号中的名字
match = re.search(r'<([^>]+)>', text)
if match:
return match.group(1)
# 尝试匹配电子邮件地址的本地部分
match = re.search(r'([^@]+)@', text)
if match:
return match.group(1)
# 如果都没有匹配到,返回None
return None
# 测试例子
examples = [
"<Tom Paris> tom@voyager.org",
"bob@example.com"
]
for example in examples:
print(f"Input: {example}")
print(f"Extracted Name: {extract_name(example)}\n")
解释正则表达式 r'<([^>]+)>' :
-
r'...':前缀
r表示原始字符串 (raw string),在 Python 中,原始字符串中的反斜杠不会被转义。这对于正则表达式非常有用,因为正则表达式中通常会包含很多反斜杠字符。 -
<:匹配一个字符
<,表示字符串中的左尖括号。 -
([^>]+):- 这是一个捕获组,表示我们感兴趣的部分将被捕获,以供后续引用。
[和]之间的内容表示一个字符类,匹配一组字符中的任意一个。^放在字符类的开头,表示取非,即匹配除右尖括号>之外的任意字符。+表示前面的字符类匹配一次或多次,即至少要匹配一个字符。
-
>:匹配一个字符
>,表示字符串中的右尖括号。
总结一下,正则表达式 r'<([^>]+)>' 的含义是:
- 匹配一个左尖括号
<。 - 匹配并捕获左尖括号和右尖括号之间的所有字符(只要这些字符不是
>)。 - 匹配一个右尖括号
>。
因此,捕获组 ([^>]+) 将提取出字符串 Tom Paris。
说明:
在正则表达式中,
^有不同的含义,这取决于它的上下文:
- 在字符类中(例如
[^...]),^表示取非,匹配不在字符类中的字符。- 在字符类外(例如
^...),^表示行开头,匹配行的开头位置。
12万+

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



