MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

深入理解Python中的正则表达式

2022-04-122.1k 阅读

正则表达式基础

什么是正则表达式

正则表达式(Regular Expression),又称规则表达式,是一种文本模式,用于描述字符串的匹配规则。它可以用于查找、替换、验证文本等操作。正则表达式在各种编程语言中都有广泛应用,Python 也不例外。在 Python 中,通过 re 模块来支持正则表达式操作。

正则表达式的基本语法

  1. 字符匹配
    • 普通字符:大多数普通字符,如字母、数字、标点符号等,在正则表达式中就表示它们自身。例如,正则表达式 abc 会匹配字符串中连续出现的 abc
    • 元字符:一些具有特殊含义的字符,如 .^$*+?{}[]()| 等。
      • .:匹配除换行符 \n 之外的任意单个字符。例如,正则表达式 a.c 可以匹配 abca ca#c 等。
      • ^:匹配字符串的开始位置。例如,正则表达式 ^abc 只会匹配以 abc 开头的字符串,像 abcdef 可以匹配,而 defabc 则不能匹配。
      • $:匹配字符串的结束位置。例如,正则表达式 abc$ 只会匹配以 abc 结尾的字符串,如 defabc 可以匹配,abcdef 则不能匹配。
  2. 字符类
    • []:定义一个字符类,匹配方括号内的任意一个字符。例如,[abc] 可以匹配 abc 中的任意一个字符。
    • 范围表示:在字符类中,可以使用 - 表示字符范围。例如,[a - z] 匹配任意小写字母,[0 - 9] 匹配任意数字。
    • 否定字符类[^ ] 表示匹配不在方括号内的任意字符。例如,[^a - z] 匹配任意非小写字母的字符。
  3. 量词
    • *:匹配前面的字符零次或多次。例如,a* 可以匹配空字符串,也可以匹配一个或多个 a,如 aaaaaa 等。
    • +:匹配前面的字符一次或多次。例如,a+ 至少匹配一个 a,可以是 aaaaaa 等,但不能匹配空字符串。
    • ?:匹配前面的字符零次或一次。例如,a? 可以匹配空字符串或一个 a
    • {n}:匹配前面的字符恰好 n 次。例如,a{3} 只匹配连续出现三次的 a,即 aaa
    • {n,}:匹配前面的字符至少 n 次。例如,a{3,} 匹配连续出现三次或更多次的 a,如 aaaaaaa 等。
    • {n,m}:匹配前面的字符至少 n 次,最多 m 次。例如,a{3,5} 匹配连续出现三次到五次的 a,如 aaaaaaaaaaaa
  4. 分组
    • ():用于分组,可以将多个字符作为一个整体进行操作。例如,(ab)+ 会把 ab 作为一个整体,匹配一个或多个 ab,如 abababababab 等。
    • 捕获组:分组默认是捕获组,在匹配成功后,可以通过索引获取捕获组内匹配的内容。例如,(a(b)c) 有两个捕获组,第一个捕获组是 abc,第二个捕获组是 b
  5. 分支
    • |:表示分支,匹配 | 两边的任意一个表达式。例如,a|b 可以匹配 ab

Python 中的 re 模块

常用函数

  1. re.search()
    • 功能:在字符串中搜索正则表达式的第一个匹配项。
    • 语法re.search(pattern, string, flags = 0),其中 pattern 是正则表达式,string 是要搜索的字符串,flags 是可选参数,用于控制匹配行为。
    • 示例
import re
text = "python is a great language, python is fun"
match = re.search('python', text)
if match:
    print('Match found:', match.group())
else:
    print('Match not found')
  • 解释:上述代码使用 re.searchtext 字符串中搜索 python,如果找到匹配项,就打印出匹配的内容。match.group() 方法用于获取匹配的字符串。
  1. re.match()
    • 功能:从字符串的开头开始匹配正则表达式。
    • 语法re.match(pattern, string, flags = 0)
    • 示例
import re
text = "python is a great language"
match = re.match('python', text)
if match:
    print('Match found:', match.group())
else:
    print('Match not found')
  • 解释:这里使用 re.matchtext 字符串开头匹配 python,由于 text 开头是 python,所以能匹配成功并打印出匹配内容。如果字符串不是以 python 开头,则匹配失败。
  1. re.findall()
    • 功能:在字符串中找到所有匹配正则表达式的子串,并以列表形式返回。
    • 语法re.findall(pattern, string, flags = 0)
    • 示例
import re
text = "python is a great language, python is fun"
matches = re.findall('python', text)
print('Matches found:', matches)
  • 解释re.findalltext 字符串中找到所有的 python 子串,并以列表形式返回,所以会打印出 ['python', 'python']
  1. re.finditer()
    • 功能:在字符串中找到所有匹配正则表达式的子串,并返回一个迭代器,迭代器的每个元素是一个 Match 对象。
    • 语法re.finditer(pattern, string, flags = 0)
    • 示例
import re
text = "python is a great language, python is fun"
for match in re.finditer('python', text):
    print('Match found at position:', match.start())
  • 解释:通过 re.finditer 遍历 text 字符串中所有匹配 python 的子串,match.start() 方法用于获取匹配子串在原字符串中的起始位置。
  1. re.sub()
    • 功能:用指定的字符串替换字符串中所有匹配正则表达式的子串。
    • 语法re.sub(pattern, repl, string, count = 0, flags = 0),其中 repl 是替换的字符串,count 是替换的最大次数,默认为 0 表示替换所有匹配项。
    • 示例
import re
text = "python is a great language, python is fun"
new_text = re.sub('python', 'Java', text)
print('New text:', new_text)
  • 解释:上述代码将 text 字符串中所有的 python 替换为 Java,并打印出替换后的新字符串。

Match 对象

当使用 re.search()re.match()re.finditer() 找到匹配项时,会返回一个 Match 对象。Match 对象有以下常用方法和属性:

  1. group([group1, ...])
    • 功能:返回整个匹配的子串或指定捕获组的子串。如果没有指定参数,返回整个匹配的子串;如果指定了一个或多个捕获组的索引,则返回相应捕获组的子串。
    • 示例
import re
text = "I love python"
match = re.search('I love (\w+)', text)
if match:
    print('Whole match:', match.group())
    print('Group 1:', match.group(1))
  • 解释:在这个例子中,(\w+) 是一个捕获组。match.group() 返回整个匹配的子串 I love pythonmatch.group(1) 返回第一个捕获组匹配的子串 python
  1. start([group])
    • 功能:返回整个匹配子串或指定捕获组子串在原字符串中的起始位置。如果没有指定参数,返回整个匹配子串的起始位置;如果指定了捕获组索引,则返回相应捕获组子串的起始位置。
    • 示例
import re
text = "I love python"
match = re.search('I love (\w+)', text)
if match:
    print('Whole match start position:', match.start())
    print('Group 1 start position:', match.start(1))
  • 解释match.start() 返回整个匹配子串 I love pythontext 字符串中的起始位置 0,match.start(1) 返回第一个捕获组匹配子串 python 的起始位置 7。
  1. end([group])
    • 功能:返回整个匹配子串或指定捕获组子串在原字符串中的结束位置(结束位置不包含在匹配子串内)。参数用法与 start([group]) 类似。
    • 示例
import re
text = "I love python"
match = re.search('I love (\w+)', text)
if match:
    print('Whole match end position:', match.end())
    print('Group 1 end position:', match.end(1))
  • 解释match.end() 返回整个匹配子串 I love python 的结束位置 13,match.end(1) 返回第一个捕获组匹配子串 python 的结束位置 13。
  1. span([group])
    • 功能:返回一个元组,包含整个匹配子串或指定捕获组子串在原字符串中的起始位置和结束位置,等价于 (start(group), end(group))
    • 示例
import re
text = "I love python"
match = re.search('I love (\w+)', text)
if match:
    print('Whole match span:', match.span())
    print('Group 1 span:', match.span(1))
  • 解释match.span() 返回 (0, 13),表示整个匹配子串的起始和结束位置;match.span(1) 返回 (7, 13),表示第一个捕获组匹配子串的起始和结束位置。

高级正则表达式特性

非贪婪匹配

  1. 贪婪匹配:在正则表达式中,默认情况下量词是贪婪的,即尽可能多地匹配字符。例如,正则表达式 a.*b 在字符串 aabb 中,会匹配整个 aabb,因为 .* 会贪婪地匹配尽可能多的字符直到遇到最后一个 b
  2. 非贪婪匹配:通过在量词后面加上 ? 可以实现非贪婪匹配。例如,a.*?b 在字符串 aabb 中,只会匹配 aab,因为 .*? 会尽可能少地匹配字符,一旦遇到 b 就停止匹配。
    • 示例
import re
text = "aabb"
greedy_match = re.search('a.*b', text)
non_greedy_match = re.search('a.*?b', text)
if greedy_match:
    print('Greedy match:', greedy_match.group())
if non_greedy_match:
    print('Non - greedy match:', non_greedy_match.group())
  • 解释:上述代码分别演示了贪婪匹配和非贪婪匹配在同一字符串上的效果。贪婪匹配结果为 aabb,非贪婪匹配结果为 aab

零宽断言

  1. 正向肯定断言(?=pattern),断言在当前位置之后能匹配 pattern,但不消耗字符。例如,正则表达式 \w+(?=ing) 会匹配以 ing 结尾的单词,但不包含 ing 部分。
    • 示例
import re
text = "running jumping"
matches = re.findall('\w+(?=ing)', text)
print('Matches:', matches)
  • 解释re.findall 使用正向肯定断言,在 text 字符串中找到以 ing 结尾的单词部分,所以会打印出 ['run', 'jump']
  1. 正向否定断言(?!pattern),断言在当前位置之后不能匹配 pattern。例如,\w+(?!ing) 会匹配不以 ing 结尾的单词。
    • 示例
import re
text = "running jumping play"
matches = re.findall('\w+(?!ing)', text)
print('Matches:', matches)
  • 解释:此代码使用正向否定断言,在 text 字符串中找到不以 ing 结尾的单词,所以会打印出 ['play']
  1. 反向肯定断言(?<=pattern),断言在当前位置之前能匹配 pattern,但不消耗字符。例如,(?<=abc)\d+ 会匹配在 abc 之后的数字。
    • 示例
import re
text = "abc123 def456"
matches = re.findall('(?<=abc)\d+', text)
print('Matches:', matches)
  • 解释:这里使用反向肯定断言,在 text 字符串中找到在 abc 之后的数字部分,所以会打印出 ['123']
  1. 反向否定断言(?<!pattern),断言在当前位置之前不能匹配 pattern。例如,(?<!abc)\d+ 会匹配不在 abc 之前的数字。
    • 示例
import re
text = "abc123 def456"
matches = re.findall('(?<!abc)\d+', text)
print('Matches:', matches)
  • 解释:此代码使用反向否定断言,在 text 字符串中找到不在 abc 之前的数字部分,所以会打印出 ['456']

命名捕获组

  1. 普通捕获组:在前面的例子中,我们使用 () 定义捕获组,并通过索引获取捕获组的内容。例如,(a(b)c) 有两个捕获组,索引分别为 1 和 2。
  2. 命名捕获组:可以给捕获组命名,通过 (?P<name>pattern) 的形式定义,其中 name 是捕获组的名称,pattern 是捕获组的匹配模式。
    • 示例
import re
text = "I love python"
match = re.search('I love (?P<language>\w+)', text)
if match:
    print('Language:', match.group('language'))
  • 解释:这里定义了一个名为 language 的命名捕获组,通过 match.group('language') 可以获取该捕获组匹配的内容,即 python。命名捕获组使得代码更易读,尤其是在有多个捕获组的复杂正则表达式中。

标志位(Flags)

  1. re.I(IGNORECASE):使匹配对大小写不敏感。例如,re.search('python', 'Python is great', re.I) 能匹配成功,因为 re.I 忽略了大小写。
  2. re.M(MULTILINE):在多行模式下,^$ 不仅匹配字符串的开头和结尾,还匹配每一行的开头和结尾。例如,对于字符串 line1\nline2,使用 re.findall('^line', string, re.M) 会找到两个匹配项,分别是 line1line2 开头的 line
  3. re.S(DOTALL):使 . 可以匹配包括换行符 \n 在内的任意字符。默认情况下,. 不匹配换行符。例如,re.search('a.*b', 'a\nb', re.S) 能匹配成功,因为 re.S. 可以匹配换行符。
  4. re.X(VERBOSE):可以在正则表达式中添加注释,使正则表达式更易读。例如:
import re
pattern = re.compile(r"""
    \d+ # 匹配一个或多个数字
    \. # 匹配点号
    \d+ # 匹配一个或多个数字
""", re.X)
text = "1.2"
match = pattern.search(text)
if match:
    print('Match found:', match.group())
  • 解释:在 re.compile 中使用 re.X 标志位,使得可以在正则表达式中添加注释,增强了代码的可读性。

实际应用场景

数据验证

  1. 验证邮箱地址:邮箱地址的格式通常为 用户名@域名,可以使用正则表达式进行验证。
    • 示例
import re
def validate_email(email):
    pattern = r'^[a-zA - Z0 - 9_.+-]+@[a-zA - Z0 - 9 -]+\.[a-zA - Z0 - 9-.]+$'
    if re.match(pattern, email):
        return True
    return False


email1 = "user@example.com"
email2 = "user.example.com"
print(validate_email(email1))
print(validate_email(email2))
  • 解释:上述代码定义了一个函数 validate_email,使用正则表达式验证邮箱地址的格式。^$ 确保整个字符串都匹配指定模式,[a-zA - Z0 - 9_.+-]+ 匹配用户名部分,@ 匹配 @ 符号,[a-zA - Z0 - 9 -]+\.[a-zA - Z0 - 9-.]+ 匹配域名部分。
  1. 验证手机号码:不同国家的手机号码格式不同,以中国手机号码为例,通常为 11 位数字,且以 1 开头。
    • 示例
import re
def validate_phone(phone):
    pattern = r'^1\d{10}$'
    if re.match(pattern, phone):
        return True
    return False


phone1 = "13800138000"
phone2 = "12345678901"
print(validate_phone(phone1))
print(validate_phone(phone2))
  • 解释:此函数 validate_phone 使用正则表达式验证手机号码,^1 表示以 1 开头,\d{10} 表示后面跟着 10 位数字。

文本提取

  1. 从 HTML 中提取链接:在处理网页数据时,经常需要从 HTML 代码中提取链接。
    • 示例
import re
html = '<a href="https://example.com">Example</a>'
links = re.findall(r'href="([^"]+)"', html)
print('Links:', links)
  • 解释:这里使用 re.findall 和正则表达式 href="([^"]+)" 从 HTML 代码中提取链接。([^"]+) 是一个捕获组,用于捕获 href 属性值中的链接部分。
  1. 从日志文件中提取特定信息:假设日志文件中有如下格式的记录:[2023 - 01 - 01 12:00:00] INFO: Message,可以提取日期、时间、日志级别和消息。
    • 示例
import re
log = '[2023 - 01 - 01 12:00:00] INFO: Message'
pattern = r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+): (.*)'
match = re.search(pattern, log)
if match:
    print('Date and time:', match.group(1))
    print('Log level:', match.group(2))
    print('Message:', match.group(3))
  • 解释:通过定义合适的正则表达式,使用捕获组分别提取日志中的日期时间、日志级别和消息内容。

文本替换

  1. 替换敏感词:在处理文本时,可能需要替换一些敏感词汇。
    • 示例
import re
text = "This is a bad word"
new_text = re.sub(r'bad', 'good', text)
print('New text:', new_text)
  • 解释:使用 re.sub 将文本中的 bad 替换为 good
  1. 格式化文本:例如,将日期格式从 YYYY - MM - DD 转换为 MM/DD/YYYY
    • 示例
import re
date = "2023 - 01 - 01"
new_date = re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\2/\3/\1', date)
print('New date:', new_date)
  • 解释:通过捕获组分别捕获年、月、日,然后在替换字符串中按照新的格式重新组合。

编写高效的正则表达式

避免不必要的捕获组

  1. 捕获组的开销:每个捕获组都会增加正则表达式引擎的处理开销,因为引擎需要记录捕获组的匹配内容。例如,在 (a|b) 这样的简单分支中,如果不需要获取捕获组的内容,就可以使用非捕获组 (?:a|b)。非捕获组不会记录匹配内容,从而提高匹配效率。
  2. 示例对比
import re
import timeit


# 使用捕获组
pattern1 = re.compile('(a|b)')
text = 'ab'
def test1():
    pattern1.findall(text)


# 使用非捕获组
pattern2 = re.compile('(?:a|b)')
def test2():
    pattern2.findall(text)


print('Time with capturing group:', timeit.timeit(test1, number = 10000))
print('Time with non - capturing group:', timeit.timeit(test2, number = 10000))
  • 解释:上述代码通过 timeit 模块对比了使用捕获组和非捕获组的执行时间。可以看到,在不需要获取捕获组内容时,使用非捕获组的效率更高。

优先使用字符类

  1. 字符类的效率:在匹配字符集合时,使用字符类比多个分支效率更高。例如,要匹配 abc,使用 [abc]a|b|c 效率更高。因为字符类是一种更紧凑的表示方式,正则表达式引擎可以更快地处理。
  2. 示例对比
import re
import timeit


# 使用分支
pattern1 = re.compile('a|b|c')
text = 'abc'
def test1():
    pattern1.findall(text)


# 使用字符类
pattern2 = re.compile('[abc]')
def test2():
    pattern2.findall(text)


print('Time with alternation:', timeit.timeit(test1, number = 10000))
print('Time with character class:', timeit.timeit(test2, number = 10000))
  • 解释:通过 timeit 模块对比可以发现,使用字符类的正则表达式在匹配字符集合时效率更高。

简化复杂的正则表达式

  1. 逐步构建和测试:对于复杂的正则表达式,不要试图一次性写出完整的表达式。可以先从简单的部分开始,逐步构建并测试。例如,要匹配一个复杂的文件路径格式,可以先分别测试匹配盘符、路径分隔符、文件名等部分的正则表达式,然后再组合起来。
  2. 使用注释和命名捕获组:如前面提到的,使用注释(结合 re.X 标志位)和命名捕获组可以使复杂的正则表达式更易读和维护。例如,在匹配复杂的网络地址时,使用命名捕获组分别表示不同部分,代码会更清晰。
  3. 避免过度复杂:有时候,过于复杂的正则表达式可能会导致可读性和效率都降低。如果可以通过其他方式(如结合字符串操作函数)来简化处理,应该优先考虑。例如,在处理一些简单的文本替换任务时,可能使用字符串的 replace 方法比复杂的正则表达式更合适。

通过深入理解正则表达式的基本语法、Python 的 re 模块以及高级特性,并在实际应用中注意编写高效的正则表达式,开发者可以在文本处理任务中更加得心应手,提高编程效率和代码质量。无论是数据验证、文本提取还是文本替换等任务,正则表达式都是 Python 开发者工具箱中的强大工具。