Python 常用模块之re模块(正则表达式)

200 阅读8分钟

一、基本概念

什么是正则?

正则就是用一些具有特殊含义的符号组合到一起(称为正则表达式)来描述字符或者字符串组成规则的方法。

正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。简单地说,正则表达式就是一门专门用于处理字符串的增强语法。

本质上是嵌入在Python中的一种微小的、高度专业化的编程语言,可通过re模块获得。

为什么要用正则?

给定一个正则表达式和另一个字符串,我们可以达到如下的目的:

  1. 给定的字符串是否符合正则表达式的过滤逻辑(称作“匹配”)
  2. 可以通过正则表达式,从字符串中获取我们想要的特定部分。

正则表达式的特点是:

  1. 灵活性、逻辑性和功能性非常强;
  2. 可以迅速地用极简单的方式达到字符串的复杂控制。

二、正则的使用

常用匹配模式(元字符)
模 式描 述
^匹配字符串的开头
$匹配字符串的末尾。
.匹配任意字符,除了换行符。当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符。
[...]用来表示一组字符,单独列出:[amk] 匹配 'a','m'或'k'
[^...]不在[ ]中的字符:[^abc] 匹配除了a,b,c之外的字符。
*匹配0个或多个的表达式。
+匹配1个或多个的表达式。
?匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式
{ n}精确匹配 n 个前面表达式。例如, o{2} 不能匹配 "Bob" 中的 "o",但是能匹配 "food" 中的两个 o。
{ n,}匹配 n 个前面表达式。例如, o{2,} 不能匹配"Bob"中的"o",但能匹配 "foooood"中的所有 o。"o{1,}" 等价于 "o+"。"o{0,}" 则等价于 "o*"。
{ n, m}匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式
a | b匹配a或b
(re)匹配括号内的表达式,也表示对正则表达式分组并记住匹配的文本
\w匹配字母数字及下划线
\W匹配非字母数字及下划线
\s匹配任意空白字符,等价于 [ \t\n\r\f]
\S匹配任意非空字符
\d匹配任意数字,等价于 [0-9].
\D匹配任意非数字
\A匹配字符串开始(开头)
\Z匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串。
\z匹配字符串结束(结尾)
\G匹配最后匹配完成的位置。
\n匹配一个换行符。
\t匹配一个制表符。

实际应用(import re)

单个简单使用:

  • \w与\W

    print(re.findall('\w', 'abc 123'))
    # ['a', 'b', 'c', '1', '2', '3']
    print(re.findall('\W', 'abc 123'))
    # [' ']
    
  • \s与\S

    print(re.findall('\s', 'abc 123'))
    # [' ']
    print(re.findall('\S', 'abc 123'))
    # ['a', 'b', 'c', '1', '2', '3']
    
  • \n、\t都是空,都可以被\s匹配

    print(re.findall('\s', 'abc \n def \t 123'))
    # [' ', '\n', ' ', ' ', '\t', ' ']
    
  • \n与\t

    print(re.findall(r'\n', 'abc def \n123'))
    # ['\n']
    print(re.findall(r'\t', 'abc def\t123'))
    # ['\t']
    

    上面的代码块中,“r”的作用是告诉编译器,后面引号里的内容是我指定的正则规则,而不是我用它原来的功能。==(但凡写正则最好在前加上r)==

  • \d与\D

    print(re.findall('\d', 'abc 123'))
    # ['1', '2', '3']
    print(re.findall('\D', 'abc 123'))
    # ['a', 'b', 'c', ' ']
    
  • \A与\Z

    print(re.findall('\Aab', 'abc 123'))   # \A ==> ^
    # ['ab']
    print(re.findall('123\Z', 'abc 123'))  # \Z ==> $
    # ['123']
    
    print(re.findall('^abc$', 'abcabc'))
    # []
    print(re.findall('^abc$', 'abc'))
    # ['abc']
    

重复匹配:| . | * | ? | .* | .*? | + | {n,m} |

  • .(点号):匹配除了\n之外的任意一个字符,指定re.DOTALL之后才能匹配换行符

    print(re.findall('a.b', 'a1b a2b a b abbbb a\nb a\tb a*b'))
    # ['a1b', 'a2b', 'a b', 'abb', 'a\tb', 'a*b']
    
    print(re.findall('a.b', 'a1b a2b a b abbbb a\nb a\tb a*b', re.DOTALL))
    # ['a1b', 'a2b', 'a b', 'abb', 'a\nb', 'a\tb', 'a*b']
    
  • *(星号):左侧字符单独重复0次或无穷次,有几个就拿几个,全都薅走,就算没有也要薅走。性格贪婪

    print(re.findall('ab*', 'a ab abb abbbbbbbb bbbbbbbb'))
    # ['a', 'ab', 'abb', 'abbbbbbbb']
    
  • +(加号):左侧字符单独重复1次或无穷次,也就比星号克制那么一丢丢,没有就留着,只要有就一定要薅。性格贪婪

    print(re.findall('ab+', 'a ab abb abbbbbbbb bbbbbbbb'))
    # ['ab', 'abb', 'abbbbbbbb']
    
  • ?(问号):左侧字符单独重复0次或1次,比起上面俩不要脸的货可强太多了,但也不是好人啊。

    print(re.findall('ab?', 'a ab abb abbbbbbbb bbbbbbbb'))
    # ['a', 'ab', 'ab', 'ab']
    
  • {n,m}:左侧字符自定重复n次到m次。

    形式解释
    {0,}相当于 *
    {1,}相当于 +
    {0,1}相当于 ?
    {n}单独一个n代表只出现n次,多一次不行少一次也不行
    print(re.findall('ab{2,5}', 'a ab abb abbb abbbb abbbbbbbb bbbbbbbb'))
    # ['abb', 'abbb', 'abbbb', 'abbbbb']
    

使用正则表达式提取出字符串中的数字(整数和小数)
print(re.findall('\d+\.?\d*', "asdfasdf123as1111111.123dfa12adsf1asdf3"))
# ['123', '1111111.123', '12', '1', '3']

问题是会有下面这种状况发生:

print(re.findall('\d+\.?\d*', "as1111111.dfa"))
# ['1111111.']

这个时候我们就可以通过对列表进行第二次正则筛选来解决。


贪婪与非贪婪
source = '<html><head><title>Title</title>'

我们要把上面的字符串中的所有html标签都提取出来,得到下面这样的一个列表

['<html>', '<head>', '<title>', '</title>']

我们很容易想到使用正则表达式 <.*>

print(re.findall(r'<.*>',source))

可是运行的结果却是下面这样:

['<html><head><title>Title</title>']

在正则表达式中,‘*’和‘+’都是贪婪地,使用他们时,会尽可能多的匹配内容,所以, <.*> 中的 星号(表示任意次数的重复),一直匹配到了 字符串最后的 </title> 里面的e。

解决这个问题,就需要使用非贪婪模式,让其尽可能少的匹配内容,也就是在星号后面加上 ? ,变成 <.*?>

运行结果:

print(re.findall(r'<.*?>', source))
# ['<html>', '<head>', '<title>', '</title>']

[ ]匹配指定字符之一

  • [ ]内单独指定、范围指定

    print(re.findall('a[0-5]b', 'a1111111b a3b a4b a9b aXb a b a\nb', re.DOTALL))
    # ['a3b', 'a4b']
    print(re.findall('a[012345]b', 'a1111111b a3b a4b a9b aXb a b a\nb', re.DOTALL))
    # ['a3b', 'a4b']
    
  • [ ]内联合指定,可以指定范围或符号,不支持语法

    print(re.findall('a[0-9A-Z ]b', 'a1111111b a3b a4b a9b aXb a b'))
    # ['a3b', 'a4b', 'a9b', 'aXb', 'a b']
    
  • [ ]内指定a+b*,+与*就不会被当做语法处理,而是被当做互相独立的单个字符。[ ]能识别出的语法也就是“-”指定范围与“^”取反。

    print(re.findall('a[a+b*]b', 'aaba+ba*babb'))
    # ['aab', 'a+b', 'a*b', 'abb']
    
也就是说:
模式相比于[ ]
\d匹配任何十进制数字;这等价于类 [0-9]
\D匹配任何非数字字符;这等价于类 [^0-9]
\s匹配任何空白字符;这等价于类 [ \t\n\r\f\v]
\S匹配任何非空白字符;这相当于类 [^ \t\n\r\f\v]
\w匹配任何字母与数字字符;这相当于类 [a-zA-Z0-9_]
\W匹配任何非字母与数字字符;这相当于类 [^a-zA-Z0-9_]

注意:[ ]内的字符,若不是指定范围,那多个字符之间就是“或者”的意思。


单行模式与多行模式

正则表达式可以设定 单行模式多行模式

默认为单行模式,添加“re.M”则指定为多行模式。

^ 表示匹配文本的 开头 位置。

  • 如果是 单行模式 ,表示匹配 整个文本 的开头位置。
  • 如果是 多行模式 ,表示匹配 文本每行 的开头位置。
import re

content = '''001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80'''

print(re.findall(r'^\d+', content))
# 单行模式下:['001']
print(re.findall(r'^\d+', content, re.M))
# 多行模式下:['001', '002', '003']

$ 表示匹配文本的 结尾 位置。

  • 如果是 单行模式 ,表示匹配 整个文本 的结尾位置。
  • 如果是 多行模式 ,表示匹配 文本每行 的结尾位置。
import re

content = '''001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80'''

print(re.findall(r'\d+$', content))
# 单行模式下:['80']
print(re.findall(r'\d+$', content, re.M))
# 多行模式下:['60', '70', '80']

( ) 分组选择,组选择

括号称之为 正则表达式的 组选择。

就是把 正则表达式 匹配的内容 里面 其中的某些部分 标记为某个组。

我们可以在 正则表达式中 标记 多个

举个例子:

import re

content = '''苹果,苹果是绿色的
橙子,橙子是橙色的
香蕉,香蕉是黄色的'''

print(re.findall(r'^.*,', content, re.M))
# ['苹果,', '橙子,', '香蕉,']
print(re.findall(r'^(.*),', content, re.M))
# ['苹果', '橙子', '香蕉']

我们可以看到,当我们使用括号进行组选择之后,输出的内容不再带有中文逗号了。也就是说,加了括号进行组选择之后,相当于使用不加括号的规则先筛选出了内容,然后又丢弃了括号外的其他部分,只保留了组选择(括号内)的部分进行输出。

如果是进行了多组选择呢?

情况如下:

print(re.findall(r'^(.*)(,)', content, re.M))

# [('苹果', ','), ('橙子', ','), ('香蕉', ',')]

我们看到,组与组的输出结果是一起作为一个元组返回的,而这个元组的元素顺序是按分组顺序决定的。

三、使用正则表达式切割字符串

字符串 对象的 split 方法只适用于 简单的字符串分割。 有时,你需要更加灵活的字符串切割。

比如,我们需要从下面字符串中提取武将的名字。

names = '关羽; 张飞, 赵云,马超, 黄忠  李逵'

我们发现这些名字之间, 有的是分号隔开,有的是逗号隔开,有的是空格隔开, 而且分割符号周围还有不定数量的空格

这时,可以使用正则表达式里面的 split 方法:

import re

names = '关羽; 张飞, 赵云,   马超, 黄忠  李逵'

namelist = re.split(r'[;,\s]\s*', names)
print(namelist)
# ['关羽', '张飞', '赵云', '马超', '黄忠', '李逵']

正则表达式 [;,\s]\s* 指定了,分割符为 分号、逗号、空格 里面的任意一种均可,并且 该符号周围可以有不定数量的空格。

四、字符串替换

字符串 对象的 replace 方法只适应于 简单的 替换。 有时,你需要更加灵活的字符串替换。

比如,我们需要在下面这段文本中 所有的 链接中 找到所以 /avxxxxxx/ 这种 以 /av 开头,后面接一串数字, 这种模式的字符串。

然后,这些字符串全部 替换为 /cn345677/

names = '''

下面是这学期要学习的课程:

<a href='https://www.bilibili.com/video/av66771949/?p=1' target='_blank'>点击这里,边看视频讲解,边学习以下内容</a>
这节讲的是牛顿第2运动定律

<a href='https://www.bilibili.com/video/av46349552/?p=125' target='_blank'>点击这里,边看视频讲解,边学习以下内容</a>
这节讲的是毕达哥拉斯公式

<a href='https://www.bilibili.com/video/av90571967/?p=33' target='_blank'>点击这里,边看视频讲解,边学习以下内容</a>
这节讲的是切割磁力线
'''

被替换的内容不是固定的,所以没法用 字符串的replace方法。

这时,可以使用正则表达式里面的 sub 方法:

import re

names = '''

下面是这学期要学习的课程:

<a href='https://www.bilibili.com/video/av66771949/?p=1' target='_blank'>点击这里,边看视频讲解,边学习以下内容</a>
这节讲的是牛顿第2运动定律

<a href='https://www.bilibili.com/video/av46349552/?p=125' target='_blank'>点击这里,边看视频讲解,边学习以下内容</a>
这节讲的是毕达哥拉斯公式

<a href='https://www.bilibili.com/video/av90571967/?p=33' target='_blank'>点击这里,边看视频讲解,边学习以下内容</a>
这节讲的是切割磁力线
'''

newStr = re.sub(r'/av\d+?/', '/cn345677/' , names)
print(newStr)

sub 方法就是也是替换 字符串, 但是被替换的内容 用 正则表达式来表示 符合特征的所有字符串。

比如,这里就是第一个参数 /av\d+?/ 这个正则表达式,表示以 /av 开头,后面是一串数字,再以 / 结尾的 这种特征的字符串 ,是需要被替换的。

第二个参数,这里 是 '/cn345677/' 这个字符串,表示用什么来替换。

第三个参数是源字符串