Skip to content

正则表达式

在处理字符串时,经常会有查找符合某些复杂规则的字符串的需求。正则表达式就是用于描述这些规则的工具

在线正则表达式测试

1
2
3
4
5
6
7
8
9
import re
a = 'C|C++|Python'
r = re.findall('Python', a)
print(r)
print(re.findall('PHP', a))
"""
['Python']
[]
"""

元字符

元字符 说明
\d 匹配数字字符
\D 匹配非数字字符
\w 匹配字母、数字、下划线或汉字
\W 匹配除字母、数字、下划线或汉字以外的字符
\s 匹配单个空白字符(包括Tab键和换行符)
\S 匹配单个空白字符以外的所有字符
. 匹配除换行符以外的任意字符
\b 匹配单词的开始或结束,单词的分界符通常是空格、标点符号或者换行
^ 匹配字符串的开始
$ 匹配字符串的结束
\A 匹配字符串开头
\Z 匹配字符串结尾,如果存在换行,只匹配到换行前的结束字符串
\z 匹配字符串结尾,结果存在换行,同时还会匹配换行符
\G 匹配最后匹配完成的位置
\n 匹配一个换行符
\t 匹配一个制表符
1
2
3
4
5
6
7
In [3]: a = 'C/c++0,7,333,Pythonhhh2333,666'

In [4]: print(re.findall('\d', a))
['0', '7', '3', '3', '3', '2', '3', '3', '3', '6', '6', '6']

In [5]: print(re.findall('\D', a))
['C', '/', 'c', '+', '+', ',', ',', ',', 'P', 'y', 't', 'h', 'o', 'n', 'h', 'h', 'h', ',']

字符集/字符类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
In [6]: s = 'abc, acc, adc, aec, afc, ahc'

In [7]: print(re.findall('a[cf]c', s))
['acc', 'afc']

In [8]: print(re.findall('[cf]', s))
['c', 'c', 'c', 'c', 'c', 'f', 'c', 'c']

In [9]: print(re.findall('a[^cf]c', s))
['abc', 'adc', 'aec', 'ahc']

In [10]: print(re.findall('a[c-f]c', s))
['acc', 'adc', 'aec', 'afc']

概括字符集

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
In [1]: a = '123hhh996ai'

In [4]: print(re.findall('[^0-9]', a))
['h', 'h', 'h', 'a', 'i']

In [5]: print(re.findall('[0-9]', a))
['1', '2', '3', '9', '9', '6']

In [6]: print(re.findall('\w', a))
['1', '2', '3', 'h', 'h', 'h', '9', '9', '6', 'a', 'i']

In [7]: a = '123hhh996ai&23333'

In [8]: print(re.findall('\w', a))
['1', '2', '3', 'h', 'h', 'h', '9', '9', '6', 'a', 'i', '2', '3', '3', '3', '3']

In [10]: a = '123hhh996ai&23___333'

In [11]: print(re.findall('[A-Za-z0-9_]', a))
['1', '2', '3', 'h', 'h', 'h', '9', '9', '6', 'a', 'i', '2', '3', '_', '_', '_', '3', '3', '3']

In [12]: print(re.findall('\w', a))
['1', '2', '3', 'h', 'h', 'h', '9', '9', '6', 'a', 'i', '2', '3', '_', '_', '_', '3', '3', '3']

In [13]: print(re.findall('\W', a))
['&']

In [14]: a = '123hhh996ai&23___3 3\n3'

In [15]: print(re.findall('\W', a))
['&', ' ', '\n']

# 要想匹配给定字符串中任意一个汉字,可以使用 '[\u4e00-\u9fa5]'
In [62]: print(re.findall('[\u4e00-\u9fa5]', "哈哈哈,666,很强"))
['哈', '哈', '哈', '很', '强']

数量词

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
In [16]: print(re.findall('[a-z][a-z][a-z]', "python 111111java c++hhhphhhhp"))
['pyt', 'hon', 'jav', 'hhh', 'phh', 'hhp']

In [17]: print(re.findall('[a-z]{3}', "python 111111java c++hhhphhhhp"))
['pyt', 'hon', 'jav', 'hhh', 'phh', 'hhp']

In [19]: print(re.findall('[a-z]{3,6}', "python 111111java php")) # 匹配前面的字符最少 3 次,最多 6 次
['python', 'java', 'php']

In [60]: print(re.findall('[a-z]{3,}', "python 111111java php")) # 匹配前面的字符最少 3 次
['python', 'java', 'php']

贪婪与非贪婪

1
2
3
4
5
In [19]: print(re.findall('[a-z]{3,6}', "python 111111java php")) # 贪婪
['python', 'java', 'php']

In [20]: print(re.findall('[a-z]{3,6}?', "python 111111java php")) # 非贪婪
['pyt', 'hon', 'jav', 'php']

使用.*进行匹配时,可能有时候匹配到的并不是我们想要的结果

1
2
3
4
5
6
7
8
9
import re
content = 'Hello 1234567 World This is a Regex Demo'
result = re.match('^He.*(\d+).*Demo$', content)
print(result)
print(result.group(1))
"""
<re.Match object; span=(0, 40), match='Hello 1234567 World This is a Regex Demo'>
7
"""

这里我们依然想获取中间的数字,所以中间依然写的是(\d+)。而数字两侧由于内容比较杂乱,所以想省略来写,都写成.*。最后,组成^He.*(\d+).*Demo$,看样子并没有什么问题

但是运行后发现,只得到了一个数字 7,这是怎么回事?

这里涉及一个贪婪匹配和非贪婪匹配的问题。在贪婪匹配下,.*会匹配尽可能多的字符。正则表达式中.*后面是\d+,也就是至少一个数字,并没有指定具体多少个数字,因此.*就尽可能匹配多的字符,这里就把123456匹配了,给\d+留下一个可满足条件的数字 7,最后得到的内容就只有数字 7 了

但这很明显会给我们带来很大的不便。有时候,匹配结果会莫名其妙少了一部分内容。其实,这里只需要使用非贪婪匹配就好了。非贪婪匹配的写法是.*?,多了一个?,那么它可以达到怎样的效果?

1
2
3
4
5
6
7
8
9
import re
content = 'Hello 1234567 World This is a Regex Demo'
result = re.match('^He.*?(\d+).*Demo$', content)
print(result)
print(result.group(1))
"""
<re.Match object; span=(0, 40), match='Hello 1234567 World This is a Regex Demo'>
1234567
"""

此时就可以成功获取1234567了。原因可想而知,贪婪匹配是尽可能匹配多的字符,非贪婪匹配就是尽可能匹配少的字符。当.*匹配到Hello后面的空白字符时,再往后的字符就是数字了,而\d+恰好可以匹配,那么这里.*?就不再进行匹配,交给\d+去匹配后面的数字。所以这样.*?匹配了尽可能少的字符,\d+的结果就是1234567

所以说,在做匹配的时候,字符串中间尽量使用非贪婪匹配,也就是用.*?来代替.*,以免出现匹配结果缺失的情况

但这里需要注意,如果匹配的结果在字符串结尾,.*?就有可能匹配不到任何内容了,因为它会匹配尽可能少的字符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import re
content = 'http://weibo.com/comment/kEraCN'
result1 = re.match('http.*?comment/(.*?)', content)
result2 = re.match('http.*?comment/(.*)', content)
print('result1', result1.group(1))
print('result2', result2.group(1))
"""
result1 
result2 kEraCN
"""

可以观察到.*?没有匹配到任何结果,而.*则尽量匹配多的内容,成功得到了匹配结果

匹配0次1次或者无限次

* 表示匹配前面的字符零次或无限多次

1
2
In [22]: print(re.findall('python*', "pytho0python1pythonn2"))
['pytho', 'python', 'pythonn']

+ 匹配1次或者无限多次

1
2
In [23]: print(re.findall('python+', "pytho0python1pythonn2"))
['python', 'pythonn']

? 匹配0次或1次

1
2
In [24]: print(re.findall('python?', "pytho0python1pythonn2"))
['pytho', 'python', 'python']
1
2
3
4
5
In [25]: print(re.findall('python{1,2}', "pytho0python1pythonn2")) # 贪婪
['python', 'pythonn']

In [26]: print(re.findall('python{1,2}?', "pytho0python1pythonn2")) # 非贪婪
['python', 'python']

边界匹配符

^表示行的开始;$表示行的末尾

1
^tm

该表达式表示要匹配子串 tm 的开始位置是行头,如"tm equal Tomorrow Moon"就可以匹配,而"Tomorrow Moon equal tm"则不匹配。但如果使用

1
tim$

后者可以匹配而前者不能匹配。如果要匹配的子串出现在字符串的任意部分,那么可以直接写成

1
tm

这样两个字符串就都可以匹配了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
In [27]: print(re.findall('\d{4,8}', "100001"))
['100001']

In [28]: print(re.findall('\d{4,8}', "101"))
[]

In [29]: print(re.findall('\d{4,8}', "123456789"))
['12345678']

In [30]: print(re.findall('^\d{4,8}$', "123456789"))
[]

In [31]: print(re.findall('^000', "100000001"))
[]

In [32]: print(re.findall('000$', "100000001"))
[]

匹配模式参数

findall() 的语法格式为:re.findall(pattern, string, flags=0)

其中 flags 为可选参数,表示标志位,用于控制匹配方式,如是否区分大小写

常用标志

标志 说明
I 或 IGNORECASE 执行不区分大小写的匹配
S 或 DOTALL 使用 (.) 字符匹配所有字符,包括换行符
M 或 MULTILINE 将 ^ 和 $ 用于包括整个字符串的开始和结尾的每一行(默认情况下,仅适用于整个字符串的开始和结尾处)
X 或 VERBOSE 忽略模式字符串中未转义的空格和注释
A 或 ASCII 对于 \w、\W、\b、\B、\d、\D、\s 和 \S 只进行 ASCII 匹配
U 根据 Unicode 字符集解析字符。这个标志影响 \w, \W, \b 和 \B
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
In [44]: print(re.findall('c#', "PythonC#JavaPHP"))
[]

In [45]: print(re.findall('c#', "PythonC#JavaPHP", re.I)) 
['C#']

In [50]: print(re.findall('c#.{1}', "PythonC#\nJavaPHP", re.I))
[]

In [51]: print(re.findall('c#.{1}', "PythonC#\nJavaPHP", re.I | re.S)) 
['C#\n']

search和match函数

re.match(pattern, string, flags=0)

从字符串的开始处进行匹配,如果在起始位置匹配成功,则返回 Match 对象,否则返回 None

Match 对象中包含了匹配值的位置和匹配数据。其中,要获取匹配值的起始位置可以使用 Match 对象的 start() 方法;要获取匹配值的结束位置可以使用 end() 方法;通过 span() 方法可以返回匹配位置的元组;通过 string 属性可以获取要匹配的字符串

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
pattern = r"mr_\w+"
string = "MR_SHOP mr_shop"
match = re.match(pattern, string, re.I)
print(f"匹配值的起始位置: {match.start()}")
print(f"匹配值的结束位置: {match.end()}")
print(f"匹配位置的元组: {match.span()}")
print(f"要匹配的字符串: {match.string}")
print(f"匹配数据: {match.group()}")
"""
匹配值的起始位置: 0
匹配值的结束位置: 7
匹配位置的元组: (0, 7)
要匹配的字符串: MR_SHOP mr_shop
匹配数据: MR_SHOP
"""
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import re
content = 'Hello 1234567 World This is a Regex Demo'
result = re.match('^Hello\s(\d+)\sWorld', content)
print(result.group())
print(result.group(1))
print(result.span())
"""
Hello 1234567 World
1234567
(0, 19)
"""

group() 会输出完整的匹配结果,group() 会输出第一个被 () 包围的匹配结果。如果正则表达式后面还有 () 包含的内容,那么可以依次用 group(2)、group(3) 等来获取

re.search(pattern, string, flags=0)

用于在整个字符串中搜索第一个匹配的值,如果匹配成功,则返回 Match 对象,否则返回 None

re.compile(pattern, flags=0)

将正则表达式的样式编译为一个 正则表达式对象 (正则对象),可以用于匹配,通过这个对象的方法 match(), search()

向 re.compile 传入一个字符串值,表示正则表达式,它将返回一个 Regex 模式对象(或者就简称为 Regex 对象)

序列

1
2
prog = re.compile(pattern)
result = prog.match(string)

等价于

1
result = re.match(pattern, string)

如果需要多次使用这个正则表达式的话,使用 re.compile() 和保存这个正则对象以便复用,可以让程序更加高效。

Pattern.search(string[, pos[, endpos]])

扫描整个 string 寻找第一个匹配的位置, 并返回一个相应的匹配对象。如果没有匹配,就返回 None

可选的第二个参数 pos 给出了字符串中开始搜索的位置索引;默认为 0,它不完全等价于字符串切片; '^' 样式字符匹配字符串真正的开头,和换行符后面的第一个字符,但不会匹配索引规定开始的位置。

可选参数 endpos 限定了字符串搜索的结束;它假定字符串长度到 endpos , 所以只有从 pos 到 endpos - 1 的字符会被匹配。如果 endpos 小于 pos,就不会有匹配产生;另外,如果 rx 是一个编译后的正则对象, rx.search(string, 0, 50) 等价于 rx.search(string[:50], 0)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
pattern = re.compile("d")
s1 = pattern.search("dog")     # Match at index 0
print(s1)
print(s1.group())
s2 = pattern.search("dog", 1)  # No match; search doesn't include the "d"
print(s2)
"""
<_sre.SRE_Match object; span=(0, 1), match='d'>
d
None
"""

Pattern.match(string[, pos[, endpos]])

如果 string 的开始位置能够找到这个正则样式的任意个匹配,就返回一个相应的匹配对象。如果不匹配,就返回 None

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
pattern = re.compile("o")
m1 = pattern.match("dog")      # No match as "o" is not at the start of "dog".
print(m1)
m2 = pattern.match("dog", 1)   # Match as "o" is the 2nd character of "dog".
print(m2)
print(m2.group())
"""
None
<_sre.SRE_Match object; span=(1, 2), match='o'>
o
"""

利用括号分组

用 findall() 方法进行匹配,如果在指定的模式字符串中,包含分组,则返回与分组匹配的文本列表

1
2
3
4
5
6
7
pattern = r"[1-9]{1,3}(\.[0-9]{1,3}){3}"
str1 = "127.0.0.1 192.168.1.66"
match = re.findall(pattern, str1)
print(match)
"""
['.1', '.66']
"""

得到的结果是分根据分组进行匹配的结果,即"(\.[0-9]{1,3})"匹配的结果。如果想获取整个模式字符串的匹配,可以将整个模式字符串使用一对小括号进行分组,然后在获取结果时,只取返回值列表中的每个元素(是一个元组)的第一个元素

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
pattern = r"([1-9]{1,3}(\.[0-9]{1,3}){3})"
str1 = "127.0.0.1 192.168.1.66"
match = re.findall(pattern, str1)
print(match)
for item in match:
    print(item[0])
"""
[('127.0.0.1', '.1'), ('192.168.1.66', '.66')]
127.0.0.1
192.168.1.66
"""
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
In [68]: print(re.findall('(Python){1}', "PythonPythonPythonPython"))
['Python', 'Python', 'Python', 'Python']

In [69]: print(re.findall('(Python){2}', "PythonPythonPythonPython"))
['Python', 'Python']

In [70]: print(re.findall('(Python){3}', "PythonPythonPythonPython"))
['Python']

In [71]: print(re.findall('(Python){4}', "PythonPythonPythonPython"))
['Python']

In [67]: print(re.findall('(Python)', "PythonPythonPythonPython"))
['Python', 'Python', 'Python', 'Python']

In [72]: print(re.findall('(Python)(Python)', "PythonPythonPythonPython"))
[('Python', 'Python'), ('Python', 'Python')]

In [42]: print(re.findall('(Python)(Python)(Python)', "PythonPythonPythonPython"))
[('Python', 'Python', 'Python')]

In [73]: print(re.findall('((Python)(Python))', "PythonPythonPythonPython"))
[('PythonPython', 'Python', 'Python'), ('PythonPython', 'Python', 'Python')]

In [74]: print(re.findall('((Python){3})', "PythonPythonPythonPython"))
[('PythonPythonPython', 'Python')]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
s = "life is short, i use python, i love python"
r = re.search("life(.*)python(.*)python", s)
print(r.group(0))
print(r.group(1))
print(r.group(2))
print(r.group(0, 1, 2))
print(r.groups())
"""
life is short, i use python, i love python
 is short, i use 
, i love
('life is short, i use python, i love python', ' is short, i use ', ', i love ')
(' is short, i use ', ', i love ')
"""

转义字符

例如用正则表达式匹配诸如"127.0.0.1"这样格式的 IP 地址。如果直接使用点字符是不对的,需要使用转义字符\

括号在正则表达式中也是一个元字符

通过在字符串的第一个引号之前加上 r,可以将该字符串标记为原始字符串,它不包括转义字符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
print(re.findall("\d", "12hh"))
print(re.findall("\bmr\w*\b", "mrsoft"))
print(re.findall("\\bmr\w*\\b", "mrsoft"))
print(re.findall(r"\bmr\w*\b", "mrsoft"))
"""
['1', '2']
[]
['mrsoft']
['mrsoft']
"""

我们知道正则表达式定义了许多匹配模式,如.匹配除换行符以外的任意字符,但是如果目标字符串里面就包含.,那该怎么办?

这里就需要用到转义匹配了

1
2
3
4
5
6
7
import re
content = '(百度)www.baidu.com'
result = re.match('\(百度\)www\.baidu\.com', content)
print(result)
"""
<re.Match object; span=(0, 17), match='(百度)www.baidu.com'>
"""

用管道匹配多个分组

字符|称为“管道”。希望匹配许多表达式中的一个时,就可以使用它。例如,正则表达式r"Batman|Tina Fey"将匹配"Batman""Tina Fey"

1
2
3
4
print(re.findall(r"Batman|Tina Fey", "Batman and Tina Fey."))
"""
['Batman', 'Tina Fey']
"""

也可以使用管道来匹配多个模式中的一个,作为正则表达式的一部分。例如,假设希望匹配"Batman""Batmobile""Batcopter""Batbar"中任意一个。因为所有这些字符串都以"Bat"开始,所以如果能够只指定一次前缀,就很方便

1
2
3
4
5
6
7
8
bat_regex = re.compile(r"Bat(man|mobile|copter|bat)")
s = bat_regex.search("Batmobile lost a wheel")
print(s.group())
print(s.group(1))
"""
Batmobile
mobile
"""

re.sub 正则替换

除了使用正则表达式提取信息外,有时候还需要借助它来修改文本。比如,想要把一串文本中的所有数字都去掉,如果只用字符串的 replace() 方法,那就太繁琐了,这时可以借助 sub() 方法

语法格式:re.sub(pattern, repl, string, count, flags)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
In [52]: print(re.sub("C#", "GO", "PythonC#JAVAC++"))
PythonGOJAVAC++

In [53]: print(re.sub("C#", "GO", "PythonC#JAVAC#C++"))
PythonGOJAVAGOC++

In [54]: print(re.sub("C#", "GO", "PythonC#JAVAC#C++", 1))
PythonGOJAVAC#C++


In [55]: def convert(value):
    ...:     pass

In [56]: print(re.sub("C#", convert, "PythonC#JAVAC#C++", 1))
PythonJAVAC#C++

In [57]: print(re.sub("C#", convert, "PythonC#JAVAC#C++"))
PythonJAVAC++

In [58]: def convert(value):
    ...:     matched = value.group()
    ...:     return f"!!{matched}!!"

In [59]: print(re.sub("C#", convert, "PythonC#JAVAC#C++"))
Python!!C#!!JAVA!!C#!!C++

把函数作为参数传递

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def convert(value):
    matched = value.group()
    if int(matched) >= 6:
        return "9"
    else:
        return "0"
print(re.sub("\d", convert, "A8C3721D86"))
"""
A9C0900D99
"""

使用正则表达式分割字符串

语法格式:re.split(pattern, string, [maxsplit], [flags])

1
2
3
4
5
6
7
pattern = r"[?&]"
url = "http://www.baidu.com/login.html?username='hhh'&pwd='lalala'"
result = re.split(pattern, url)
print(result)
"""
['http://www.baidu.com/login.html', "username='hhh'", "pwd='lalala'"]
"""