有些事情是冥冥之中,命中注定,比如这篇文章------关于正则表达式的两年
可以这样说,这篇文章的一个完整生命周期,整整持续了大概两年。
开始回忆 ——
- 第一份工作的第一份领导的某次例行沟通谈话中,在对我学习和工作的方方面面提出建议后,在结束的时候说:有时间多学学正则表达式,它不仅仅局限于python一种语言,用途比较广泛。“喔”,我一脸蒙蔽。回到座位后,我开始百度“正则表达式”,………
- 后来的第二份工作,也就是目前的工作中,因为是做---测试开发,因此对一些脚本、编程语言的使用更加频繁。因此,在日常工作中,对正则的使用更是频繁,简单的search/findall/match还可以应付,对于相对复杂的用法,我就需要百度、知乎借鉴各位大神的思路和解决办法了。
- 直到上个月,我接到一个‘根据正则表达式生成合法/非法+遍历的字符串’的需求(这个需求是json、jsonschema生态中的一个子集-由jsonschema生成json,供给开发或测试 数据)。基于这个需求,我需要深入理解正则表达式,并灵活运用。因此,在完成这个需求后,我对正则表达式有了更深层次的认识。在这里,结合多个知识点做一个总结。
这里对正则表达式做一个总结
简单的使用
1 import re2 re.match #从开始位置开始匹配,如果开头没有则无
3 re.search #搜索整个字符串
4 re.findall #搜索整个字符串,返回一个list
5 # r 用在pattern之前,表示单引号中的字符串为原生字符,不会进行任何转义
修饰符 描述
- re.I 使匹配对大小写不敏感
- re.L 做本地化识别(locale-aware)匹配
- re.M 多行匹配,影响 ^ 和 $
- re.S 使 . 匹配包括换行在内的所有字符
- re.U 根据Unicode字符集解析字符。这个标志影响 \w, \W, \b, \B.
- re.X 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。
正则表达式模式 -- http://www.runoob.com/python/python-reg-expressions.html
模式 描述
- ^ 匹配字符串的开头
- $ 匹配字符串的末尾。
- . 匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符。
- [...] 用来表示一组字符,单独列出:[amk] 匹配 'a','m'或'k'
- [^...] 不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符。
- re* 匹配0个或多个的表达式。
- re+ 匹配1个或多个的表达式。
- re? 匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式
- re{ n}
- re{ n,} 精确匹配n个前面表达式。
- re{ n, m} 匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式
- a|b 匹配a或b
- (re) G匹配括号内的表达式,也表示一个组
- (?imx) 正则表达式包含三种可选标志:i, m, 或 x 。只影响括号中的区域。
- (?-imx) 正则表达式关闭 i, m, 或 x 可选标志。只影响括号中的区域。
- (?: re) 类似 (...), 但是不表示一个组
- (?imx: re) 在括号中使用i, m, 或 x 可选标志
- (?-imx: re) 在括号中不使用i, m, 或 x 可选标志
- (?#...) 注释.
- (?= re) 前向肯定界定符。如果所含正则表达式,以 ... 表示,在当前位置成功匹配时成功,否则失败。但一旦所含表达式已经尝试,匹配引擎根本没有提高;模式的剩余部分还要尝试界定符的右边。
- (?! re) 前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功
- (?> re) 匹配的独立模式,省去回溯。
- \w 匹配包括下划线的任何单词字符。等价于'[A-Za-z0-9_]'
- \W 匹配任何非单词字符。等价于 '[^A-Za-z0-9_]'
- \s 匹配任意空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v].
- \S 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]
- \d 匹配任意数字,等价于 [0-9].
- \D 匹配任意非数字,等价于 [^0-9]
- \A 匹配字符串开始
- \Z 匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串。c
- \z 匹配字符串结束
- \G 匹配最后匹配完成的位置。
- \b 匹配一个单词边界,也就是指单词和空格间的位置。例如, 'er\b' 可以匹配"never" 中的 'er',但不能匹配 "verb" 中的 'er'。
- \B 匹配非单词边界。'er\B' 能匹配 "verb" 中的 'er',但不能匹配 "never" 中的 'er'。
- \n, \t, 等. 匹配一个换行符。匹配一个制表符。等
- \1...\9 匹配第n个分组的子表达式。
- \10 匹配第n个分组的子表达式,如果它经匹配。否则指的是八进制字符码的表达式。
正则表达式实例
- [Pp]ython 匹配 "Python" 或 "python"
- rub[ye] 匹配 "ruby" 或 "rube"
- [aeiou] 匹配中括号内的任意一个字母
- [0-9] 匹配任何数字。类似于 [0123456789]
- [a-z] 匹配任何小写字母
- [A-Z] 匹配任何大写字母
- [a-zA-Z0-9] 匹配任何字母及数字
- [^aeiou] 除了aeiou字母以外的所有字符
- [^0-9] 匹配除了数字外的字符
Python 正则式的基本用法
- Python 的正则表达式的模块是 ‘re’, 它的基本语法规则就是指定一个字符序列,比如你要在一个字符串 s=’123abc456’ 中查找字符串 ’abc’, 只要这样写:
2 >>> s='123abc456eabc789'
3 >>> re.findall(r’abc’,s)
4
5 结果就是:
6 ['abc', 'abc']
这里用到的函数 ”findall(rule , target [,flag] )” 是个比较直观的函数,就是在目标字符串中查找符合规则的字符串。第一个参数是规则,第二个参数是目标字符串,后面还可以跟一个规则选项。返回结果结果是一个列表, 中间存放的是符合规则的字符串。如果没有符合规则的字符串被找到,就返回一个空列表。
- 为什么要用 r’ ..‘ 字符串( raw 字符串) ----- 表示单引号中的字符串为原生字符,不会进行任何转义
基本规则
- ‘[‘ ‘]’ 字符集合设定符
- ‘|’ 或规则
- 第一, 它在 ’[‘ ‘]’ 之中不再表示或,而表示他本身的字符。如果要在 ’[‘ ‘]’ 外面表示一个 ’|’ 字符,必须用反斜杠引导,即 ’/|’ ;
- 第二, 它的有效范围是它两边的整条规则,比如‘ dog|cat’ 匹配的是‘ dog’ 和 ’cat’ ,而不是 ’g’ 和 ’c’ 。如果想限定它的有效范围,必需使用一个无捕获组 ‘(?: )’ 包起来。比如要匹配 ‘ I have a dog’ 或 ’I have a cat’ ,需要写成 r’I have a (?:dog|cat)’ ,而不能写成 r’I have a dog|cat’
- ‘.’ 匹配所有字符
匹配除换行符 ’/n’ 外的所有字符。如果使用了 ’S’ 选项,匹配包括 ’/n’ 的所有字符。
例:
1 >>> s=’123 /n456 /n789’2 >>> findall(r‘.+’,s)
3 ['123', '456', '789']
4
5 >>> re.findall(r‘.+’ , s , re.S)
6 ['123/n456/n789']
- ‘^’ 和 ’$’ 匹配字符串开头和结尾
注意 ’^’ 不能在‘ [ ] ’中,否则含意就发生变化。 在多行模式下,它们可以匹配每一行的行首和行尾。
- ‘\d’ 匹配数字
这是一个以 ’\’ 开头的转义字符, ’\d’ 表示匹配一个数字,即等价于 [0-9]
- ‘\D’ 匹配非数字
这个是上面的反集,即匹配一个非数字的字符,等价于 [^0-9] 。注意它们的大小写。
- ‘\w’ 匹配字母和数字
匹配所有的英文字母和数字,即等价于 [a-zA-Z0-9] 。
- ‘\W’ 匹配非英文字母和数字
即 ’\w’ 的补集,等价于 [^a-zA-Z0-9] 。
- ‘\s’ 匹配间隔符
即匹配空格符、制表符、回车符等表示分隔意义的字符,它等价于 [ /t/r/n/f/v] 。(注意最前面有个空格 )
- ‘/S’ 匹配非间隔符
即间隔符的补集,等价于 [^ /t/r/n/f/v]
- ‘\A’ 匹配字符串开头
匹配字符串的开头。它和 ’^’ 的区别是, ’\A’ 只匹配整个字符串的开头,即使在 ’M’ 模式下,它也不会匹配其它行的很首。
- ‘\Z’ 匹配字符串结尾
匹配字符串的结尾。它和 ’$’ 的区别是, ’\Z’ 只匹配整个字符串的结尾,即使在 ’M’ 模式下,它也不会匹配其它行的行尾。
- ‘\b’ 匹配单词边界
它匹配一个单词的边界,比如空格等,不过它是一个‘ 0 ’长度字符,它匹配完的字符串不会包括那个分界的字符。而如果用 ’\s’ 来匹配的话,则匹配出的字符串中会包含那个分界符。
- ‘\B’ 匹配非边界
和 ’\b’ 相反,它只匹配非边界的字符。它同样是个 0 长度字符。
- ‘(?:)’ 无捕获组
当你要将一部分规则作为一个整体对它进行某些操作,比如指定其重复次数时,你需要将这部分规则用 ’(?:’ ‘)’ 把它包围起来,而不能仅仅只用一对括号,那样将得到绝对出人意料的结果。
- ‘(?# )’ 注释
Python 允许你在正则表达式中写入注释,在 ’(?#’ ‘)’ 之间的内容将被忽略。
重复
正则式需要匹配不定长的字符串,那就一定需要表示重复的指示符。 Python 的正则式表示重复的功能很丰富灵活。重复规则的一般的形式是在一条字符规则后面紧跟一个表示重复次数的规则,已表明需要重复前面的规则一定的次数。重复规则有:
- ‘*’ 0 或多次匹配
表示匹配前面的规则 0 次或多次。
- ‘+’ 1 次或多次匹配
表示匹配前面的规则至少 1 次,可以多次匹配
- ‘?’ 0 或 1 次匹配
只匹配前面的规则 0 次或 1 次。
- ‘{m}’ 精确匹配 m 次
- ‘{m,n}’ 匹配最少 m 次,最多 n 次。 (n>m)
- 如果你只想指定一个最少次数或只指定一个最多次数,你可以把另外一个参数空起来。比如你想指定最少 3 次,可以写成 {3,} (注意那个逗号),同样如果只想指定最大为 5 次,可以写成 { , 5} ,也可以写成 {0,5} 。
- ‘*?’ ‘+?’ ‘??’ 最小匹配
前向界定与后向界定
有时候需要匹配一个跟在特定内容后面的或者在特定内容前面的字符串, Python 提供一个简便的前向界定和后向界定功能,或者叫前导指定和跟从指定功能。它们是:
- ‘(?<=…)’ 前向界定
括号中 ’…’ 代表你希望匹配的字符串的前面应该出现的字符串。
- ‘(?=…)’ 后向界定
括号中的 ’…’ 代表你希望匹配的字符串后面应该出现的字符串。
- 例: 你希望找出 c 语言的注释中的内容,它们是包含在 ’/*’ 和 ’*/’ 之间,不过你并不希望匹配的结果把 ’/*’ 和 ’*/’ 也包括进来,那么你可以这样用:
2 >>> re.findall( r’(?<=//*).+?(?=/*/)’ , s )
3 [' comment 1 ', ' comment 2 ']
4
5 >>> s = ‘aaa111aaa , bbb222 , 333ccc ‘
6 >>> re.findall( r’(?<=[a-z]+)/d+(?=[a-z]+)' , s ) # 错误的用法
7 它会给出一个错误信息:
8 error: look-behind requires fixed-width pattern
9
10 >>> re.findall( r’/d+(?=[a-z]+)’, s )
11 ['111', '333']
前向非界定和后向非界定,它的写法为:
- ‘(?<!...)’ 前向非界定
只有当你希望的字符串前面不是’…’ 的内容时才匹配
- ‘(?!...)’ 后向非界定
只有当你希望的字符串后面不跟着 ’…’ 内容时才匹配。
组的基本知识
- ‘(‘’)’ 无命名组
最基本的组是由一对圆括号括起来的正则式。比如上面匹配包夹在字母中间的数字的例子中使用的 (/d+)
1 >>> s = ‘aaa111aaa , bbb222 , 333ccc ‘2 >>> re.findall (r'[a-z]+(/d+)[a-z]+' , s )
3 ['111']
4
5
- ‘(?P<name>…)’ 命名组
‘(?P’ 代表这是一个 Python 的语法扩展 ’<…>’ 里面是你给这个组起的名字,比如你可以给一个全部由数字组成的组叫做 ’num’ ,它的形式就是 ’(?P<num>/d+)’ 。起了名字之后,我们就可以在后面的正则式中通过名字调用这个组,它的形式是 ‘(?P=name)’ 调用已匹配的命名组。要注意,再次调用的这个组是已被匹配的组,也就是说它里面的内容是和前面命名组里的内容是一样的。
1 我们可以看更多的例子:请注意下面这个字符串各子串的特点。2 >>>s='aaa111aaa,bbb222,333ccc,444ddd444,555eee666,fff777ggg'
3 >>> re.findall( r'([a-z]+)/d+([a-z]+)' , s ) # 找出中间夹有数字的字母
4 [('aaa', 'aaa'), ('fff', 'ggg')]
5
6 >>> re.findall( r '(?P<g1>[a-z]+)/d+(?P=g1)' , s ) # 找出被中间夹有数字的前后同样的字母
7 ['aaa']
8
9 >>> re.findall( r'[a-z]+(/d+)([a-z]+)' , s ) # 找出前面有字母引导,中间是数字,后面是字母的字符串中的中间的数字和后面的字母
10 [('111', 'aaa'), ('777', 'ggg')]
11
12
13 我们再看一个例子:
14 >>> s='111aaa222aaa111 , 333bbb444bb33'
15 >>> re.findall( r'(/d+)([a-z]+)(/d+)(/2)(/1)' , s ) # 找出完全对称的 数字-字母-数字-字母-数字 中的数字和字母
16 [('111', 'aaa', '222', 'aaa', '111')]
re 模块的基本函数
- findall(rule , target [,flag] )
- 使用 compile 加速 - compile( rule [,flag] )
例:
1 >>> s='111,222,aaa,bbb,ccc333,444ddd'2 >>> rule=r’/b/d+/b’
3 >>> compiled_rule=re.compile(rule)
4 >>> compiled_rule.findall(s)
5 ['111', '222']
- match 与 search
- match(rule , targetString [,flag] )
- search(rule , targetString [,flag] )
例:
1 >>> s= 'Tom:9527 , Sharry:0003'2 >>> m=re.match( r'(?P<name>/w+):(?P<num>/d+)' , s )
3 >>> m.group()
4 'Tom:9527'
5
6 >>> m.groups()
7 ('Tom', '9527')
8
9 >>> m.group(‘name’)
10 'Tom'
11
12 >>> m.group(‘num’)
13 '9527'
- 字符串的替换和修改
- sub (rule , replace , target [,count] )
- subn(rule , replace , target [,count] )
- sub 返回一个被替换的字符串
- subn 返回一个元组,第一个元素是被替换的字符串,第二个元素是一个数字,表明产生了多少次替换。
2 >>> s=’ I have a dog , you have a dog , he have a dog ‘
3 >>> re.sub( r’dog’ , ‘cat’ , s )
4 ' I have a cat , you have a cat , he have a cat '
5
6 如果我们只想替换前面两个,则
7 >>> re.sub( r’dog’ , ‘cat’ , s , 2 )
8 ' I have a cat , you have a cat , he have a dog '
9
10 或者我们想知道发生了多少次替换,则可以使用 subn
11 >>> re.subn( r’dog’ , ‘cat’ , s )
12 (' I have a cat , you have a cat , he have a cat ', 3)
- split( rule , target [,maxsplit] )
更深入的了解 re 的组与对象
- 编译后的 Pattern 对象
将一个正则式,使用 compile 函数编译,不仅是为了提高匹配的速度,同时还能使用一些附加的功能。编译后的结果生成一个 Pattern 对象,这个对象里面有很多函数,他们看起来和 re 模块的函数非常象,它同样有 findall , match , search ,finditer , sub , subn , split 这些函数,只不过它们的参数有些小小的不同。一般说来, re 模块函数的第一个参数,即正则规则不再需要了,应为规则就包含在 Pattern 对象中了,编译选项也不再需要了,因为已经被编译过了。因此 re 模块中函数的这两个参数的位置,就被后面的参数取代了。
findall , match , search 和 finditer 这几个函数的参数是一样的,除了少了规则和选项两个参数外,它们又加入了另外两个参数,它们是:查找开始位置和查找结束位置,也就是说,现在你可以指定查找的区间,除去你不感兴趣的区间。它们现在的参数形式是:
findall ( targetString [, startPos [,endPos] ] )
finditer ( targetString [, startPos [,endPos] ] )
match ( targetString [, startPos [,endPos] ] )
search ( targetString [, startPos [,endPos] ] )
- 组的名字与序号
正则式中的每个组都有一个序号,它是按定义时从左到右的顺序从 1 开始编号的。其实, re 的正则式还有一个 0 号组,它就是整个正则式本身。
我们来看个例子:
1 >>> p=re.compile( r’(?P<name>[a-z]+)/s+(?P<age>/d+)/s+(?P<tel>/d+).*’ , re.I )2 >>> p.groupindex
3 {'age': 2, 'tel': 3, 'name': 1}
4
5 >>> s=’Tom 24 88888888 <=’
6 >>> m=p.search(s)
7 >>> m.groups() # 看看匹配的各组的情况
8 ('Tom', '24', '8888888')
9
10 >>> m.group(‘name’) # 使用组名获取匹配的字符串
11 ‘Tom’
12 >>> m.group( 1 ) # 使用组序号获取匹配的字符串,同使用组名的效果一样
13
14 >>> m.group(0) # 0 组里面是什么呢?
15 'Tom 24 88888888 <='
16 原来 0 组就是整个正则式 , 包括没有被包围到组里面的内容。当获取 0 组的时候,你可以不写这个参数。 m.group(0) 和 m.group() 的效果是一样的:
17
18 >>> m.group()
19 'Tom 24 88888888 <='