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) |
为捕获的分组指定一个名称 | (? |
捕获日期字符串中的年份,月份和日志,并使用命名组引用 |
正向零宽断言 | (?=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> [email protected]",
"[email protected]"
]
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
。
说明:
在正则表达式中,
^
有不同的含义,这取决于它的上下文:
- 在字符类中(例如
[^...]
),^
表示取非,匹配不在字符类中的字符。- 在字符类外(例如
^...
),^
表示行开头,匹配行的开头位置。