首页 > Python资料 博客日记

Python 基础 (标准库):re (正则表达式操作)

2024-09-13 22:00:05Python资料围观18

本篇文章分享Python 基础 (标准库):re (正则表达式操作),对你有帮助的话记得收藏一下,看Python资料网收获更多编程知识

1. 官方文档(中英文)

re — Regular expression operations — Python 3.12.2 documentation

re --- 正则表达式操作 — Python 3.12.2 文档

2. 正则表达式语法

<Part1 特殊字符(元字符)>

阅读2.1~2.3前,请先简单浏览一下<6.1 re.compile>、<6.2 re.findall>、<3. 正则对象>。

2.1 点号 '.'

(点号) 在默认模式下,匹配除换行符以外的任意字符。 如果指定了旗标 DOTALL ,它将匹配包括换行符在内的任意字符。

# 示例1
text1 = 'a1$5\ns'
re1 = re.compile('.')
re2 = re.compile('.', flags=re.DOTALL)

result1 = re1.findall(text1)
result2 = re2.findall(text1)

print(result1)
print(result2)

# 返回结果
# ['a', '1', '$', '5', 's']
# ['a', '1', '$', '5', '\n', 's']

2.2 '^'

(插入符) 匹配字符串的开头, 并且在 MULTILINE 模式下也匹配换行后的首个符号。

# 示例2
text1 = 'haha\nhaha'
text2 = '\nhaha'
text3 = ' haha'

re1 = re.compile('^.')
re2 = re.compile('^.', flags=re.MULTILINE)

result11 = re1.findall(text1)
result12 = re1.findall(text2)
result13 = re1.findall(text3)

result21 = re2.findall(text1)
result22 = re2.findall(text2)
result23 = re2.findall(text3)

print(f're1: {result11}, {result12}, {result13}')
print(f're2: {result21}, {result22}, {result23}')

# 返回结果:
# re1: ['h'], [], [' ']
# re2: ['h', 'h'], ['h'], [' ']

2.3 '$'

匹配字符串尾或者在字符串尾的换行符的前一个字符,在 MULTILINE 模式下也会匹配换行符之前的文本。

# 示例3 $
text1 = 'haha'
text2 = 'haha\n'
text3 = 'haha\n\n'
text4 = 'haha\n\nha'

re1 = re.compile('$')
re2 = re.compile('.$')
re3 = re.compile('.$', flags=re.MULTILINE)

result11 = re1.findall(text1)
result12 = re1.findall(text2)
result13 = re1.findall(text3)
result14 = re1.findall(text4)

result21 = re2.findall(text1)
result22 = re2.findall(text2)
result23 = re2.findall(text3)
result24 = re2.findall(text4)

result31 = re3.findall(text1)
result32 = re3.findall(text2)
result33 = re3.findall(text3)
result34 = re3.findall(text4)

print(f're1: {result11}, {result12}, {result13}, {result14}')
print(f're2: {result21}, {result22}, {result23}, {result24}')
print(f're3: {result31}, {result32}, {result33}, {result34}')

# 返回结果
# re1: [''], ['', ''], ['', ''], ['']
## 在 'foo\n' 中搜索 $ 会找到两个(空的)匹配:一个在换行符之前,一个在字符串的末尾。
# re2: ['a'], ['a'], [], ['a']
## .不匹配换行符
# re3: ['a'], ['a'], ['a'], ['a', 'a']

### re3 = re.compile('.$', flags=re.MULTILINE | re.DOTALL)
### 返回值为:re3: ['a'], ['a', '\n'], ['a', '\n', '\n'], ['a', '\n', 'a']
### re1 = re.compile('$', flags=re.MULTILINE)
### 返回值为:re1: [''], ['', ''], ['', '', ''], ['', '', '']

2.4 数量限定符 '*','+','?'

尽量多的匹配字符串(贪婪的)

*

对它前面的正则式匹配0到任意次重复

ab* 会匹配 'a','ab',或者 'a' 后面跟随任意个 'b'

+

对它前面的正则式匹配1到任意次重复

ab+ 会匹配 'ab',或 'a' 后面跟随任意个 'b',不会匹配 'a'

 对它前面的正则式匹配0到1次重复

ab? 会匹配 'a' 或者 'ab'

re.findall('A*', 'AAA123AAAAA456')
# 返回结果
# ['AAA', '', '', '', 'AAAAA', '', '', '', '']

re.findall('A+', 'AAA123AAAAA456')
# 返回结果
# ['AAA', 'AAAAA']

re.findall('A?', 'AAA123AAAAA456')
# 返回结果
# ['A', 'A', 'A', '', '', '', 'A', 'A', 'A', 'A', 'A', '', '', '', '']

2.5 非贪婪数量限定符 '*?', '+?', '??'

re.findall('((A+)(.*)(B+))', 'AAAabcBBB')
# [('AAAabcBBB', 'AAA', 'abcBB', 'B')]

re.findall('((A+)(.*?)(B+))', 'AAAabcBBB')
# [('AAAabcBBB', 'AAA', 'abc', 'BBB')]

不管是贪婪模式,还是非贪婪模式,都需要发生回溯才能完成相应的功能。

# 式子1
re.findall('Y*Z', 'YYZ')
# 返回结果
# ['YYZ']

# 式子2
re.findall('Y*?Z', 'YYZ')
# 返回结果
# ['YYZ']

# 式子3
re.findall('Y??Z', 'YYZ')
# 返回结果
# ['YZ']

式子1在匹配时,'Y*' 会尽可能多地去匹配,当匹配完 'YY' 后,继续用正则 'Y' 与下一个字符 'Z' 匹配,匹配不上,这时就会发生回溯,吐出当前字符 'Z',接着用正则 'Z' 去匹配,匹配成功。

式子2在匹配时,'Y*?' 匹配上一个 'Y' 后,使用下一个正则 'Z' 与下一个文本内容 'Y' 比较,发现不匹配,这时向前回溯,重新查看 'Y' 匹配两个的情况,匹配上 'YY',然后再用正则 'Z' 去匹配文本中的 'Z',匹配成功。

2.6 重复运算符 '{m}','{m,n}'

注意'{m,n}',逗号后没有空格

尽量多的匹配字符串(贪婪的)

{m}

对其之前的正则式指定匹配 m 个重复;

少于 m 的话就会导致匹配失败。

a{6} 将匹配6个 'a' ,

但是不能是5个

{m,n}

对正则式进行 m 到 n 次匹配,在 m 和 n 之间取尽量多;

忽略 m 意为指定下界为0,忽略 n 指定上界为无限次;

逗号不能省略,否则无法辨别修饰符应该忽略哪个边界。

a{4,}b 将匹配 'aaaab' 或者

1000个 'a' 尾随一个 'b'

但不能匹配 ‘aaab

2.7 非贪婪重复运算符 '{m, n}?'

'{m}' 固定次数,因而没必要用 '{m}?'

re.findall('1{3,5}', '11111111aaa')
# ['11111', '111']

re.findall('1{3,5}?', '11111111aaa')
# ['111', '111']

2.8 转义字符 '\'

正则表达式使用反斜杠 '\' 作为转义字符,以便匹配字符 '*' ,'?' 等此类字符;或者表示特殊序列,如\w、\W、\d、\D等。

Python也使用反斜杠作为转义字符,Python 中反斜杠符号用 '\\' 表示;或者使用 raw 字符串(原始字符串 r'') 表示为 r'\',r'' 的功能就是把如 "\" 变成 "\\",从而使转义失效,简单来说,使用 r'' 的效果就是:所见即所得。

因而在正则表达式中,想表示星号,可使用 '\*'(Python中,\* 没有特殊含义,仅以普通格式输出,\*只代表\*),r'\*',或者 '\\*';想表示反斜杠,应该使用 '\\\\' 或者 r'\\'。

 

re.findall('\*', '\*')   # 正则表达式'\*',匹配星号*,返回['*']
re.findall('\\*', '\*')  # 正则表达式'\*',匹配星号*,返回['*']
re.findall(r'\*', '\*')  # 正则表达式'\*',匹配星号*,返回['*']
re.findall('\\\*', '\*')   # 正则表达式'\\*',匹配0个或者多个\
# ['\\', '', '']

re.findall(r'\\*', '\*')   # 正则表达式'\\*',匹配0个或者多个\
# ['\\', '', '']

re.findall('\\\+', '\*')   # 正则表达式'\\+',匹配1个或者多个\
# ['\\']
re.findall('\d\\\\1', r'1234\15678\1')  # 正则表达式'\d\\1',匹配一个数字+字符'\'+数字1
# ['4\\1', '8\\1']

re.findall(r'\d\\1', r'1234\15678\1')  # 正则表达式'\d\\1',匹配一个数字+字符'\'+数字1
# ['4\\1', '8\\1']

# re.findall('\\1', r'\1')  # 正则表达式'\1',引用第一个分组,但是之前又没有()产生的分组,故而报错
# 报错:invalid group reference 1 at position 1

2.9 字符集 '[]'

  1. 字符可以单独列出,比如 [amk] 匹配 'a', 'm', 或者 'k'
  2. 通过用 '-' 将两个字符连起来,表示字符范围。比如 [a-z] 将匹配任何小写ASCII字符;[0-5][0-9] 将匹配从 00 到 59 的两位数字;[0-9A-Fa-f] 将匹配任何十六进制数位。 如果 - 进行了转义 (比如 [a\-z])或者它的位置在首位或者末尾(如 [-a] 或 [a-]),它就只表示普通字符 '-'​​​​​​​。
  3. 特殊字符在集合中会失去其特殊意义。比如 [(+*)] 只会匹配这几个字面字符之一: '(', '+', '*' 或 ')' 。
  4. 字符类如 \w 或者 \S (定义如下) 也在集合内被接受(参考<part3 特殊序列>部分),不过它们可匹配的字符依赖于所使用的 flags。
  5. 不在集合范围内的字符可以通过取反来进行匹配。如果集合首字符是 '^'​​​​​​​ ,所有不在集合内的字符将会被匹配,比如 [^5] 将匹配所有字符,除了 '5', [^^] 将匹配所有字符,除了 '^',^ 如果不在集合首位,就没有特殊含义。
  6. 要在集合内匹配一个 ']' 字面值,可以在它前面加上反斜杠,或是将它放到集合的开头。 例如,[()[\]{}] 和 []()[{}] 都可以匹配右方括号,以及左方括号,花括号和圆括号。

2.10 或操作 '|',对于 'A|B',一旦 A 匹配成功, B 就不再进行匹配,即便 B 能产生一个更好的匹配,或操作是不贪婪的。

re.findall('(?:abc){2}|[a-z]{6}', 'qwertyuioabcabc123243432abcabc')
# 返回结果
# ['qwerty', 'uioabc', 'abcabc']

re.findall('(?:abc){2}|(?:abc){3}', '1abcabcabc2abcabcabc3abcabcabc')
# 返回结果
# ['abcabc', 'abcabc', 'abcabc']

re.findall('(?:abc){3}|(?:abc){2}', '1abcabcabc2abcabcabc3abcabcabc')
# 返回结果
# ['abcabcabc', 'abcabcabc', 'abcabcabc']

2.11 Python不支持,跳过:占有型数量限定符 '*+','++','?+'

这种模式类似与贪婪模式,会尽可能多地去匹配,但不会进行回溯,如果匹配失败就结束,这样比较节省时间。不过 Python 不支持这种模式,会将+理解为数量限定符,报错:multiple repeat(不支持这种写法,与 '**','+*',报错相同:多个重复)

含义如下(如果需要这个功能,参见<参考链接1>,安装 regex 模块):

2.12 Python不支持,跳过:占有型重复运算符 '{m, n}+'

报错同2.11,含义如下:

<part2 分组>

2.13 '(...)' 普通捕获组,将表达式匹配的内容保存到以数字编号的组里,编号规则是以 '(' 从左到右出现的顺序,从1开始进行编号。通常情况下,编号为0的组表示整个表达式匹配的内容。

要匹配字符 '(' 或者 ')', 用 '\(' 或 '\)', 或者把它们包含在字符集合里: [(]  或 [)]。

分组的概念在 Match对象(匹配对象)中很重要,Match 对象是re中两个非常常用的方法search() 和 match() 的返回值,因而,阅读后续内容前,请先简单浏览一下 <6.4 re.search>、<6.5 re.match>、<4. 匹配对象>、<3. 正则对象>。

obj = re.match('([a-zA-Z]+)([0-9]+)([a-zA-Z]+)([0-9]+)', 'aBc123dEf456')
obj.groups()
obj.group(0)
obj.lastindex
obj.group(0, 1, 2, 3, 4)
obj.groupdict()


# ('aBc', '123', 'dEf', '456')
# 'aBc123dEf456'
# 4
# ('aBc123dEf456', 'aBc', '123', 'dEf', '456')
# {}

接下来介绍一些扩展标记法,形如 '(?…)'

'?' 后面的第一个字符决定了这个构建采用什么样的语法。

除 '?P<name>...)外,这种扩展通常并不创建新的组合。

2.14 (?P<name>…) 命名捕获组,可以通过捕获组名而不是序号对捕获组内容进行引用,这种便捷的引用方式,不用关注捕获组的序号,也不用担心表达式部分变更会导致引用错误的捕获组。

s = '1102231990xxxxxxxx'
res = re.search('(?P<province>[0-9]{3})(?P<city>[0-9]{3})(?P<born_year>[0-9]{4})',s)
print(res.groupdict())

# 返回结果
# {'province': '110', 'city': '223', 'born_year': '1990'}

2.15 (?:...) 非捕获组,将匹配内容保存到最终的 整个表达式的区配结果中,但Expression 匹配的内容不单独保存到一个组内。

re.match('(A+)(?:.*)(B+)', 'AAA123abcBBB').group(0, 1, 2)
# 返回结果
# ('AAA123abcBBB', 'AAA', 'B')
re.match('(A+)(?:.*)(B+)', 'AAA123abcBBB').group(0, 1, 2, 3)
# 报错 no such group

2.16 '(?P=name)' 反向引用一个命名组合,不会增加新的组;它匹配前面那个叫 name 的命名组中匹配到的串同样的字串。

re.match(r'(?P<name>[\w]+).*?(?P=name)-(\d+)', 'AnnaSmith@123 AnnaSmith-1398383720').groups()

# 返回结果
# ('AnnaSmith', '1398383720')

2.17 '(?#...)' 注释模式,在实际工作中,正则可能会很复杂,导致编写、阅读和维护正则都会很困难,添加注释能便够便于阅读和维护,正则中注释模式使用 '(?#...)' 来表示。

re.search('(?#abcabc)((abc)+)', 'abcabcabc').group(0,1,2)
# 返回结果
# ('abcabcabc', 'abcabcabc', 'abc')

re.search('(?#abcabc)((?:abc)+)', 'abcabcabc').group(0,1)
# 返回结果
# ('abcabcabc', 'abcabcabc')

2.18~2.21 的主题为环视

  1. 环视是一种特殊的正则语法,从本质上来说,它匹配的是位置,正则表达式用来限定这个位置的左右应该是什么或者应该不是什么,然后去寻找这个位置;环视中的正则表达式只进行匹配,匹配内容不计入最终的匹配结果。
  2. 环视中虽然也有括号,但不会保存成子组。
  3. 环视中的表达式应该是固定长度的(look-behind requires fixed-width pattern)。

2.18 '(?=...)' 顺序肯定环视,表示所在位置右侧能够匹配 Expression

可应用于文件名处理,如:要求文件名只能以字母或下滑线开头时,对文件名的规范化操作:

re.sub 含义参考 6.7;\W 参考 <part3 特殊序列>

re.sub('\W|^(?=\d)','_','1asd')
# '_1asd'

re.sub('\W|^(?=\d)','_','+asd')
# '_asd'

2.19 '(?<=...)' 逆序肯定环视,表示所在位置左侧能够匹配 Expression

re.findall('(?<=abc).{6}', '123abc456abc789abc')         # ['456abc', '789abc']
re.findall('(?=abc).{6}', '123abc456abc789abc')          # ['abc456', 'abc789']

re.search('(?=abc).{6}', '123abc456abc789abc').group(0)  # 'abc456'
re.findall('(?=abc).{7}', '123abc456abc789abc')          # ['abc456a']

逆序环视,断言必须有固定的宽度,否则会发生报错:re.error: look-behind requires fixed-width pattern。

re.findall("(?<!2000)Windows", "1232320Windows")
# ['Windows']

# 长度不固定时报错
# re.findall("(?<!2000|98)Windows", "1232320Windows")
# re.error: look-behind requires fixed-width pattern

re.findall("(?<!2000)(?<!98)Windows", "1232320Windows")
# ['Windows']
re.findall("(?<!2000)(?<!98)Windows", "98Windows")
# []
a = 'there are three tables'
re.findall('(?:(?<=^)|(?<= ))t.+?(?= |$)', a)
# ['there', 'three', 'tables']

2.20 '(?!...)' 顺序否定环视,表示所在位置右侧不能匹配 Expression

re.search('(?!abc).{6}', '123abc456abc789abc').group(0)  # '123abc'
re.findall('(?!abc).{7}', '123abc456abc789abc')          # ['123abc4', '56abc78']

有一个有趣的例子(阅读前请先了解一下对分组的引用 <part3 特殊序列 \number>)

来源:正则表达式校验之逗号分割的不重复单个字母 - 知乎

目标:用正则校验以逗号分隔的字符串且其中不可存在重复的子串。例如,有两个字符串:'Q,WW,EEE,1111,rrr222”和“A,SS,DDD,3333,fff444,SS',前者符合要求,后者不符合,因为后者里面的“SS”子串出现了两次。

解答如下,主要应用了顺序否定环视语法,同时可以体会一下复杂句式中运用命名捕获组的优势。

text0 = "AA,SS,DDD,SS,BB"
re.match(r'(^|.*,)(?P<repeat>[^,]+)(?:,.*,|,)(?P=repeat)(?:,.*|$)', text0).group(0)

# 匹配成功,返回"AA,SS,DDD,SS,BB"
# 在这个句式中,注意要保证:1.repeat组在句子开始,或者前后都有逗号;2.后向引用的repeat组前面一定有个逗号,且除在句末否则其后也有逗号。这两点要求用以避免两个子串部分内容相同的场景被错误地视作为重复子串。
pattern = re.compile(r'(?!(^|.*,)(?P<repeat>[^,]+)(?:,.*,|,)(?P=repeat)(?:,.*|$))[^,]+(,[^,]+)+')
text1 = "Q,WW,EEE,1111,rrr222"
text2 = "SS,SS"
text3 = "SS,DDD,SS"
text4 = "AA,SS,DDD,SS"
text5 = "AA,SS,DDD,SS,BB"
text6 = "SSabc,SSabcdef,DDD,3333,fff444"
text7 = "SSabc,defSSabc,DDD,3333,fff444"

pattern.match(text1)  # 匹配成功 -- 'Q,WW,EEE,1111,rrr222'
pattern.match(text2)  # None
pattern.match(text3)  # None
pattern.match(text4)  # None
pattern.match(text5)  # None
pattern.match(text6)  # 匹配成功 -- 'SSabc,SSabcdef,DDD,3333,fff444'
pattern.match(text7)  # 匹配成功 -- 'SSabc,defSSabc,DDD,3333,fff444'

2.21 '(?<!...)' 逆序否定环视,表示所在位置左侧不能匹配 Expression

re.findall('(?<!abc).{6}', '123abc456abc789abc')         # ['123abc', '56abc7']
re.findall('(?!abc).{6}', '123abc456abc789abc')          # ['123abc', '456abc', '789abc']

2.22 '(?aiLmsux)',?'后面是一个或多个来自'a','i','L','m','s','u','x'​​​​​​​ 集合的字母,如: '(?a)','(?i)','(?m)','(?s)','(?u)','(?x)',或字母组合'(?ms)'等,这些字母将为<整个正则表达式>设置相应的旗​​​​​​​标。

效果同flag,具体含义参见下文<5. 旗标 Flags>

re.findall(r'(^.)', 'haha\nxixi\n\nenen', flags=re.MULTILINE | re.DOTALL)
# ['h', 'x', '\n', 'e']
re.findall(r'(.$)', 'haha\nxixi\n\nenen', flags=re.MULTILINE | re.DOTALL)
# ['a', 'i', '\n', 'n']

re.findall(r'(?ms)(^.)', 'haha\nxixi\n\nenen')
# ['h', 'x', '\n', 'e']
re.findall(r'(?ms)(.$)', 'haha\nxixi\n\nenen')
# ['a', 'i', '\n', 'n']

re.findall(r'(^.)', 'haha\nxixi\n\nenen', re.M | re.S)
# ['h', 'x', '\n', 'e']
re.findall(r'(.$)', 'haha\nxixi\n\nenen', re.M | re.S)
# ['a', 'i', '\n', 'n']
text = "Hello \
World \
HELLO \
WORLD"
re.findall(r'(?mi)hello', text)
# ['Hello', 'HELLO']

2.23 '(?aiLmsux-imsx:…)','?' 后面是零个或多个来自 'a','i','L','m','s','u','x'​​​​​​​集合的字母,后面可以带 '-'​​​​​​​ 再跟1个或多个来自 'i','m','s','x' 集合的字母,这些字母将为<这部分表达式>设置或移除相应的旗标。如 '(?m-i:...)','(?m:...)' 等。

re.findall(r'(?i)aA', 'aA-Aa-AA-aa')
# ['aA', 'Aa', 'AA', 'aa']

re.findall(r'(?i:a)A', 'aA-Aa-AA-aa')
# ['aA', 'AA']

re.findall(r'(?i:a)A', 'aA-Aa-AA-aa', flags=re.I)
# ['aA', 'Aa', 'AA', 'aa']

re.findall(r'(?-i:a)A', 'aA-Aa-AA-aa', flags=re.I)
# ['aA', 'aa']

re.findall(r'(?i)(?-i:a)A', 'aA-Aa-AA-aa')
# ['aA', 'aa']

# re.findall(r'(?is-i:.aa)', 'AAaaAaaA\naaAa')  # 报错 bad inline flags: flag turned on and off at position 6

2.24 '(?(id/name)yes-pattern|no-pattern)'

re.match('(<)?(\w+@\w+(?:\.\w+)(?(1)>|$))', 'user@host.com').group(0, 1, 2)
# ('user@host.com', None, 'user@host.com')
re.match('(<)?(\w+@\w+(?:\.\w+)(?(1)>|$))', '<user@host.com>').group(0, 1, 2)
# ('<user@host.com>', '<', 'user@host.com>')
re.match('(<)?(\w+@\w+(?:\.\w+)(?(1)>|$))', '<user@host.com')  # None
re.match('(<)?(\w+@\w+(?:\.\w+)(?(1)>|$))', 'user@host.com>')  # None

<part3 特殊序列>

Unicode和ASCII的区别

ASCII编码是美国标准信息交换码(American Standard Code for Information Interchange),主要用于显示现代英语。Unicode: (万国码、国际码、统一码、单一码)是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得计算机可以用更为简单的方式来呈现和处理文字。 总的来说,Unicode 编码是 ASCII 编码的扩展,它可以表示更多的字符,并且支持多种语言。

1. ASCII 编码只能表示英文字母、数字、标点符号和一些控制字符,共计128个字符。而Unicode 编码可以表示世界上几乎所有的字符,包括各种语言的文字、符号、表情等,共计超过13万个字符。

2. ASCII 编码使用7位二进制数表示一个字符,最高位始终为0。而 Unicode 编码使用16位二进制数表示一个字符,可以表示更多的字符。

3. Unicode 和 ASCII 是字符集,而 UTF-8 / UTF-16 / UTF-32 都是编码规则。

Unicode是一个标准,UTF-8 是实现,UTF-8 以字节为单位对 Unicode 进行编码。

4. 示例

当 Unicode 模式 [a-z] 或 [A-Z] 与 IGNORECASE 旗标(忽略大小写匹配)一起使用时,它们将匹配 52 个 ASCII 字母和 4 个额外的非 ASCII 字母: 'İ' (U+0130, 大写拉丁字母 I 带有上方的点), 'ı' (U+0131, 小写拉丁字母 i 不带上方的点), 'ſ' (U+017F, 小写拉丁字母长 s) 和 'K' (U+212A, 开尔文标记)。 如果使用了 ASCII 旗标,则只匹配字母 'a' 到 'z' 和 'A' 到 'Z'。

\A

只匹配字符串开始。

\Z

只匹配字符串尾。

\b

匹配空字符串,但只在单词开始或结尾的位置。

\B

匹配空字符串,但仅限于它不在单词的开头或结尾的情况。

\d

匹配任意 Unicode 十进制数码,这包括 [0-9],还包括许多其他的数码类字符。

如果使用了 ASCII 旗标则匹配 [0-9]。

\D

匹配不属于 Unicode 十进制数码的任意字符,与 \d 正相反。

如果使用了 ASCII 旗标则匹配 [^0-9]

\s

匹配 Unicode 空白字符,包括 [ \t\n\r\f\v],还包括许多其他字符。

\S

匹配不属于 Unicode 空白字符的任意字符,与 \s 正相反。

如果使用了 ASCII 旗标则匹配 [^ \t\n\r\f\v]

\w

匹配 Unicode 单词类字符,包括所有 Unicode 字母数字类字符 (由 str.isalnum() 定义),

以及下划线 (_)。

如果使用了ASCII 旗标则匹配 [a-zA-Z0-9_]。

可以用 r'\b\w+\b' 匹配单词

\W

匹配不属于 Unicode 单词类字符的任意字符,与 \w 正相反。

在默认情况下,将匹配除下划线 (_) 以外的 str.isalnum() 返回 False 的字符。

如果使用了 ASCII 旗标则匹配 [^a-zA-Z0-9_]。

\number

组合从1开始编号,用 \number 匹配数字代表的组合。

参考下方细节说明-1。

1. 简单说明

Python中,除 '\number' 和 '\b' 外(python中,\b 表示 Backspace 退格,正则表达式中,\b 表示单词开始和结束位置的空字符,如果想在正则表达式内表示退格,要将其放在字符集合内:[\b]),上述 '\+ASCII字母' 形式的特殊序列,在 Python 中均以普通格式输出,因而在表示正则表达式时,即便没有使用原始字符串 r'',也不会产生问题;但是涉及 '\number' 和 '\b' 时,切记要用原始字符串的形式 r''。

re.findall('\b', 'hello world')
# []

re.findall(r'\b.?', 'hello world')
# ['h', ' ', 'w', '']

re.findall(r'\b\w+\b', 'Richard Feynman is a famous physicist')
# ['Richard', 'Feynman', 'is', 'a', 'famous', 'physicist']

re.findall('.[\b]', 'hello\b world')
# ['o\x08']

2. '\number'

1)要使用原始字符串 r'\1' 或者 '\\1',否则表示的是python中的转义字符 '\1';

2)必须存在对应序号的分组,否则会报错。

text = 'the the little cat cat is in the hat hat hat, we like it.'
re.findall(r'(\w+)((?:\s+\1)+)', text)  

# 返回结果:[('the', ' the'), ('cat', ' cat'), ('hat', ' hat hat')]
re.findall('\1', r'12344\1')               # [] 匹配失败
re.findall('\1', '12344\1')                # ['\x01'] 匹配成功
# re.findall(r'\1', r'12344\1')            # 前面没有分组,报错 nvalid group reference 1 at position 1
re.findall('(\d)\1', r'12344\1')           # []  \1表示python中的\1(方框里面带一个问号)
re.findall('(\d)\\1', r'12344\1')          # ['4'] 
re.findall(r'(\d)\1', r'12344\1')          # ['4'] 如果有且仅有一个组,返回与该组匹配的字符串列表
re.search(r'(\d)\1', r'12344\1').group(0)  # '44'

3. 正则对象 Regular Expression Objects

由 re.compile() 返回的已编译正则表达式对象。

正则表达式对象的对象方法与 re 中的函数基本一致(部分函数多了两个可选参数pos和endpos,能限制搜索范围)。

方法或属性

描述

细节

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

类似于函数search()

(1)可选参数 pos 给出了字符串中开始搜索的位置索引,默认为0,不过这不完全等价于字符串切片:元字符 ‘^' 可匹配字符串真正的开头、换行符后面的第一个字符,但不会匹配由 pos 规定开始的位置。

(2)可选参数 endpos 限定了字符串搜索的结束位置,它假定字符串长度到 endpos , 所以只有从 pos 到 endpos-1 的字符会被匹配。如果 endpos 小于 pos,就不会有匹配产生;pattern.search(string,0,50) 等价于 pattern.search(string[:50],0)。

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

类似于函数match()

可选参数 pos\endpos 与 search() 含义相同;

用于限制搜索范围。

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

类似于函数fullmatch()

可选参数 pos\endpos 与 search() 含义相同;

用于限制搜索范围。

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

类似于函数findall()

可选参数 pos\endpos 与 search() 含义相同;

用于限制搜索范围。

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

类似于函数finditer()

可选参数 pos\endpos 与 search() 含义相同;

用于限制搜索范围。

Pattern.sub(repl, string, count=0)

等价于函数sub() 

--

Pattern.subn(repl, string, count=0)

等价于函数subn() 

--

Pattern.split(string, maxsplit=0)

等价于函数split()

--

Pattern.flags

--

正则表达式匹配旗标:传给 compile() 的旗标组合,模式中的任何 (?...) 内联旗标,以及隐式旗标如UNICODE。

Pattern.groups

--

捕获到的模式串中组的数量。

Pattern.groupindex

--

映射由 (?P<id>) 定义的命名符号组合和数字组合的字典。如果没有符号组,那字典就是空的。

Pattern.pattern

--

编译对象的原始样式字符串。

pattern1 = re.compile(r'^([a-z])(\w?\d+)(?P<name1_group>\s*)', re.I | re.A)
pattern2 = re.compile(r'([a-z])(\w?\d+)(?P<name1_group>\s*)', re.I | re.A)
text = 'aa11 123'

pattern1.match(text).group(0)
# 'aa11 '

# 其他属性值
pattern1.flags
# 258
pattern1.groups
# 3
pattern1.groupindex
# mappingproxy({'name1_group': 3})
pattern1.pattern
# '^([a-z])(\\w?\\d+)(?P<name1_group>\\s*)'


# 有^时,pos不等于1将匹配不到
pattern1.match(text, 1)
# None

pattern2.match(text)
# <re.Match object; span=(0, 5), match='aa11 '>

pattern2.match(text, 1)
# <re.Match object; span=(1, 5), match='a11 '>

注意:

(1)flags 打印值为258,对应 re.ASCII | re.IGNORECASE,其他旗标对应值参考<5. 正则旗标>;

(2)'^' 不能匹配由 pos 规定的开始位置,在上述例子中,pattern1 有 '^',使用 re.mathc() 且pos设置为1时,匹配不到结果,返回 None。

4. 匹配对象 Match Objects

由成功的 match 和 search 所返回的匹配对象。

方法或属性

描述

Match.group([group1, ...])

返回一个或者多个匹配的子组。如果只有一个参数,结果就是一个字符串,如果有多个参数,结果就是一个元组(每个参数对应一个项),如果没有参数,默认为0,整个匹配。

参数支持组号(组索引值)、字符串(组名),如:

re.match('(a)(b)(c)', 'abc').group(0, 1, 2)
re.match('(a)(b)(?P<name>c)', 'abc').group(0, 1,'name')。

如果传入字符串参数(组名),但在 pattern 中不存在此组名,会引发 IndexError 异常;如果一个组号是负数,或者大于样式中定义的组数,会引发 IndexError 异常。

如果一个组匹配成功多次,就只返回最后一个匹配,如:

re.match(r"(..)+", "a1b2c3").group(1) 的返回值是 'c3'。

Match.groups()

返回一个元组,包含所有匹配的子组(在pattern中出现的从1到任意多的组合)。 

没有分组返回 ()

Match.groupdict()

返回一个字典,包含了所有的命名子组:

key是组名,value是匹配的值;

没有分组返回 {}

Match.__getitem__(g)

等价于 Match.group(g),如,可以通过 Match[0],获取第一个分组的内容,即 Match.group(0)。

Match.start([group])

返回 group 匹配到的字串的开始标号。

Match.end([group])

返回 group 匹配到的字串的结束标号。

Match.span([group])

返回一个二元组 (m.start(group), m.end(group)) 。

如果 group 没有在这个匹配中,就返回 (-1, -1) 。

group 默认为0,表示整个匹配。

Match.pos

正则引擎开始在字符串搜索一个匹配的位置索引。

Match.endpos

正则引擎停止在字符串搜索一个匹配的位置索引。

Match.lastindex

捕获组的最后一个匹配的整数索引值,如果没有的话返回 None。

比如,对于字符串 'ab' ,表达式 '(a)b' , '((a)(b))' , 和 '((ab))' 将得到lastindex == 1,而 '(a)(b)'  会得到 lastindex == 2 。

Match.lastgroup

最后一个匹配的命名组名字,如果没有的话返回 None。

Match.re

返回产生这个实例的正则对象(<3. 正则对象>)。

Match.string

返回传递到 match() 或 search() 的字符串。

先看一个示例:

matchobj = re.match(r'^([a-z])([a-z]?[0-9]+)(?P<name1>[ ,\n\t]*)(?P<name2>[0-9]+)([0-9]?)', 'aa11 123')
matchobj.pos  
# 0
matchobj.endpos
# 8
matchobj.lastindex
# 5
matchobj.lastgroup
# None
matchobj.re
# re.compile(r'^([a-z])([a-z]?[0-9]+)(?P<name1>[ ,\n\t]*)(?P<name2>[0-9]+)([0-9]?)', re.UNICODE)
matchobj.re.groups
# 5
matchobj.string
# 'aa11 123'
matchobj.re.pattern
# '^([a-z])([a-z]?[0-9]+)(?P<name1>[ ,\\n\\t]*)(?P<name2>[0-9]+)([0-9]?)'
matchobj.group(0)
# 'aa11 123'
matchobj.group(0, 1, 2)
# ('aa11 123', 'a', 'a11')
matchobj.group(0, 'name1')
# ('aa11 123', ' ')

matchobj.groupdict()
# {'name1': ' ', 'name2': '123'}

matchobj.groups()
# ('a', 'a11', ' ', '123', '')

matchobj[2]
# 'a11'
matchobj['name2']
# '123'
# 关于Match.start()、Match.end()、Match.span()
matchobj.start()
# 0
matchobj.end()
# 8
matchobj.span()
# (0, 8)

matchobj.start(2)
# 1
matchobj.end(2)
# 4
matchobj.span(2)
# (1, 4)

matchobj.start('name2')
# 5
matchobj.end('name2')
# 8
matchobj.span('name2')
# (5, 8)

再看一个没有分组的例子:

nogroup_obj = re.match(r'.{3}', 'aa11 123')
if nogroup_obj.lastindex is None:
    print('yes')
# yes
nogroup_obj.group(0)
# 'aa1'
nogroup_obj.groups()
# ()
nogroup_obj.groupdict()
# {}

5. 正则旗标 Flags(Regex Flags)

简写

内联旗标

描述

re.ASCII

re.A

(?a)

使 \w, \W, \b, \B, \d, \D, \s 和 \S 执行仅限 ASCII 匹配而不是完整的 Unicode 匹配。

Python 3 中 str 模式默认使用 Unicode。

re.IGNORECASE

re.I

(?i)

执行忽略大小写的匹配。

[A-Z] 这样的表达式也将匹配小写字母。

re.MULTILINE

re.M

(?m)

使 '^' 匹配字符串的开始和每一行的开头

(紧随在换行符之后);

使 '$' 匹配字符串的末尾和每一行的末尾

(紧接在换行符之前)。

re.DOTALL

re.S

(?s)

使 '.' 特殊字符匹配任意字符,包括换行符。

re.UNICODE

re.U

(?u)

在 Python 3 中,str 模式默认将匹配 Unicode 字符。

因此这个旗标无任何效果,仅保留用于向下兼容。

re.VERBOSE

re.X

(?x)

允许通过在视觉上分隔表达式的逻辑段落和添加注释来编写更为友好并更具可读性的正则表达式。

表达式中的空白符会被忽略,除非是在字符类中,或前面有一个未转义的反斜杠,或者是在 *?, (?: 或 (?P<...> 等形符之内。

re.LOCALE

re.L

(?L)

使 \w, \W, \b, \B 和忽略大小写的匹配依赖于当前语言区域。

已不建议使用,请考虑改用 Unicode 匹配,语言区域机制不可靠。

re.DEBUG

显示有关被编译表达式的调试信息。

re.NOFLAG

表示未应用任何旗标,该值为 0。

该旗标可被用作某个函数,关键字参数的默认值;或用作将与其他旗标进行有条件 OR 运算的基准值。

具体使用方法,参见2.1,2.2,2.3,2.22。

6. 函数 Functions

6.1 re.compile:将正则表达式编译为正则表达式对象,使用对象方法进行匹配(match()、search()等)。适用于需要多次使用某个正则表达式的场景,能让程序更高效。

6.2 re.findall:从左到右扫描 string,返回所有与 pattern 匹配的结果,返回字符串列表或字符串元组列表(按照找到的顺序返回)。

注意:

  1. 如果没有组,返回与整个模式匹配的字符串列表;
  2. 如果有且仅有一个组,返回与该组匹配的字符串列表;
  3. 如果有多个组,返回与这些组匹配的字符串元组列表。
re.findall('A+1+', 'AAA111A111AA111')
# ['AAA111', 'A111', 'AA111']

re.findall('(A+)1+', 'AAA111A111AA111')
# ['AAA', 'A', 'AA']

re.findall('(A+)(1+)', 'AAA111A111AA111')
# [('AAA', '111'), ('A', '111'), ('AA', '111')]

6.3 re.finditer:针对正则表达式 pattern 在 string 里的所有非重叠匹配返回一个产生 Match 对象的 iterator。 string 将被从左至右地扫描,并且匹配也将按被找到的顺序返回。 空匹配也会被包括在结果中。

for item in re.finditer('A+1+', 'AAA111A111AA111'):
    print(item.group(0))
# AAA111
# A111
# AA111

re.findall('\s*', 'ABC')
# ['', '', '', '']

for item in re.finditer('\s*', 'ABC'):
    print(item.group(0))

print('*')

在阅读6.4~6.6之前,建议简单浏览 <4.匹配对象 Match Objects>

6.4 re.search:扫描整个string,找到与正则表达式 pattern 匹配的第一个位置,返回相应的 Match 对象,若没有与模式匹配的位置返回 None。

6.5 re.match:从 string 起始位置开始匹配,能与 pattern 匹配则返回相应的 Match 对象,否则返回 None。

6.6 re.fullmatch:如果整个 string 与正则表达式 pattern 匹配,则返回相应的 Match,如果字符串与模式不匹配则返回 None。

re.match('abc\d', 'abc1').group(0)
# 'abc1'

re.fullmatch('abc\d', 'abc1').group(0)
# 'abc1'

# re.fullmatch('abc\d', 'abc12').group(0)
# 报错

6.7 re.sub:在字符串中进行正则表达式的替换操作,返回通过使用 repl 替换在 string 最左边非重叠出现的 pattern 而获得的字符串。 如果样式没有找到,则不加改变地返回 string。 repl 可以是字符串或函数。

  1. repl 如果是字符串,其中任何反斜杠转义符号都会被处理,即 \n 会被转换为一个换行符,\r 会被转换为一个回车符;未知的 ASCII 字符转义序列会报错(保留,可能在未来会定义);其他未知转义序列例如 \& 会保持原样;向后引用,如 \6 对应样式中第 6 组所匹配到的子字符串,引用 \g<name> 对应样式中命名为 name 的组合所匹配到的子字符串,\g<number> 对应数字组,\g<2> 就是 \2\g<2>0 就是 \20 ,组20。
  2. repl 可以是一个函数,该函数接受单个 Match对象作为参数,并返回替换字符串。
  3. count 表示最大替换个数,count 必须是非负整数。如果省略这个参数或设为 0,所有的匹配都将会被替换。

​​​​​1)repl为字符串

re.sub(r'\sAND\s', ' & ', 'Baked Beans And Spam', flags=re.IGNORECASE)
# 'Baked Beans & Spam'

# 存在空匹配时:与前一个空匹配不相邻时发生替换
re.sub('x*', '-', 'abxd')
# '-a-b--d-'

print(re.sub('([a-z]\d)([A-Z]\s)', r'a1A\n', 'm2M s4Z\tu9Z\n'))
print(re.sub('([a-z]\d)([A-Z]\s)', r'a1A\1', 'm2M s4Z\tu9Z\n'))
print(re.sub('([a-z]\d)([A-Z]\s)', 'a1A\1', 'm2M s4Z\tu9Z\n'))  # python中\1这里没能显示

# 如果想表示字符\n
print(re.sub('([a-z]\d)([A-Z]\s)', r'a1A\\n', 'm2M s4Z\tu9Z\n'))
print(re.sub('([a-z]\d)([A-Z]\s)', 'a1A\\\\n', 'm2M s4Z\tu9Z\n'))

print('\d')
# print(re.sub('([a-z]\d)([A-Z]\s)', 'a1A\d', 'm2M s4Z\tu9Z\n'))  # 会报错

再看一个有趣的例子(同<part3 特殊序列 \number>),一个句子中可能有某些个单词重复打印,需要去掉这些多余的重复的单词,re.sub 可以轻松的解决这个问题。

需要注意的是,'?P=<name>' 只能在同一正则表达式内部使用,在 re.sub 中引用 pattern 中的分组时,需要用 '\g<id/name>'。

text = 'the the little cat cat is in the hat hat hat, we like it.'


re.sub(r'(\w+)((?:\s+\1)+)', r'\1', text) 
# 'the little cat is in the hat, we like it.'

re.sub(r'(\w+)((?:\s+\1)+)', r'\g<1>', text)
# 'the little cat is in the hat, we like it.'

re.sub(r'(?P<word>\w+)((?:\s+\1)+)', r'\g<word>', text)
# 'the little cat is in the hat, we like it.'

2)repl为函数

def func1(matchobj):
    print(matchobj.group(0))
    if matchobj.group(0) == '-': return ' '
    else: return '-'

re.sub('-{1,2}', func1, 'pro----gram-files')
# --
# --
# -
# 'pro--gram files'

re.sub('\d', func1, 'pro----gram-files')
# 返回原字符串
# 'pro----gram-files'

问题:已知一个字符串 sentence 和一个整数 discount,要求对句子中每个表示价格的单词($+数值),计算此价格的基础上减免 discount% 的折后价并更新到句子中,更新后的价格应该表示为一个恰好保留小数点后两位的数字。

使用 re.sub 解决:

import re
def discountPrices(sentence, discount):
        def f(match):
            v = match.group(1)
            return f'${float(v) * (100-discount) / 100:.2f}'
        return re.sub(r'(?:(?<=^)|(?<= ))\$([\d.]+)(?= |$)', f, sentence)

s = "there are $10 $20 and 50$ candies in the shop 10$ $9$ $40"
discountPrices(s, 3)
# 'there are $9.70 $19.40 and 50$ candies in the shop 10$ $9$ $38.80'

3)count

re.sub('\w\d','Z0','a1b2c3d4e5')
# 'Z0Z0Z0Z0Z0'

re.sub('\w\d','Z0','a1b2c3d4e5',2)
# 'Z0Z0c3d4e5'

6.8 re.subn:行为与re.sub相同,但是返回值是数组:(替换后的字符串,替换次数)

print(re.sub('([a-z]\d)([A-Z]\s)', r'a1A\1', 'm2M s4Z\tu9Z\n'))
# a1Am2a1As4a1Au9

print(re.subn('([a-z]\d)([A-Z]\s)', r'a1A\1', 'm2M s4Z\tu9Z\n'))
# ('a1Am2a1As4a1Au9', 3)

6.9 re.split: 以pattern为分隔符,分割string,返回一个字符串列表。

  1. 如果在 pattern 中有捕获组合 '()',那么 ‘分隔符’ 对应的文字也会包含在列表里,如果不需要分隔符,可以使用 (?:)。
  2. 如果在 pattern 匹配到字符串的开始,那么结果将会以一个空字符串开始;如果匹配到字符串结尾,则结果会以一个空字符结束。
  3. 如果 maxsplit 非零, 最多进行 maxsplit 次分隔, 剩下的字符全部返回到列表的最后一个元素
re.split('[a-f]+', '0a3B9', flags=re.IGNORECASE)
# ['0', '3', '9']

# pattern中有分组的场景
re.split(r'(abc)def', '1abcdef2abcdef')
# ['1', 'abc', '2', 'abc', '']

# 首尾都匹配到分隔符
re.split(r'(\W+)', '...words, words...')
# ['', '...', 'words', ', ', 'words', '...', '']
re.split(r'\W+', '...words, words...')
# ['', 'words', 'words', '']

# 尾部匹配到分隔符
re.split(r'\W+', 'Words, words, words.')
# ['Words', 'words', 'words', '']
re.split(r'(\W+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']

# 带maxsplit
re.split(r'\W+', 'Words, words, words.', 1)
# ['Words', 'words, words.']
re.split(r'\b', 'Words, words, words.')
# ['', 'Words', ', ', 'words', ', ', 'words', '.']
# \b表示字符首尾

re.findall('', 'A1AA2')
# ['', '', '', '', '', '']

re.findall('A*', 'A1AA2')  # 匹配''或'A',或多个A字符如'AA','AAA'
# ['A', '', 'AA', '', '']

re.split('A*', 'A1AA2')
# ['', '', '1', '', '2', '']

re.split(r'(A*)', 'A1AA2')
# ['', 'A', '', '', '1', 'AA', '', '', '2', '', '']

一个有趣的例子是,从 txt 文件读取数据,每行数据中,列之间用空格分割,不过因为某些原因,数据不规范,列之间的空格数不一定,比如,txt 文件中的第一行内容为:" 3   4      6  4    6",希望把这行内容转换为列表 [3,4,6,4,6],可以使用如下代码:

a = ' 3   4     6   4  6 '
re.split(' +', a)
# ['', '3', '4', '6', '4', '6', '']

re.split(' +', a.strip(' '))
# ['3', '4', '6', '4', '6']

re.split('(?: +)', a)
# ['', '3', '4', '6', '4', '6', '']

re.split('( +)', a)
# ['', ' ', '3', '   ', '4', '     ', '6', '   ', '4', '  ', '6', ' ', '']

re.findall('[^ ]+', a) # 第一个^表示取反
# ['3', '4', '6', '4', '6']

re.split('(?<=[^^])(?: +)(?=[^$])', a)
# [' 3', '4', '6', '4', '6 ']
import re
import pandas as pd

# 读取txt文件
with open('data.txt', 'r') as f:
    data = f.read()

# # 将txt文件内容转换为列表
lines = data.split('\n')
lines = [re.split(' +', line.strip(' ')) for line in lines]
if [''] in lines:
    lines.remove([''])
    
# 创建dataframe
df = pd.DataFrame(lines, columns=['F1', 'F2', 'F3', 'F4'], dtype=float)
df.head()
df.dtypes

6.10 re.escape:转义 pattern 中的特殊字符。在对包含正则表达式元字符(如 '.','\','*' 等)的文本字符串进行匹配时,此方法是方便高效的,不必使用转义符号 '\' 处理每个字符。

re.findall(r'\*\.\(\)\[\]', '*.()[]')
# ['*.()[]']

re.findall(re.escape('*.()[]'), '*.()[]')
# ['*.()[]']

6.11 re.purge:用于清除缓存的正则表达式模式。在使用正则表达式时,Python 会将编译后的模式缓存起来,以便下次使用。但是,如果你需要频繁地使用不同的正则表达式模式,缓存可能会占用大量的内存。这时候,你可以使用 `re.purge()` 函数来清除缓存,释放内存。

import re

re.match('abc', 'abcdef')

re.purge()
re.match('abc', 'abcdef')
re.match('abcdef', 'abcdef')
re.purge()

可通过Debug操作,在Debug窗口中查看re的缓存列表,对比使用re.purge()前后的缓存数据。

其实,re的初始化缓存中,就已经包含了很多正则表达式,将这些常用的正则表达式保存在初始化缓存中,能够减少每次使用正则表达式时的编译时间和内存开销,提高程序的性能和效率。

初始化缓存中,已经有大量内容:

调用re.purge()后:

继续运行 re.match('abc', 'abcdef') 和 re.match('abcdef', 'abcdef') 后:

参考链接

【Python笔记】正则表达式基础和应用_multiple repeat at position 39-CSDN博客


版权声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

相关文章

本站推荐