1 什么是正则表达式
正则表达式(Regular Expression,在代码中常简写为regex、regexp或RE)是一种模式化的字符串,用于搜索、替换、分割和匹配文本数据。其基本思想是使用一些特殊的字符表示一个给定的模式,然后在文本中匹配这个模式。
正则表达式的作用:
- 匹配:判断给定的字符串是否符合正则表达式的过滤逻辑;
- 获取子串:可以通过正则表达式,从字符串中获取我们想要的特定部分。
正则表达式的特点:
- 非常强的灵活性、逻辑性和功能性;
- 可以迅速地用极简单的方式达到字符串的复杂控制。
- 对于刚接触的人来说,比较晦涩难懂。
2 正则表达式在Python中的使用
在Python中,主要使用re模块和regex模块来实现正则表达式操作。下面分别进行详细的讲解。
2.1 re模块
Python的re模块(正则表达式)是一种强大和灵活的工具,用来进行对字符串的模式匹配、替换和分割操作。re模块可以用于处理各种字符数据,包括文本文件、日志文件、编程语言代码等。re模块中包含了大量的正则表达式函数,包括搜索、替换、分割、匹配、复制、提取等,可以帮助用户完成高效的文本处理任务。
注:Python自带了re模块,不需要额外安装。因此,使用re模块也非常方便,而无需像安装其他第三方库一样在终端中运行pip install命令。
2.1.1 re.search()
re.search()是Python re模块中常用的一个函数,用于在一个字符串中搜索指定的正则表达式模式。在搜索时,该函数会将整个字符串扫描一遍,直到找到第一个与模式匹配的字符串子串,然后返回一个匹配对象(Match Object)。如果没有找到相应的匹配子串,函数会返回None。
re.search()函数的常见用法如下:
match_object = re.search(pattern, string, flags=0)
其中,pattern参数表示要匹配的正则表达式,string参数表示要进行匹配的字符串,flags参数表示匹配选项(如是否忽略大小写等)。当函数返回一个匹配对象时,可以调用match_object.group()方法来获取匹配的子串,而该方法的参数表示要获取的子串的序号(如果正则表达式中有多个括号,每个括号表示一个组,其序号从左向右依次增加)。
例如,下面是一个示例代码,用于在一个字符串中搜索一个正则表达式:
import re text = "Python is a popular programming language" pattern = "programming" match_object = re.search(pattern, text) if match_object: print("Found a match:", match_object.group()) else: print("No match found.")
输出结果:
Found a match: programming
2.1.2 re.match()
在Python中,re.match()模块和re.search()模块类似,用于在字符串中搜索正则表达式的匹配项。但是,re.match()只会在字符串的开头进行匹配,如果在开头无法找到匹配项,它会返回一个None对象。因此,re.match()更适用于需要在字符串开头进行匹配的场景。
match的函数用法与search一样,下面来看一下同样的测试字符串,match返回的结果:
text = "Python is a popular programming language" pattern = "programming" match_object = re.match(pattern, text) if match_object: print("Found a match:", match_object.group()) else: print("No match found.")
输出:
No match found.
由于programming并不是第一个单词,所以无法匹配。
2.1.3 re.findall()
re.findall()是Python中re模块提供的另一种匹配模式的函数,它可以搜索字符串中所有匹配正则表达式的模式,并返回一个列表,列表中的每个元素都是与正则表达式匹配的子字符串。
与re.search()
和re.match()
不同,re.findall()
会返回所有的匹配项,而不仅仅是第一个或最后一个匹配项。因此,如果您需要查找一个文本中所有匹配某个正则表达式的模式的情况,re.findall()
是非常有用的函数。
以下是该函数的示例代码:
import re # 定义一个正则表达式,匹配以数字开头的子字符串 pattern = r'\d+' # 定义一个待匹配的字符串 text = "Today is Oct 15, 2021, and the temperature is 20 degrees Celsius." # 使用re.findall()函数查找所有匹配项,并将它们存储在一个list对象中 matches = re.findall(pattern, text) # 输出匹配结果 print(matches)
在这个示例中,我们首先定义一个正则表达式模式,r'\d+'
,用于匹配以数字开头的子字符串。然后,我们定义了一个待匹配的字符串,text
。接下来,我们使用re.findall()
函数查找所有匹配项,并将它们存储在一个list对象中,matches
。最后,我们将匹配结果输出到屏幕上。
输出结果:
['15', '2021', '20']
这是因为,在示例中的文本中,有三个以数字开头的子字符串,分别是15、2021和20度。re.findall()
函数找到它们,并将它们存储在一个list对象中。
2.1.4 re.sub()
re.sub()是Python re模块中提供的另一种匹配模式的函数,用于在字符串中对以某种模式匹配的子字符串进行替换。re.sub()函数返回一个新的字符串,其中所有匹配指定模式的子字符串都被替换为指定的内容。
下面是一个简单的代码示例,说明如何使用re.sub()进行字符串替换:
import re # 定义一个正则表达式,匹配所有'is'字符 pattern = 'is' # 定义一个待匹配的字符串 text = "The pattern of the book is not easy to find." # 使用re.sub()函数将匹配项替换为指定字符串 new_text = re.sub(pattern, "was", text) # 输出结果 print(new_text)
在这个示例中,我们首先定义了一个正则表达式模式,'is'
,用于匹配所有的is
字符。然后,我们定义了一个待匹配的字符串,text
。接下来,我们使用re.sub()
函数将所有匹配项替换为"was"
。最后,我们输出被替换后的新字符串。
输出:
The pattern of the book was not easy to find.
2.2 regex模块
除了标准库中的re模块,还有一些第三方正则表达式模块,例如regex模块,它提供了比re模块更加全面、高级和兼容Perl正则表达式语法的功能。
regex模块与re模块类似,提供了re模块中的大多数函数,但它支持更多的正则表达式语法和功能,例如复杂的断言、Unicode属性和匹配嵌套结构等。此外,regex模块的性能也比re模块更好,可以处理更大的正则表达式和更长的文本数据。
总之,Python中的正则表达式模块有很多,其中标准库中的re模块是最常用的。如果需要在处理正则表达式时需要更复杂的语法和功能,可以尝试使用regex模块。
3 正则表达式的分类
正则表达式由一些普通字符和一些元字符(metacharacters)组成。普通字符包括大小写的字母和数字,而元字符则具有特殊的含义,也就是我们要用来做匹配的标记。
3.1 简单的元字符
元字符 | 作用 |
\ | 将下一个字符标记符、或一个向后引用、或一个八进制转义符。 |
^ | 匹配输入字行首。 |
$ | 匹配输入行尾。 |
(星号)* | 匹配前面的子表达式任意次。 |
(加号)+ | 匹配前面的子表达式一次或多次(大于等于1次)。 |
? | 匹配前面的子表达式零次或一次。 |
示例代码:
import re # 匹配开头字符 res1 = re.match('^a', 'abandon') print(res1) # <re.Match object; span=(0, 1), match='a'> print(res1.group()) # a # 匹配结尾字符 res2 = re.match('.*d$', 'wood') print(res2) # <re.Match object; span=(0, 4), match='wood'> print(res2.group()) # wood # 匹配至少出现一次的字符 res3 = re.match('a+', 'aabcd') print(res3) # <re.Match object; span=(0, 2), match='aa'> print(res3.group()) # aa # 匹配一次或零次的字符 res4 = re.match('a?', 'aaabandon') print(res4) # <re.Match object; span=(0, 1), match='a'> print(res4.group()) # a
3.2 用于单字符匹配的元字符
元字符 | 作用 |
. (点) | 匹配除“\n”和"\r"之外的任何单个字符。 |
\d | 匹配一个数字字符。 |
\D | 匹配一个非数字字符。 |
\f | 匹配一个换页符。 |
\n | 匹配一个换行符。 |
\r | 匹配一个回车符。 |
\s | 匹配任何不可见字符,包括空格、制表符、换页符等等。 |
\S | 匹配任何可见字符。 |
\t | 匹配一个制表符。 |
\w | 匹配包括下划线的任何单词字符。 |
\W | 匹配任何非单词字符。 |
注:在元字符后面加一个加号(+)表示匹配一个或多个该类型字符
代码示例一(.):
# 指定要匹配的模式 pattern = "py." # 测试字符串1 test_str1 = "python" result1 = re.match(pattern, test_str1) print(result1) # 输出 <re.Match object; span=(0, 3), match='pyt'>
代码示例二(\d、\D):
# 指定要匹配的模式 pattern = "\d" # 测试字符串2 test_str2 = "The price is 199.99 dollars" result2 = re.findall(pattern, test_str2) print(result2) # 输出['1', '9', '9', '9', '9'] # 指定要匹配的模式 pattern = "\D" # 测试字符串3 test_str3 = "My phone number is 555-1234" result3 = re.findall(pattern, test_str3) print(result3) # 输出 ['M', 'y', ' ', 'p', 'h', 'o', 'n', 'e', ' ', 'n', 'u', 'm', 'b', 'e', 'r', ' ', 'i', 's', ' ', '-']
代码示例四(\s、\S):
# 指定要匹配的模式 pattern1 = r"\s+" # 匹配一个或多个空白字符 pattern2 = r"\S+" # 匹配一个或多个非空白字符 # 测试字符串1 test_str1 = "Hello\tworld\n" result1 = re.findall(pattern1, test_str1) print(result1) # 输出 ['\t', '\n'] result2 = re.findall(pattern2, test_str1) print(result2) # 输出 ['Hello', 'world'] # 测试字符串2 test_str2 = " This is a demo. " result3 = re.findall(pattern1, test_str2) print(result3) # 输出 [' ', ' ', ' '] result4 = re.findall(pattern2, test_str2) print(result4) # 输出 ['This', 'is', 'a', 'demo.']
代码示例四(\w、\W):
# 指定要匹配的模式 pattern1 = r"\w+" # 匹配一个或多个单词字符 pattern2 = r"\W+" # 匹配一个或多个非单词字符 # 测试字符串1 test_str1 = "Hello, world!" result1 = re.findall(pattern1, test_str1) print(result1) # 输出 ['Hello', 'world'] result2 = re.findall(pattern2, test_str1) print(result2) # 输出 [', ', '!'] # 测试字符串2 test_str2 = "This is a demo." result3 = re.findall(pattern1, test_str2) print(result3) # 输出 ['This', 'is', 'a', 'demo'] result4 = re.findall(pattern2, test_str2) print(result4) # 输出 [' ', ' ', ' ', '.']
3.3 用于字符集匹配的元字符
元字符 | 作用 |
[xyz] | 字符集合。匹配所包含的任意一个字符。 |
代码示例:
import re # 指定要匹配的模式 pattern1 = r"[aeiou]" # 匹配任何元音字母 pattern2 = r"[A-Z]" # 匹配任何大写字母 # 测试字符串1 test_str1 = "Hello, world!" result1 = re.findall(pattern1, test_str1) print(result1) # 输出 ['e', 'o', 'o'] # 测试字符串2 test_str2 = "This is a Demo." result2 = re.findall(pattern2, test_str2) print(result2) # 输出 ['T', 'D']
3.4 用于量词匹配的元字符
元字符 | 作用 |
{n} | n是一个非负整数。匹配确定的n次。 |
{n,} | n是一个非负整数。至少匹配n次。 |
{n,m} | m和n均为非负整数,其中n<=m。最少匹配n次且最多匹配m次。 |
代码示例:
import re # 指定要匹配的模式 pattern1 = r"[a-z]{3}" # 匹配任何由三个小写字母组成的连续子串 pattern2 = r"\d{2,3}" # 匹配任何由两个或三个数字组成的连续子串 # 测试字符串1 test_str1 = "apple banana cherry" result1 = re.match(pattern1, test_str1) print(result1) # 输出 <re.Match object; span=(0, 3), match='app'> # 测试字符串2 test_str2 = "1234567890 12 123 1234" result2 = re.findall(pattern2, test_str2) print(result2) # 输出 ['12', '123', '234']
3.5 用于分组匹配的元字符
元字符 | 作用 |
() | 将( 和 ) 之间的表达式定义为“组”(group),并且将匹配这个表达式的字符保存到一个元组中 |
示例代码:
# 指定要匹配的模式 pattern1 = r"(\d{3})-(\d{4})-(\d{4})" # 匹配格式为 3-4-4 的电话号码 pattern2 = r"<(\w+)>.*</\1>" # 匹配任何形如 <tag>value</tag> 的 XML 节点 # 测试字符串1 test_str1 = "My phone number is 123-4567-8901." result1 = re.search(pattern1, test_str1) if result1: area_code, prefix, line_number = result1.groups() print("Area code: {}, Prefix: {}, Line number: {}".format(area_code, prefix, line_number)) else: print("No match.") # 测试字符串2 test_str2 = "<title>This is a title</title>" result2 = re.match(pattern2, test_str2) if result2: tag = result2.group(1) print("Tag name: {}".format(tag)) else: print("No match.")
输出:
Area code: 123, Prefix: 4567, Line number: 8901 Tag name: title
在上面的示例中,我们定义了两个模式 (\d{3})-(\d{4})-(\d{4})
(其中 ()
表示将其包含的字符集合作为一个捕获组,可以在后续的处理中通过 groups()
方法获取该组的内容)和 <(\w+)>.*</\1>
(其中 \1
表示引用第一个捕获组所匹配到的内容)。然后我们使用 re.search
函数搜索两个不同的测试字符串,看看它们是否匹配这些模式。第一个测试字符串中包含格式为 3-4-4
的电话号码,我们将其匹配到了,并进一步提取出了该号码的区号、前缀和线路号信息。而第二个测试字符串是一个 XML 节点,我们将其匹配到了,并进一步提取出了节点的名称。
大家可以根据需要修改这个示例,以实现更复杂的文本处理功能。
- 最后,还有一个元字符" | ",用作将两个匹配条件进行逻辑“或”(or)运算。
示例代码如下:
# 指定要匹配的模式 pattern = r"(cat|dog|bird)\d*" # 匹配任何形如 cat\d* 或 dog\d* 或 bird\d* 的字符串 # 测试字符串 test_str = "I have a cat3 and a dog4 but no bird." results = re.findall(pattern, test_str) if results: print("Matching results: ", results) else: print("No match.")
输出:
Matching results: ['cat', 'dog', 'bird']
4 正则表达式的等价
正则难理解因为里面有一个等价的概念,这个概念大大增加了理解难度,让很多初学者看起来会懵,如果把等价都恢复成原始写法,自己书写正则就超级简单了,就像说话一样去写你的正则了
?,*,+,\d,\w 都是等价字符
- ? 等价于 匹配长度{0,1}
- *等价于 匹配长度{0,}
- +等价于 匹配长度{1,}
- \d 等价于 [0-9]
- \D 等价于 [^0-9]
- \w 等价于 [A-Za-z_0-9]
- \W 等价于 [^A-Za-z_0-9]。
5 贪婪匹配
正则表达式的贪婪匹配和非贪婪匹配是指当正则表达式中有多个可能匹配文本时,它们所选择的匹配方式不同。
贪婪匹配指的是在匹配时尽可能多地匹配所有符合条件的字符串,即优先匹配更长的文本。比如 a.*b
表示以 a
开始,以 b
结束,并且中间任意字符(包括空格)至少出现一次,正则表达式引擎在匹配符合该规则的字符串时会从最左边开始选取满足条件的尽可能多的字符。例如对于字符串 abbbcbbbd
,该正则表达式的贪婪匹配结果为 abbbcbbb
。
相对应的,非贪婪匹配也称为惰性匹配或最小匹配,指的是在匹配时只匹配符合条件的最短的字符串。默认情况下,正则表达式引擎采用贪婪匹配模式,在量词的后面添加 ? 前缀可以将其转化为非贪婪匹配模式。例如 a.*?b
表示以 a
开始,以 b
结束,并且中间任意字符(包括空格)至少出现一次,而加上 ?
后,表示在尽可能短的条件下满足这个规则。例如对于字符串 abbbcbbbd
,该正则表达式的非贪婪匹配结果为 abb
。
- 正则表达式的贪婪或非贪婪匹配取决于量词后是否有一个问号,应根据实际需求来决定。需要注意的是,虽然非贪婪通常更加安全和可靠,但也会导致性能损失,因为引擎需要不断回溯以查找符合条件的文本。
示例代码:
# 原始字符串 str1 = "hello-world-and-hi" # 贪婪匹配,获取第一个连字符到最后一个连字符之间的所有字符 result1_greedy = re.findall(r"-.*-", str1) print(result1_greedy) # 输出 ['-world-and-'] # 非贪婪匹配,只获取第一个连字符到第二个连字符之间的所有字符 result1_non_greedy = re.findall(r"-\w+?-", str1) print(result1_non_greedy) # 输出 ['-world-'] # 原始字符串 s = "hello world, this is a test string." # 贪婪匹配,获取以 h 开头、以空格结尾的所有字符 result_greedy = re.findall(r"h.* ", s) print(result_greedy) # ['hello world, this is a test '] # 非贪婪匹配,获取以 h 开头、以空格结尾的最短字符 result_non_greedy = re.findall(r"h.*? ", s) print(result_non_greedy) # ['hello ', 'his ']