我的根据《正则指引》内容总结记录,侵删!!javascript
转载至个人博客java
最近看了编译原理方面的书,以为正则表达式很是重要,在各个语言当中都有支持,因此总结了这篇文章,做为学习总结以及记录~python
Regular Expression
即描述某种规则的表达式。git
字符组(Character Class
)是一组字符,表示 “在同一个位置可能出现的各类字符”正则表达式
其写法是在一对方括号[
和]
之间列出全部可能出现的字符。markdown
#只要字符串中包含数字、字符就能够匹配 re.search("[0123456789]","2") != None #=>True 复制代码
默认状况下re.search(pattern,string)
只判断string
的某个子串可否由pattern
匹配,为了测试整个string
可否被pattern
匹配,在pattern
两端加上^
和 $
。它们并不匹配任何字符,只表示“定位到字符串的起始位置”和“定位到字符串的结束位置”。oop
#使用^和$测试string被pattern完整匹配 #只要字符串中包含数字、字符就能够匹配 re.search("[0123456789]","2") != None #=>True re.search("[0123456789]","a2") != None #=>True #整个字符串就是一个数字字符,才能够匹配 re.search("^[0123456789]$","2") != None #=>True re.search("^[0123456789]$","12") != None #=>False 复制代码
字符组中的字符排列顺序并不影响字符组的功能,出现重复字符也不影响,可是并不推荐在字符组中出现重复字符。性能
例如上例中匹配数字就要把全部数字都列出来仍是有些繁琐,为此正则表达式提供了范围表示法(range
),它更直观,能进一步简化字符组。学习
在字符组中-
表示范围,通常是根据字符对应的码值(Code Point
)也就是字符在对应码表中的编码数值来肯定的。小的在前,大的在后,因此[0-9]
正确,而[9-0]
会报错。测试
在字符组中能够同时并列多个-范围表示法
。
#[0-9a-fA-F]准确判断十六进制字符 re.search("^[0-9a-fA-F]$","0") != None #=>True re.search("^[0-9a-fA-F]$","c") != None #=>True re.search("^[0-9a-fA-F]$","i") != None #=>True 复制代码
还能够用转义序列\xhex
来表示一个字符,其中\x
是固定前缀。字符组中有时会出现这种表示法,它能够表现一些难以输入或者难以显示的字符。依靠这种表示法能够很方便的匹配全部的中文字符。
#[\x00-\x7F]准确判断ASCII字符 re.search("^[\x00-\x7F]$","c") != None #=>True re.search("^[\x00-\x7F]$","I") != None #=>True re.search("^[\x00-\x7F]$","<") != None #=>True re.search("^[\x00-\x7F]$","0") != None #=>True 复制代码
字符组中的-
并不能匹配横线字符,这类字符叫作元字符。[
、]
、^
、$
都算元字符。
若是-
紧邻字符组中的[
那么它就是普通字符,其余状况都是元字符。
取消特殊含义的作法是在元字符前加上反斜杠\
。
#做为普通字符 re.search("^[-09]$","3") != None #=>False re.search("^[-09]$","-") != None #=>True #做为元字符 re.search("^[0-9]$","3") != None #=>True re.search("^[0-9]$","-") != None #=>False #转义以后做为普通字符 re.search("^[0\\-9]$","3") != None #=>False re.search("^[0\\-9]$","-") != None #=>True 复制代码
这段例子中,正则表达式是以字符串的方式传入的,而字符串自己也有关于转义的规定,因此要加两个反斜杠\\
。
针对这种问题Python
提供了原生字符串(Raw String),不须要考虑正则表达式以外的转义(只有双引号是例外,必须转义成\"
)。
#原生字符串的使用 r"^[0\-9]$" == "^[0\\-9]$" #=>True #原生字符串的转义要简单许多 re.search(r"^[0\-9]$","3") != None #=>False re.search(r"^[0\-9]$","-") != None #=>True #]出现的位置不一样含义不一样 #未转义的] re.search(r"^[012]345$","2345") != None #=>True re.search(r"^[012]345]$","5") != None #=>False re.search(r"^[012]345]$","]") != None #=>False #转义的] re.search(r"^[012\]345]$","2345") != None #=>False re.search(r"^[012\]345]$","5") != None #=>True re.search(r"^[012\]345]$","]") != None #=>True 复制代码
请注意,只有开方括号[
须要转义,闭方括号]
不用。
#取消其余元字符的特殊含义 re.search(r"^[012]345]$","3") != None #=>False re.search(r"^[012\\]345]$","3") != None #=>False re.search(r"^[012]$","[012]") != None #=>False re.search(r"^\[012]$","[012]") != None #=>True 复制代码
排除型字符组(Negated Character Class)只是在**方括号[
以后紧跟一个脱字符^**
,因此[^0-9]
表示0-9
以外的字符,也就是“非数字字符”。
#使用排除型字符组 re.search(r"^[^0-9][0-9]$","A8") != None #=>True re.search(r"^[^0-9][0-9]$","x6") != None #=>True #排除型字符组必须匹配一个字符 re.search(r"^[0-9][0-9]$","8") != None #=>False #排除型字符组中,紧跟在"^"以后的一个"-"不是元字符 #匹配一个"-"、"0"、"9"以外的字符 re.search(r"^[^0-9]$","-") != None #=>False re.search(r"^[^-09]$","8") != None #=>True 复制代码
在排除型字符组中,^
是一个元字符,但只有它紧跟在[
以后时才是元字符,若是想表示这个字符组中能够出现^
字符,不要让它紧挨着[
,不然要转义。
#匹配4个字符之一:"0","^","1","2" re.search(r"^[0^12]$","^") != None #=>True #"^"紧跟在"["以后,但通过转义变为普通字符,等于上一个表达式,不推荐。 re.search(r"^[\^012]$","^") != None #=>True 复制代码
字符组间记法(shorthands):对于经常使用的表示数字字符、小写字母这类字符组提供的简单记法。
常见的有\d
、\w
、\s
,其中\d
等价于[0-9]
,d
表明“数字(digit)”;\w
等价于[0-9a-zA-Z_]
,w
表明“单词(word)”;\s
等价于[ \t\r\n\v\f]
(第一个字符是空格),s
表明“空白字符(space)”。(这些等价前提是采用ASCII匹配规则,采用Unicode匹配规则就不对了)。
#若是没有原声字符串\d就必须写做\\d re.search(r"^\d$","8") != None #=>True re.search(r"^\d$","a") != None #=>False re.search(r"^\w$","8") != None #=>True re.search(r"^\w$","a") != None #=>True re.search(r"^\w$","_") != None #=>True re.search(r"^\s$"," ") != None #=>True re.search(r"^\s$","\t") != None #=>True re.search(r"^\s$","\n") != None #=>True 复制代码
\w
能匹配下划线_
。
#字符组简记法与普通字符组混用 #用在普通字符组内部 re.search(r"^[\da-zA-Z]$","8") != None #=>True re.search(r"^[\da-zA-Z]$","a") != None #=>True re.search(r"^[\da-zA-Z]$","c") != None #=>True #用在排除型字符组内部 re.search(r"^[^\w]$","8") != None #=>False re.search(r"^[^\w]$","_") != None #=>False re.search(r"^[^\w]$",",") != None #=>True 复制代码
相对于\d
、\w
和\s
这三个普通字符组简记法,正则表达式也提供了对应的排除型字符组的简记法:\D
、\W
和\S
——字母彻底同样,只是改成大写。
这些简记法匹配字符互补:\s
能匹配的字符,\S
必定不能匹配,其余同理。
#\d和\D re.search(r"^\d$","8") != None #=>True re.search(r"^\d$","a") != None #=>False re.search(r"^\D$","8") != None #=>False re.search(r"^\D$","a") != None #=>True #\w和\W re.search(r"^\w$","c") != None #=>True re.search(r"^\w$","!") != None #=>False re.search(r"^\W$","c") != None #=>False re,search(r"^\W$","!") != None #=>True #\s和\S re.search(r"^\s$","\t") != None #=>True re.search(r"^\s$","0") != None #=>False re.search(r"^\S$","\t") != None #=>False re.search(r"^\S$","0") != None #=>True 复制代码
字符组只能匹配单个字符,为此正则表达式提供了量词(quantifier),来支持匹配多个字符的功能。
#重复肯定次数的量词 re.search(r"^\d{6}$","100859") != None #=>True re.search(r"^\d{6}$","20103") != None #=>False 复制代码
量词还能够表示不肯定的长度,其通用形式是{m,n}
,其中m
和n
是两个数字(逗号以后毫不能有空格),它限定以前的元素可以出现的次数,m
是下限,n
是上限(均为闭区间)。若是不肯定长度的上限,也能够省略,写成\d{m,}
。量词限定通常都有明确的下限,若是没有,则默认为0。有些语言支持{,n}
的记法,省略下限为0的状况,但这种用法并非全部语言都通用的,最好使用{0,n}
的记法。
量词 | 说明 |
---|---|
{n} | 以前的元素必须出现n次 |
{m,n} | 以前的元素最少出现m次,最多出现n次 |
{m,} | 以前的元素最少出现m次,出现次数无上限 |
{0,n} | 以前的元素能够不出现,也能够出现,最多出现n次(在某些语言中能够写为{,n}) |
#表示不肯定长度的量词 re.search(r"^\d{4,6}$","123") != None #=>False re.search(r"^\d{4,5}$","1234") != None #=>True re.search(r"^\d{4,6}$","123456") != None #=>True re.search(r"^\d{4,6}$","1234567") != None #=>False re.search(r"^\d{4,}$","123") != None #=>False re.search(r"^\d{4,}$","1234") != None #=>True re.search(r"^\d{4,}","123456") != None #=>True re.search(r"^\d{0,6}$","12345") != None #=>True re.search(r"^\d{0,6}$","123456") != None #=>True re.search(r"^\d{0,6}$","1234567") != None #=>False 复制代码
{m,n}
是通用形式的量词,正则表达式还有三个经常使用量词,分别是+
、?
、*
。它们形态虽然不一样于{m,n}
,功能却相同。(能够理解为“量词简记法”)
经常使用量词 | {m,n}等价形式 | 说明 |
---|---|---|
* | {0,} | 可能出现,也可能不出现,出现次数没有上限 |
+ | {1,} | 至少出现1次,出现次数没有上限 |
? | {0,1} | 至多出现1次,也可能不出现 |
#量词?的应用 re.search(r"^travell?er$","traveler") != None #=>True re.search(r"^travell?er$","traveller") != None #=>True #量词+的应用 re.search(r"^<[^>]+>$","<bold>") != None #=>True re.search(r"^<[^>]+>$","</table>") != None #=>True re.search(r"^<[^>]+>$","<>") != None #=>False #量词*的使用 re.search(r"^\"[^\"]*\"$","\"some\"") != None #=>True re.search(r"^\"[^\"]*\"$","\"\"") != None #=>True 复制代码
通常文档都说点号能够匹配“任意字符”,可是换行符\n
不能匹配,若是非要匹配"任意字符",有两种办法:可使用单行匹配;或者使用[\s\S]
(也可使用[\w\W]
、[\d\D]
)。
#点号.的匹配 re.search(r"^.$","a") != None #=>True re.search(r"^.$","0") != None #=>True re.search(r"^.$","*") != None #=>True #换行符的匹配 re.search(r"^.$","\n") != None #=>False #单行匹配 re.search(r"(?s)^.$","\n") != None #=>True re.search(r"^[\s\S]$","\n") != None #=>True 复制代码
当使用量词匹配字符串有时会出现意料以外的错误状况。
#字符串的值是"quoted string" print(re.search(r"\".*\"","\"quoted string\"").group()) "quoted string" #字符串的值是"string" and another" print(re.search(r"\".*\"","\"quoted string\" and another\"").group()) "quoted string" and another" 复制代码
咱们只想匹配"quoted string"
可是下面的语句匹配到了错误的"quoted string" and another"
,这是由于默认的量词匹配采用贪婪规则。就是在拿不许是否要匹配时,先尝试匹配,而且记下这个状态,以备未来"反悔"。这个“反悔”的过程叫作回溯(backtracking)。
#准确匹配双引号字符串,采用懒惰规则 print(re.search(r"\".*?\"","\"quoted string\" and another\"").group()) "quoted string" 复制代码
贪婪匹配量词 | 懒惰匹配量词 | 限定次数 |
---|---|---|
* | *? | 可能不出现,也可能出现,出现次数没有上限 |
+ | +? | 至少出现1次,出现次数没有上限 |
? | ?? | 至多出现1次,也可能不出现 |
{m,n} | {m,n}? | 出现次数最少为m次,最多为n次 |
{m,} | {m,}? | 出现次数最少为m次,没有上限 |
{,n} | {,n}? | 可能不出现,也可能出现,最多出现n次 |
jsStr = ''' <script type="text/javascript"> alert("1"); </script> <br /> <script type="text/javascript"> alert("2"); </script> ''' #贪婪匹配 jsRegex = r"<script type=\"text/javascript\">[\s\S]*</script>" print(re.search(jsRegex,jsStr).group()) #输出 ''' <script type="text/javascript"> alert("1"); </script> <br /> <script type="text/javascript"> alert("2"); </script> ''' #懒惰匹配 jsRegex = r"<script type=\"text/javascript\">[\s\S]*?</script>" print(re.search(jsRegex,jsStr).group()) #输出 ''' <script type="text/javascript"> alert("1"); </script> ''' 复制代码
各类量词的转义形式
量词 | 转义形式 |
---|---|
{n} | \{n} |
{m,n} | \{m,n} |
{m,} | \{m,} |
{,n} | \{,n} |
* | \* |
+ | \+ |
? | \? |
*? | \*\? |
+? | \+\? |
?? | \?\? |
. | \. |
#忽略转义点号可能致使错误 #错误判断浮点数 re.search(r"^\d+.\d+$","3.14") != None #=>True re.search(r"^\d+.\d+$","3a14") != None #=>True #准确判断浮点数 re.search(r"^\d+\.\d+$","3.14") != None #=>True re.search(r"^\d+\.\d+$","3a14") != None #=>False 复制代码
使用括号()
能够将一个字符、字符组或表达式包围起来做为一个总体,再用量词限定它们出现的次数,这种功能叫作分组。
#用括号改变量词的做用元素 re.search(r"^ab+$","ab") != None #=>True re.search(r"^ab+$","abb") != None #=>True re.search(r"^ab+$","abab") != None #=>True re.search(r"^(ab)+$","ab") != None #=>True re.search(r"^(ab)+$","abb") != None #=>False re.search(r"^(ab)+$","abab") != None #=>True #身份证号码的准确匹配 r"^[1-9]\d{14}(\d{2}[0-9xX])?$" 复制代码
多选结构的形式是(...|…)
,在括号内以竖线|
分隔开多个子表达式,这些表达式也叫多选分支(option);在一个多选结构内,多选分支的数目没有限制。在匹配时,整个多选结构被视为单个元素,只要其中某个子表达式可以匹配,整个多选结构的匹配就成功了;若是全部子表达式都不能匹配,则整个多选结构匹配失败。
#用多选结构匹配身份证号码 r"^([1-9\d{14}|[1-9]{16}[0-9xX]])$" #准确匹配0-255之间的字符串 r"^([0-9]|[0-9]{2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$" 复制代码
多选结构的补充:
第1、多选结构通常会同时使用括号()
和竖线|
;可是没有括号()
,只出现竖线|
,仍然是多选结构。
第2、多选结构并不等于字符组。字符组匹配要比多选结构效率高不少,字符组只能匹配单个字符,多选结构的每一个分支长度没有限制。
第3、多选结构应当避免某段文字能够被多个分支同时匹配的状况,这将大大增长回溯的计算量,影响效率。若是遇到多个分支都能匹配的字符串,大多数语言优先匹配左侧分支。
#多选结构的匹配顺序 re.search(r"^jeffrey|jeff$","jeffrey").group() 'jeffrey' re.search(r"^jeff|jeffrey$","jeffrey").group() 'jeff' 复制代码
使用括号以后,正则表达式会保存每一个分组真正匹配的文本,等到匹配完成后,经过**group(num)**之类的方法"引用"分组在匹配时捕获的内容。其中num表示对应括号的编号,不管括号如何嵌套,分组编号都是根据开括号出现的顺序来计数的;开括号是从左到右数起第多少个开括号,整个括号分组的编号就是多少。编号从1开始计数,不过也有0号分组,它是默认存在的,对应整个表达式匹配的文本。
#引用捕获分组 re.search(r"(\d{4})-(\d{2})-(\d{2})","2018-10-24").group() '2018-10-24' re.search(r"(\d{4})-(\d{2})-(\d{2})","2018-10-24").group(0) '2018-10-24' re.search(r"(\d{4})-(\d{2})-(\d{2})","2018-10-24").group(1) '2018' re.search(r"(\d{4})-(\d{2})-(\d{2})","2018-10-24").group(2) '10' re.search(r"(\d{4})-(\d{2})-(\d{2})","2018-10-24").group(3) '24' #嵌套的括号 nestedGroupingRegex = r"(((\d{4})-(\d{2}))-(\d{2}))" re.search(nestedGroupingRegex,"2018-10-24").group(0) '2018-10-24' re.search(nestedGroupingRegex,"2018-10-24").group(1) '2018-10-24' re.search(nestedGroupingRegex,"2018-10-24").group(2) '2018-10' re.search(nestedGroupingRegex,"2018-10-24").group(3) '2018' re.search(nestedGroupingRegex,"2018-10-24").group(4) '10' re.search(nestedGroupingRegex,"2018-10-24").group(5) '24' 复制代码
容易错误的状况:
#容易弄错的分组的结构 re.search(r"^(\d){4}-(\d{2})-(\d{2})$","2018-10-24").group(1) '8' 复制代码
这个表达式中编号为1的括号是(\d)
,其中\d
是“匹配一个数字字符“的子表达式,由于以后有量词{4}
,因此整个括号做为单个元素,要重复4次,并且编号都是1;因而每重复一次,就要更新一次匹配结果。因此在匹配过程当中,编号为1的分组匹配文本的值,依次是2
、0
、1
、0
,最后的结果是0。
#正则表达式的替换 re.sub(r"[a-z]"," ","1a2b3c") '1 2 3 ' #在替换中使用分组 re.sub(r"(\d{4})-(\d{2})-(\d{2})",r"\2/\3/\1","2018-10-24") '10/24/2018' re.sub(r"(\d{4})-(\d{2})-(\d{2})",r"\1年\2月\3日","2018-10-24") '2018年10月24日' 复制代码
**反向引用(back-reference)它容许在正则表达式内部引用以前的捕获分组匹配的文本(也就是左侧),其形式也是\num,其中num表示所引用分组的编号,编号规则与以前介绍的相同。
#用反向引用匹配重复字符 re.search(r"^([a-z])\1$","aa") != None #=>True re.search(r"^([a-z])\1$","ac") != None #=>False #用反向引用匹配成对的tag pairedTagRegex = r"^<([^>]+)[\s\S]*?</\1>$" re.search(pairedTagRegex,"<h1>title</h1>") != None #=>True re.search(pairedTagRegex,"<h1>title</bold>") != None #=>False #用反向引用匹配更复杂的成对tag pairedTagRegex = r"^<([a-zA-Z0-9]+)(\s[^>]+)?>[\s\S]*?</\1>$" re.search(pairedTagRegex,"<h1>title</h1>") != None #=>True re.search(pairedTagRegex,"<span class=\"class1\">text</span>") != None #=>True re.search(pairedTagRegex,"<h1>title</bold>") != None #=>False 复制代码
反向引用重复的是对应捕获分组匹配的文本,而不是以前的表达式;也就是说,反向引用的是由以前表达式决定的具体文本,而不是符合某种规则的位置文本。
#匹配IP地址的正则表达式 #匹配其中一段的表达式 segment = r"(0{0,2}[0-9]|0?[0-9]{2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])" #正确的表达式 idAddressRegex = r"(" + segment + r"\.){3}" + segment #错误的表达式 idAddressRegex = segment + r"\.\1.\1.\1" 复制代码
语言 | 表达式中的反向引用 | 替换中的反向引用 |
---|---|---|
.NET | \num | $num |
Java | \num | $num |
JavaScript | $num | $num |
PHP | \num | \num或$num(PHP4.0.4以上版本) |
Python | \num | \num |
Ruby | \num | \num |
通常来讲,$num要好于\num。缘由在于,$0能够准确表示“第0个分组”,而**\0则不行,由于很多语言的字符串中,\num自己是一个有意义的转义序列,它表示值为num的ASCII字符,因此\0会被解释为“ASCII编码为0的字符”。可是反向引用不存在这个问题,由于不能在正则表达式还没匹配结束时,就用\0**引用整个表达式匹配的文本。
可是不管是**\num仍是$num**,都有可能遇到二义性的问题:若是出现了**\10**(或者**$10**),它究竟是表示第10个捕获分组,仍是第1个捕获分组以后跟着一个字符0?
Python将**\10**解释成“第10个捕获分组匹配的文本”,若是想表示第1个分组以后跟一个0,须要消除二义性。
#使用g<n>消除二义性 re.sub(r"(\d)",r"\g<1>0","123") '102030' 复制代码
Python和PHP的规定明确,因此避免了**\num的二义性;Java、Ruby、Javascript这样规定\num**,若是一位数,则引用对应的捕获分组;若是是两位数且存在对应的捕获分组时,引用对应的捕获分组,若是不存在则引用一位数编号的捕获分组。这样若是存在编号为10的捕获分组,没法用**\10**表示“编号为1的捕获分组和字符0”,若是在开发中遇到这个问题,现有规则下无解,但可使用明明分组解决此问题。
为了解决捕获分组数字编号不够直观和会引发冲突的问题,一些语言提供了命名分组(named grouping)。
在Python中用(?P<name>regex)
来分组,其中的name是赋予这个分组的名字,regex则是分组内的正则表达式。
#命名分组捕获 namedRegex = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})" result = re.search(namedRegex, "2018-10-24") print(result.group("year")) 2018 print(result.group("month")) 10 print(result.group("day")) 24 #命名分组捕获时仍然保留了数字编号 print(result.group(1)) 2018 print(result.group(2)) 10 print(result.group(3)) 24 #命名分组的引用方法 re.search(r"^(?P<char>[a-z])(?P=char)$","aa") != None #=>True re.sub("(?P<digit>\d)",r"\g<digit>0","123") '102030' 复制代码
不一样语言中命名分组的记法
语言 | 分组记法 | 表达式中的引用记法 | 替换时的引用记法 |
---|---|---|---|
.NET | (?...) | \k | ${name} |
Java7开始支持 | (?...) | \k | ${name} |
PHP | (?P...) | (?P=name) | 不支持,只能使用\${num},其中num 为对应分组的数字编号 |
Python | (?P...) | (?P=name) | \g |
Ruby | (?...) | \k | \k |
在使用分组时,只要出现了括号,正则表达式在匹配时就会把括号内的子表达式存储起来,提供引用。若是不须要引用,保存这些信息无疑会影响正则表达式的性能;若是表达式比较复杂,要处理的文本又不少,更可能严重影响性能。
为解决这种问题,提供了非捕获分组(non-capturing group),非捕获分组相似普通分组,只是在开括号后紧跟一个问号和冒号(?:...)
,这样的括号叫作非捕获型括号。在引用分组时,分组的编号一样会按开括号的顺序从左到右递增,只不过必须以捕获分组为准,非捕获分组会掠过。
re.search(r"(\d{4})-(\d{2})-(\d{2})","2018-10-24").group(1) '2018' re.search(r"(?:\d{4})-(\d{2})-(\d{2})","2018-10-24").group(1) '10' 复制代码
括号的转义必须转义与括号有关的全部元字符包括(
、)
和|
。由于括号很是重要,因此不管时开括号仍是闭括号,只要出现,正则表达式就会尝试寻找整个括号,若是只转义了开括号而没有转义闭括号,通常会报告"括号不匹配"的错误。另外,多选结构中的|
也必须转义。
#括号的转义 re.search(r"^\(a\)$","(a)") != None #=>True re.search(r"^\(a)$","(a)") != None #=>报错 #未转义| re.search(r"^(\(a|b\))$","(a|b)") != None #=>False re.search(r"^(\(a\|b\))$","(a|b)") != None #=>True 复制代码
正则表达式中的大多数的结构匹配的文本会出如今最终的匹配结果中,可是有些结构并不真正匹配文本,而只负责判断某个位置左/右侧的文本是否符合要求,这种结构被称为断言(assertion)。常见的断言有三类:单词边界、行起始/结束位置、环视。
待补充
我的根据《正则指引》内容总结记录,侵删!!