Python进阶 - 正则

作者: ChaoesLuol | 来源:发表于2020-02-23 16:10 被阅读0次

正则是进行字符串处理的时候非常强有力的工具,这里记录一些笔者学习正则时总结的心得。
本文从单个字符的匹配开始,到多个字符匹配,分组和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

searchmatch的区别在于,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')

相关文章

网友评论

    本文标题:Python进阶 - 正则

    本文链接:https://www.haomeiwen.com/subject/aabjqhtx.html