[TOC]
正则表达式(Regular expressions 也称为 REs,或 regexes 或 regex patterns)本质上是一个微小的且高度专业化的编程语言。 它被嵌入到 Python 中并通过 re 模块提供给程序猿使用;而且Python 的正则表达式引擎是用 C 语言写的,所以效率是极高的。
0x00 Python正则符号分类 正则表达式的强大之处在于特殊符号的应用,特殊符号定义了字符集合、子组匹配、模式重复次数。 正是这些特殊符号使得一个正则表达式可以匹配字符串集合而不只是一个字符串。
1.元字符 下边是元字符的完整列表它们不匹配任何字符,只是简单地表示成功或失败,因此这些字符也称之为零宽断言。
[TOC]
正则表达式(Regular expressions 也称为 REs,或 regexes 或 regex patterns)本质上是一个微小的且高度专业化的编程语言。 它被嵌入到 Python 中并通过 re 模块提供给程序猿使用;而且Python 的正则表达式引擎是用 C 语言写的,所以效率是极高的。
0x00 Python正则符号分类 正则表达式的强大之处在于特殊符号的应用,特殊符号定义了字符集合、子组匹配、模式重复次数。 正是这些特殊符号使得一个正则表达式可以匹配字符串集合而不只是一个字符串。
1.元字符 下边是元字符的完整列表它们不匹配任何字符,只是简单地表示成功或失败,因此这些字符也称之为零宽断言。1 . ^ $ * + ? { } [ ] \ | ( )
元字符详情解释: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 . | ^ $ \ ''' 1. 将一个普通字符变成特殊字符,例如 \d 表示匹配所有十进制数字 (补充:这是个重点) 2. 解除元字符的特殊功能,例如 \. 表示匹配点号本身 3. 引用序号对应的子组所匹配的字符串 4.注意,'\' + 元字符的组合可以解除元字符的特殊功能 (如 \? = '?') ''' [...] ''' 字符类,匹配所包含的任意一个字符 (补充 [.] 这时.就是一个点) 注1:连字符 - 如果出现在字符串中间表示字符范围描述;如果如果出现在首位则仅作为普通字符 注2:特殊字符仅有反斜线 \ 保持特殊含义,用于转义字符。其它特殊字符如 *、+、? 等均作为普通字符匹配 注3:脱字符 ^ 如果出现在首位则表示匹配不包含其中的任意字符;如果 ^ 出现在字符串中间就仅作为普通字符匹配 ''' {M,N} ''' M 和 N 均为非负整数,其中 M <= N,表示前边的 RE 匹配 M ~ N 次 注1:{M,} 表示至少匹配 M 次 注2:{,N} 等价于 {0,N} 注3:{N} 表示需要匹配 N 次 ''' * + ? #匹配前面的子表达式零次或一次,等价于 {0,1}】 *?, +?, ?? #默认情况下匹配模式是贪婪模式即会尽可能多地匹配符合规则的字符串;.*?、+? 和 ?? 表示启用对应的非贪婪模式。 {M,N}? #同上,启用非贪婪模式,即只匹配 M 次(最小次数) (...)
注意事项:
2.特殊字符 下边列举了由字符 ‘\’ 和另一个字符组成的特殊含义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 \序号 ''' \1...\9 #匹配第n个分组的内容 1. 引用序号对应的子组所匹配的字符串,子组的序号从 1 开始计算 (会进行细细讲解) 2. 如果序号是以 0 开头,或者 3 个数字的长度。那么不会被用于引用对应的子组,而是用于匹配八进制数字所表示的 ASCII 码值对应的字符 举个栗子:(.+) \1 会匹配 "FishC FishC" 或 "55 55",但不会匹配 "FishCFishC"(注意,因为子组后边还有一个空格) ''' \A \Z \b \B \d \D \s \S \w \W '''转义符号''' 正则表达式还支持大部分 Python 字符串的转义符号:\a,\b,\f,\n,\r,\t,\u,\U,\v,\x,\\ 注1 :\b 通常用于匹配一个单词边界,只有在字符类中才表示“退格” 注2 :\u 和 \U 只有在 Unicode 模式下才会被识别 注3 :八进制转义(\数字)是有限制的,如果第一个数字是 0 ,或者如果有 3 个八进制数字,那么就被认为是八进制数;其他情况则被认为是子组引用;至于字符串,八进制转义总是最多只能是 3 个数字的长度
正则基础案例:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import rere.search(r'.' ,'www.baidu.com' ) re.search(r'\.' ,'baidu.com' ) re.search(r'[.]' ,'baidu.com' ) re.search(r'BAI(C|D)U' ,'BAIDU.com' ) re.search(r'^Regular' ,'Regular Expression' ) re.search(r'Expression$' ,'Regular Expression' ) re.search(r'[a-z]*' ,'love.com' ) re.findall(r'[a-z]' ,'love.com' ) re.search(r'chat*' ,"Im chatweb" ) re.search(r'chat{0,}' ,"Im chatweb" ) re.findall(r'web+' ,"web.web.com" ) re.findall(r'web{1,}' ,"Im chatweb" ) re.findall(r'web?' ,"webchatweb" ) re.findall(r'web{0,1}' ,"webchatweb" ) re.findall(r'(weiyi){0,2}' ,"weiyi weiyi" ) re.findall(r'[\n]' ,"This is a\n" ) re.findall(r'[^a-z]' ,"weiyigeek.github.io\n" ) re.findall(r'[a-z^]' ,"weiyigeek.github.io\n" ) re.search(r"<.+>" ,"<html><title>我是标题</title></html>" ) re.search(r"<.+?>" ,"<html><title>我是标题</title></html>" ) re.search(r'\145' ,'12e213llo.com' ) re.findall(r'\bweiyi\b' ,'www.weiyi.com' ) re.findall(r'\bweiyi\b' ,'www. weiyi_.com' ) re.findall(r'\dweiyi\b' ,'1024weiyi.com' ) re.findall(r'\sweiyi\b' ,'\tweiyi.com' ) re.findall(r'(\w+) \1' ,'FishC FishC.com' ) re.findall(r'(\w+)\1' ,'FishCFishC.com' ) re.search(r'[0-9]{0,3}?' ,'123weiyiGeek123.com' )
注意事项:
注意\数字这个模式(支持八进制),与其他元字符连用的时候需要一定的注意
3.分组 在正则表达式中,使用元字符 ( ) 来划分组,它们将包含在内部的表达式组合在一起,所以你可以对一个组的内容使用重复操作的元字符*?+等;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 (?...) #? 开头的表示为正则表达式的扩展语法(下边这些是 Python 支持的所有扩展语法 (?aiLmsux) ''' 1. (? 后可以紧跟着 'a','i','L','m','s','u','x' 中的一个或多个字符,只能在正则表达式的开头使用 2. 每一个字符对应一种匹配标志,包含这些字符将会影响整个正则表达式的规则 re-A(只匹配 ASCII 字符), re-I(忽略大小写), re-L(区域设置),本地化识别(local-aware) re-M(多行模式), re-S(. 匹配任何符号), re-X(详细表达式) re-U (根据Unicode字符集解析字符,该标志影响\w \W \b \B) 3. 当你不想通过 re.compile() 设置正则表达式标志这种方法就非常有用啦(注意点) 注意,由于 (?x) 决定正则表达式如何被解析,所以它应该总是被放在最前边(最多允许前边有空白符)。 如果 (?x) 的前边是非空白字符,那么 (?x) 就发挥不了作用了。 ''' (?:...) #非捕获组,即该子组匹配的字符串无法从后边获取(后面会用到) (?P<name>...) #命名组,通过组的名字(name)即可访问到子组匹配的字符串 (注意点) (?P=name) #反向引用一个命名组,它匹配指定命名组匹配的任何内容 (?#...) #注释,括号中的内容将被忽略 (?=...) ''' 前向肯定断言。如果当前包含的正则表达式(这里以 ... 表示)在当前位置成功匹配则代表成功,否则失败。 一旦该部分正则表达式被匹配引擎尝试过,就不会继续进行匹配了;剩下的模式在此断言开始的地方继续尝试。 ''' (?!...) #前向否定断言。这跟前向肯定断言相反(不匹配则表示成功,匹配表示失败)。 (?<=...) #后向肯定断言。跟前向肯定断言一样,只是方向相反。 (?<!...) #后向否定断言。跟前向肯定断言一样,只是方向相反。 (?(id/name)yes-pattern|no-pattern) ''' 1. 如果子组的序号或名字存在的话,则尝试 yes-pattern 匹配模式;否则尝试 no-pattern 匹配模式 2. no-pattern 是可选的 '''
案例:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 >>> re.search('love(?=weiyi)' ,'loveweiyi stusdy' ) <re.Match object; span=(0 , 4 ), match='love' > >>> re.search('weiyi(?!\.com)' ,'weiyi.club' ) <re.Match object; span=(0 , 5 ), match='weiyi' > >>> re.search('(?<=weiyi)\.com' ,'weiyi.com' ) <re.Match object; span=(5 , 9 ), match='.com' > >>> re.search('(?<!weiyi)\.com' ,'weiyii.com' ) <re.Match object; span=(6 , 10 ), match='.com' > >>> re.search('(<)?(\w+@\w+(?:\.\w+)+)(?(1)>|$)' ,'user@fish.com' ) <re.Match object; span=(0 , 13 ), match='user@fish.com' > >>> re.search('(<)?(\w+@\w+(?:\.\w+)+)(?(1)>|$)' ,'<user@fish.com>' )<re.Match object; span=(0 , 15 ), match='<user@fish.com>' >
0x01 re模块详解 Python 通过 re 模块为正则表达式引擎提供一个接口,同时允许你将正则表达式编译成模式对象,并用它们来进行匹配;re 模块仅仅是作为 C 的扩展模块包含在 Python 中,就像 socket 模块和 zlib 模块;
正则表达式对象 re.RegexObject 与 re.MatchObject:
re.compile() 返回 RegexObject 对象。
re.match() 和 re.search 返回re.MatchObject对象;
正则表达式修饰符 - 可选标志(flags) 描述:可选标志修饰符来控制匹配的模式,另外多个标志还可以同时使用(通过“|”),如:re.I | re.M 就是同时设置 I 和 M 标志。1 2 3 4 5 6 7 8 re.I|IGNORECASE re.L|LOCALE re.M|MULTILINE re.S|DOTALL re.A|ASCII re.U|UNICODE re.X|VERBOSE
Match匹配对象包含了很多方法和属性: start() 返回匹配的开始位置 end() 返回匹配的结束位置 span() 返回一个元组表示匹配位置(开始,结束) group(num=0) 返回匹配的字符串,输入参数表示提取元组 groups() 返回一个包含所有小组字符串的元组,从 1 到 所含的小组号。
(1) re.compile(pattern[, flags]): 编译正则表达式如果您需要重复的使用某个表达式的时候使用,生成一个正则表达式( Pattern )对象 (2) re.match(pattern, string, flags=0) :扫描整个字符串并返回第一个成功的匹配。 (只匹配一次,成功返回一个匹配的对象,否则返回None) (3) re.search(pattern, string, flags=0) :遍历字符串,找到正则表达式匹配的第一个位置(只匹配一次,成功返回一个匹配的对象,否则返回None) (4) re.findall(string[, pos[, endpos]]) :遍历字符串(位置点:pos,endpos),找到正则表达式匹配的所有位置,并以列表的形式返回 (5) re.findite(pattern, string, flags=0):遍历字符串,找到正则表达式匹配的所有位置,并以迭代器的形式返回 (6) re.sub(pattern, repl, string, count=0):用于替换字符串中的匹配项(repl替换字符/函数,count=替换次数0表全部) (7) re.split(pattern, string[, maxsplit=0, flags=0]) :匹配的子串将字符串分割后返回列表 | maxsplit分隔次数
re全局函数与re.compile编译正则表达式比较:
程序是大量的使用正则表达式(例如在一个循环中使用),那么建议你使用后一种方法,因为预编译的话可以节省一些函数调用。
但如果是在循环外部,由于得益于内部缓存机制,两者效率相差无几。
案例1:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 import re''' re.compile 案例 ''' p = re.compile(r'[a-z]+' ,re.M|re.I) print(p,p.match('abcdefg' )) print(p.match('abcdefg' ,1 ,3 )) ''' re.match 案例 ''' print(re.match('www' , 'www.weiyigeek.com' ).span()) print(re.match('com' , 'www.baidu.com' )) matchObj = re.match( r'(.*) are (.*?) .*' , "Cats are smarter than dogs" , re.M|re.I) if matchObj: print ("matchObj.group() : " , matchObj.group()) print ("matchObj.group(1) : " , matchObj.group(1 )) print ("matchObj.group(2) : " , matchObj.group(2 )) else : print ("No match!!" ) ''' re.search 案例 ''' print(re.search('www' , 'www.weiyigeek.com' ).span()) print(re.search('com' , 'www.weiyigeek.com' ).span()) print(re.search(r'\d+' ,'123a456' ).group()) print(re.search(r'\d+' ,'123a456' ).start()) ''' re.findall 案例 ''' p = re.compile(r'\d+' ) print(p.findall('3只甲鱼,15条腿,多出的3条在哪里?' )) print(p.findall('run88Weiyi123google456' , 0 , 10 )) print(p.findall('run88Weiyi123google456' ,5 )) ''' re.finditer 案例 ''' it = re.finditer(r"\d+" ,"12a32bc43jf3" ) for match in it: print (match.group(),end=" " ) iterator = p.finditer('3只甲鱼,15条腿,多出的3条在哪里?' ) print(iterator) for each in iterator: print(each.group(),each.span()) ''' re.sub 案例 ''' string = '2004-959-559 # 这是一个电话号码' print(re.sub(r'#.*$' , "" , string)) print(re.sub(r'\D' , "" , string)) >>> p = re.compile('x*' )>>> p.sub('-' , 'abxd' )'-a-b-d-' def double (matched) : value = int(matched.group('value' )) print(value) return str(value * 2 ) print(re.sub('(?P<value>\d+)' , double, 'A23G4HFD567' )) >>> def hexrepl (match) :... "Return the hex string for a decimal number" ... value = int(match.group())... return hex(value)... >>> p = re.compile(r'\d+' )>>> p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.' )'Call 0xffd2 for printing, 0xc000 for user code.' ''' re.split 案例 ''' re.split('\,' , 'weiyigeek,weiyigeek, weiyigeek.' ) p = re.compile(r'\W+' ) >>> p.split('This is a test, short and sweet, of split().' ) ['This' , 'is' , 'a' , 'test' , 'short' , 'and' , 'sweet' , 'of' , 'split' , '' ] p2 = re.compile(r'(\W+)' ) >>> p2.split('This... is a test.' )['This' , '... ' , 'is' , ' ' , 'a' , ' ' , 'test' , '.' , '' ] >>> print(re.match('super' , 'superstition' ).span())(0 , 5 ) >>> print(re.match('super' , 'insuperable' ))None >>> print(re.search('super' , 'superstition' ).span())(0 , 5 ) >>> print(re.search('super' , 'insuperable' ).span())(2 , 7 )
补充:
没有任何匹配的话 match() 和 search() 会返回 None,否则返回一个匹配对象match object
re.match与re.search的区别,前者只匹配字符串的开始,后者匹配整个字符串直到找到一个匹配。
如果列表很大那么使用返回迭代器的效率要高很多
为了匹配反斜杠这个字符,我们需要在字符串中使用四个反斜杠才行。所以在正则表达式中频繁地使用反斜杠,会造成反斜杠风暴,进而导致你的字符串极其难懂,强烈建议使用原始字符串来表达正则表达式。1 2 3 4 正则字符串 原始字符串(推荐) "ab*" r"ab*" "\\\\section" r"\\section" "\\w+\\s+\\1" r"\w+\s+\1"
在这些 REs 中,当编译正则表达式时指定 re.VERBOSE 标志是非常有帮助的。因为它允许你可以编辑正则表达式的格式,使之更清楚。 案例:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 charref = re.compile(r""" &[#] # 开始数字引用 ( 0[0-7]+ # 八进制格式 | [0-9]+ # 十进制格式 | x[0-9a-fA-F]+ # 十六进制格式 ) ; # 结尾分号 """ , re.VERBOSE)charref = re.compile("&#(0[0-7]+|[0-9]+|x[0-9a-fA-F]+);" ) pat = re.compile(r""" \s* # Skip leading whitespace (?P<header>[^:]+) # Header name \s* : # Whitespace, and a colon (?P<value>.*?) # The header's value -- *? used to # lose the following trailing whitespace \s*$ # Trailing whitespace to end-of-line """ , re.VERBOSE)pat = re.compile(r"\s*(?P<header>[^:]+)\s*:(?P<value>.*?)\s*$" )
0x02 分组 (重点难点) 描述:分组显示的方法
group([group1, …]) 方法用于获得一个或多个分组匹配的字符串,当要获得整个匹配的子串时,可直接使用 group() 或 group(0);
start([group]) 方法用于获取分组匹配的子串在整个字符串中的起始位置(子串第一个字符的索引),参数默认值为 0;
end([group]) 方法用于获取分组匹配的子串在整个字符串中的结束位置(子串最后一个字符的索引+1),参数默认值为 0;
span([group]) 方法返回 (start(group), end(group))。
案例:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 >>>import re >>> pattern = re.compile(r'([a-z]+) ([a-z]+)' , re.I) >>> m = pattern.match('Hello World Wide Web' )>>> print m >>> m.group(0 ) >>> m.span(0 ) >>> m.group(1 ) >>> m.span(1 ) >>> m.group(2 ) >>> m.span(2 ) >>> m.groups() >>> m.group(3 ) >>> p = re.compile('(a)b' )>>> m = p.match('ab' )>>> m.group() >>> m.group(0 ) 'ab' >>> m.group(1 ) 'a' >>> p = re.compile('(a(b)c)d' )>>> m = p.match('abcd' )>>> m.group(0 )'abcd' >>> m.group(1 )'abc' >>> m.group(2 )'b' >>> m.group(2 ,1 ,0 ) ('b' , 'abc' , 'abcd' )
1.非捕获组和命名组
它们都使用了一个公共的正则表达式扩展语法;精心设计的正则表达式可能会划分很多组,这些组不仅可以匹配相关的子串,还能够对正则表达式本身进行分组和结构化。
产生原因:复杂的正则表达式中,由于有太多的组因此通过组的序号来跟踪和使用会变得困难。
正则表达式的(?…)扩展语法:
问号 ? 紧跟在左小括号 ( 后边,本身是一个语法错误的写法,因为 ? 前边没有东西可以重复,所以这样就解决了兼容性的问题(理由是语法正确的正则表达式肯定不会这么写嘛~)。
然后紧跟在 ? 后边的字符则表示哪些扩展语法会被使用。例如 (?=foo) 表示一种新的扩展功能(前向断言),(?:foo) 则表示另一种扩展功能(一个包含子串 foo 的非捕获组)。
非捕获组: “捕获”就是匹配的意思啦,普通的子组都是捕获组,因为它们能从字符串中匹配到数据。 非捕获组案例:1 2 3 4 5 6 >>> m = re.match("([abc])+" , "abc" ) >>> m.groups()('c' ,) >>> m = re.match("(?:[abc])+" , "abc" ) >>> m.groups()()
除了你不能从非捕获组获得匹配的内容之外,其他的非捕获组跟普通子组没有什么区别了。你可以在里边放任何东西,使用重复功能的元字符,或者跟其他子组进行嵌套(捕获的或者非捕获的子组都可以)。 当你需要修改一个现有的模式的时候,(?:…) 是非常有用的。原始是添加一个非捕获组并不会影响到其他(捕获)组的序号。值得一提的是,在搜索的速度上,捕获组和非捕获组的速度是没有任何区别的。
命名组: 普通子组我们使用序列来访问它们,命名组则可以使用一个有意义的名字来进行访问。 命名组的语法是 Python 特有的扩展语法:(?P)。很明显< > 里边的 name 就是命名组的名字啦。命名组除了有一个名字标识之外,跟其他捕获组是一样的。
命名组案例:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 >>> p = re.compile(r'(?P<word>\b\w+\b)' )>>> m = p.search( '(((( Lots of punctuation )))' )>>> m.group('word' ) 'Lots' >>> m.group(1 )'Lots' InternalDate = re.compile(r'INTERNALDATE "' r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-' r'(?P<year>[0-9][0-9][0-9][0-9])' r'(?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])' r'(?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])' r'"' ) >>> p = re.compile(r'(?P<word>\b\w+)\s+(?P=word)' )>>> p.search('Paris in the the spring' ).group()'the the'
2.分组进阶高级前向断言 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 .*[.].*$ .*[.][^b].*$ .*[.]([^b]..|.[^a].|..[^t])$ .*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$ # 在第三次尝试中,我们让第二个和第三个字符变成可选的。这样就可以匹配稍短的扩展名,比如 sendmail.cf。 .*[.](?!bat$).*$ #一个前向否定断言就可以解决你的难题 .*[.](?!bat$|exe$).*$ #有了前向否定断言,要同时排除 bat 和 exe 扩展名,也变得相当容易 ''' ''' >>> p = re.compile('section{ ( [^}]* ) }' , re.VERBOSE) >>> p.sub(r'subsection{\1}' ,'section{First} section{second}' )'subsection{First} subsection{second}' >>> p = re.compile('section{ (?P<name> [^}]* ) }' , re.VERBOSE)>>> p.sub(r'subsection{\1}' ,'section{First}' ) 'subsection{First}' >>> p.sub(r'subsection{\g<1>}' ,'section{First}' ) 'subsection{First}' >>> p.sub(r'subsection{\g<name>}' ,'section{First}' )'subsection{First}'
补充提示:
有几对小括号就是分成了几个子组,例如 (a)(b) 和 (a(b)) 都是由两个子组构成的。
Python 的字符串中会使用反斜杠加数字的方式来表示数字的值对应的 ASCII 字符,所以在使用反向索引的正则表达式中,我们依然强调要使用原始字符串。
反向引用指的是你可以在后面的位置使用先前匹配过的内容,用法是反斜杠加上数字。例如 \1 表示引用前边成功匹配的序号为 1 的子组。
0x03 常用正则表达式