正则是进行字符串处理的时候非常强有力的工具,这里记录一些笔者学习正则时总结的心得。
本文从单个字符的匹配开始,到多个字符匹配,分组和Python的re
模块独有的一些特性逐渐展开。
匹配单个字符
正则表达式单个字符匹配的匹配字符如下表:
字符 | 功能 |
---|---|
. | 匹配任意一个字符(除了\n) |
[] | 匹配[]中列举的字符 |
\d | 匹配数字,即0-9 |
\D | 匹配非数字 |
\s | 匹配空白,即空格、Tab键 |
\S | 匹配非空白 |
\w | 匹配单词字符,即a-z、A-Z、0-9、_;但是需要注意它是匹配unicode编码,因此实际上中文字、日文字等也能被\w匹配到。 |
\W | 匹配非单词字符 |
对于连续的数字或者字符,比如数字从1到7(闭区间),可以用[1-7]
表示。
简单的单个字符匹配如下例:
import re
def main():
target = "速度与激情0"
ret = re.match(r"速度与激情[1-8]", target)
if ret is not None:
print(ret.group())
else:
print("target not found!")
if __name__ == '__main__':
main()
前面提到.
是不可以匹配换行符的,如果想要它可以匹配换行符,那么需要显示的在re.match
中传入参数re.S
,如:
import re
html_content = """dfljkeoigh
dsfhuiehiug
dsfhiueghuie
eiuyu te
efhhueui""" # 三个双引号包裹的字符串里面是可以包含换行符\n的
ret = re.match(r".*", html_content, re.S) # re.S使得 . 可以匹配\n
if ret is not None:
print(ret.group())
else:
print("pattern not found!")
关于.
还有另外一个注意点,当需要匹配一些带有.
的字符串,比如电子邮箱时,如果直接将pattern
的结尾写作r"@163.com"
,那么实际上会匹配到@163Acom, @163tcom
等等。要实现匹配到点的功能,需要运用转义符\
,即写做r"@163\.com"
。对其他特殊字符,例如?$
等,也是一样的。
匹配多个字符
匹配多个字符的相关功能符号:
符号 | 功能 |
---|---|
* | 前一个字符出现任意次,即可有可无 |
+ | 前一个字符至少出现1次 |
? | 前一个字符出现0次或1次 |
{m} | 前一个字符出现m次 |
{m,n} | 前一个字符出现[m, n]次 |
例如需要匹配出一个字符串,第一个字母为大写字母,后面都是小写字母,并且这些小写字母可有可无,那么可以用如下方式:
import re
def match_string(target):
"""匹配一个字符串,第一个字母为大写字母,后面都是小写字母,并且这些小写字母可有可无"""
pattern = r"[A-Z][a-z]*"
ret = re.match(pattern, target)
# 一种并不优雅的写法
if ret is None or len(ret.group()) != len(target):
return False
else:
return True
def main():
assert match_string("Abcd")
assert match_string("A")
assert not match_string("abcd")
assert not match_string("A_e")
assert not match_string("A1iehg")
assert not match_string("A ")
print("Test passed!")
if __name__ == '__main__':
main()
匹配开头结尾
符号 | 功能 |
---|---|
^ | 匹配开头 |
$ | 匹配结尾 |
前面我们用过len(re.match(pattern, target).group())
来判断是否匹配对象target
是否整个都满足我们的pattern
,但是实际上我们可以用$
更优雅的做到这一点。
例如我们需要测试一个字符串是否符合python的变量名要求:
import re
def main():
names = ["age", "_age", "1age", "age1", "a_age", "age_1_", "age!", "a#123"]
for name in names:
ret = re.match(r"[a-zA-Z_][a-zA-Z0-9_]*$", name)
if ret is not None:
print("变量名 %s 符合要求" % name)
else:
print("变量名 %s 不符合要求" % name)
if __name__ == '__main__':
main()
匹配分组
正则表达式还有对匹配进行分组的功能,配合以下符号,可以进行复杂字符串的分割保存:
字符 | 功能 |
---|---|
| | 匹配左右任意一个表达式 |
(ab) | 将括号中的字符作为一个分组 |
\num | 引用分组num匹配到的字符串,适用于需要配对的情况 |
(?P<name>) | 为分组起别名,注意P是大写 |
(?P=name) | 引用别名为name分组匹配到的字符串,注意P是大写 |
-
()
进行分组
()
可以进行分组,配合re.match().group()
取出匹配到的一组内容。
例如在匹配拨入的固定电话号码是否符合规则时,想要同时取得拨入电话的区号,那么可以这么写:
import re
def main():
numbers = ["0576-1234567", "010-7654281", "020-8970273", "012-dj284712"]
pattern = r"^([0-9]{3,4})-[0-9]{7}$"
for number in numbers:
ret = re.match(pattern, number)
if ret is not None:
print("拨入号码符合要求,区号为%s" % ret.group(1))
else:
print("拨入号码不符合要求")
if __name__ == '__main__':
main()
输出结果:
拨入号码符合要求,区号为0576
拨入号码符合要求,区号为010
拨入号码符合要求,区号为020
拨入号码不符合要求
()
的另一个作用是限定范围。还是以匹配电子邮箱为例,常用的电子邮箱后缀有许多,比如gmail.com, 163.com, qq.com, hotmail.com
等等,如何让正则可以匹配一组后缀里面的任意一个?可以使用|
,这个符号的含义等同于“或”,但是在使用时需要配合一组小括号限定范围,否则的话,就会判断用|
分割开的两部分是否能匹配target
了。此时正则表达式的结尾可写为@(163|gmail|qq|hotmail).com$
。
- 分组的引用
很多情况下,正则表达式处理的字符串中,会有成对出现的字符。例如html的标签就要求成对出现,前面有<h1>
,就要求有面有</h1>
。这里就需要用到对分组的引用。对分组的引用有两种方式:用数字引用,和用别名引用。
最简单的引用方式是对分组用数字引用,例如:
import re
def main():
html_labels = ["<h1>content</h1>", "<h1>content</h2>"]
pattern = r"^<(\w+)>.+<(/\1)>$" # 这里的\1引用的就是第一个分组(\w+)的内容
for label in html_labels:
if re.match(pattern, label):
print("html标签 %s 格式符合要求" % label)
else:
print("html标签 %s 格式不符合要求" % label)
if __name__ == '__main__':
main()
输出结果为:
html标签 <h1>content</h1> 格式符合要求
html标签 <h1>content</h2> 格式不符合要求
但是如果一个字符串中,有很多对的成对信息,那么用数字引用很容易搞混,就可以采用更加直观的用别名引用的方式。现需要用?P<name>
为分组起别名,然后用?P=name
引用别名,如需要匹配<h1><body>something</body></h1>
:
import re
def main():
html_labels = ["<body><h1>content</h1></body>", "<body><h1>content</h2></body>", "<body><h1>content</body></h1>"]
pattern = r"^<(?P<body>\w+)><(?P<h1>\w+)>.+</(?P=h1)></(?P=body)>$" # 用别名body和h1引用分组
for label in html_labels:
if re.match(pattern, label):
print("html标签 %s 格式符合要求" % label)
else:
print("html标签 %s 格式不符合要求" % label)
if __name__ == '__main__':
main()
得到结果:
html标签 <body><h1>content</h1></body> 格式符合要求
html标签 <body><h1>content</h2></body> 格式不符合要求
html标签 <body><h1>content</body></h1> 格式不符合要求
Python中re模块的高级功能
search - 查找内容
在一个字符串中搜索想要的内容而不是匹配整个字符串。
例如在一个字符串中找到需要的数字:
import re
def main():
str = "世界人均年阅读量为3.5本"
pattern = r"\d+\.*\d*"
print(re.search(pattern, str).group())
if __name__ == '__main__':
main()
输出:
3.5
search
与match
的区别在于,match会从字符串开头进行匹配,如果开头不符合pattern
就会匹配失败。
findall - 查找所有内容
当字符串中有多个需要提取的内容时,可以用findall
返回一个包含所有匹配结果的列表。
还是用上面的例子:
import re
def main():
str = "世界人均年阅读量为3.5本,中国的人均年阅读量为2本"
pattern = r"\d+\.*\d*"
print(re.findall(pattern, str))
if __name__ == '__main__':
main()
findall会返回一个列表
['3.5', '2']
而在这个例子中如果使用search
会返回第一个符合pattern的数字3.5。
sub - 替换选中内容
sub函数的用法为
re.sub(pattern, repl, string, count=0, flags=0)
参数pattern
代表正则中的模式字符串,repl
代表被替换字符串,这里可以是一个字符串,也可以是一个返回值为字符串的函数,string
代表将会被替换的字符串,count
代表需要替换的个数,用于部分替换搜索到的内容。flags
是标志位,如re.i, re.L, re.M, re.S
等
还是用上面的例子,我们想要在每个数字上加2,可以用如下方式:
import re
def operate_on_str(matched):
num_str = matched.group("number")
return str(float(num_str) + 2.0)
def main():
str = "世界人均年阅读量为3.5本,中国的人均年阅读量为2本"
pattern = r"(?P<number>\d+\.*\d*)"
print(re.sub(pattern, operate_on_str, str))
if __name__ == '__main__':
main()
输出结果:
世界人均年阅读量为5.5本,中国的人均年阅读量为4.0本
利用re
模块的sub
可以实现比str.replace()
更加复杂的功能。
split - 切割字符串
split会用正则表达式去匹配字符串,按照匹配到的字串将字符串进行切分,并返回切分后的字符串列表。
re.split(pattern, string[, maxsplit=0, flags=0])
参数pattern
代表正则中的模式字符串,string
代表将会被替换的字符串,maxsplit
代表分割次数,默认为0即不限次数。flags
是标志位,如re.i, re.L, re.M, re.S
等
一个例子如下:
import re
def main():
str = "name: Wang Er; age: 29"
pattern = r"[: ;]" # 分隔符
print(re.split(pattern, str))
if __name__ == '__main__':
main()
输出结果如下:
['name', '', 'Wang', 'Er', '', 'age', '', '29']
complie - 预编译
当需要对同一个模式进行匹配时,为了增加正则匹配效率,重复利用pattern,可以先对pattern进行预编译。利用compile函数
,可以生成一个正则表达式对象,供match()
和search()
两个函数使用。
import re
pat = re.compile(r"\d+") # 匹配至少一个数字
strLst = ["这个字符串中的数字为22", "27436267d27", "string90"]
for item in strLst:
print(pat.search(item).group()) # 用预编译过的pattern搜索字符串
这样在重复利用时,就可以省去编译的时间,增加效率。
re
模块的flags
在很多python的re
模块的方法中,都可以传入参数flags
,从而获得一些特殊的性能。如下表:
符号 | 含义 |
---|---|
re.I | 忽略大小写 |
re.L | 表示特殊字符集 |
re.M | 多行模式 |
re.S | 为. 匹配包括换行符在内的任意字符 |
re.U | 特殊字符集 \w, \W, \b, \B, \d, \D, \s, \S 依赖于 Unicode 字符属性数据库 |
re.X | 为了增加可读性,忽略空格和# 后面的注释 |
比如需要匹配that/That
,除了使用[tT]hat
以外,更优雅的写法是使用re.I
标记,但是需要注意的是,它也会匹配到tHat/tHAT
等等,在使用的时候需要考虑清楚自己的需求。
import re
pat = re.compile(r"that", re.I) # 用re.I flag来忽略大小写
strLst = ["This is not that", "That is a good thing", "THAT is a good thing"]
for item in strLst:
print(pat.search(item).group()) # 用预编译过的pattern搜索字符串
贪婪匹配
正则模式的默认匹配方式是贪婪匹配,也就是会为写在前面的正则项尝试匹配尽可能多的字符。如果需要某一个匹配项使用非贪婪模式,那么在该项后面加一个?
即可。
import re
target = "Thisisastring"
pattern = r"(\w+)(\w*)"
print(re.match(pattern, target).groups())
第一个(\w+)
会匹配尽可能多的字符,因此输出为:
('Thisisastring', '')
下面看看非贪婪模式的输出:
import re
target = "Thisisastring"
pattern = r"(\w+?)(\w*)" # 让第一个组用非贪婪模式进行匹配
print(re.match(pattern, target).groups())
输出结果:
('T', 'hisisastring')
网友评论