一 概述
正则表达式的历史:数学家发明,unix、B语言、C语言之父Ken Thompson应用于计算机编程领域(具体请百度)
适用范围:编程中的字符串查找、替换、判断是否匹配特定字符串;各种文本编辑工具(notepad++、editplus等)IDE编程环境(如vs2008等)中的查找和替换。
二 学习正则表达式
采用讲解加实际代码验证的方式来学习,代码使用python提供的re模块来演示。其他很多语言也都不同程度的支持正则表达式,但在我会使用的语言中python支持的最好。
go语言不支持分组;VBS、JS不支持”负向零宽断言”。
1 python如何使用正则表达式
import re
def OutputRet(match):
if match:
print "find:" + match.group()
print "match.start:", match.start()
print "match.end:", match.end()
else:
print "not find!"
#====================================== MAIN =======================================
print 'starting...'
match = re.search('678', '1234567890')
OutputRet(match)
print 'end!'
执行输出结果如下:
>>>
starting...
find:678
match.start: 5
match.end: 8
end!
>>>
2 正则表达式知识点
我们将以上面的python框架代码为基础,来循序渐进地学习 正则表达式,首先介绍第一个概念:
2.1 元字符
在上面的python框架代码里面,我们在字符串‘1234567890’中搜索‘678’子串,这和普通的字符串查找没有什么区别,正则表达式其强大的地方在于模糊搜索、匹配。
比如我想搜索字符串6xxxxxx8,既以6开头,以8结尾,中间可以加入任意或符合一定条件的其他字符的子串。如果模糊匹配的条件比较复杂,这个时候再自行编程使用传统的字符串搜索方式进行匹配将会非常麻烦,代码的逻辑复杂度随着模糊匹配的条件复杂度一起上升,对程序员的大脑是一个很大的考验,幸好我们可以借助“正则表达式”来解决这类问题。
要想进行模糊匹配就需要定义一些能够表示模糊范围概念的东西,这种东西被称为元字符。以下是常用的元字符列表:
代码 | 说明 |
---|---|
. | 匹配除换行符以外的任意字符 |
\w | 匹配字母或数字或下划线或汉字(word) |
\s | 匹配任意的空白符(space) |
\d | 匹配数字(digit) |
\b | 匹配单词的开始或结束 |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结束 |
需要注意的是一个元字符只表示字符窜的一个字符。
现在我们可以提一个新的查找要求:在字符串“1234567890"中查找6开头8结尾,中间是1个数字的子串。只需要修改代码如下:
match = re.search(r'6\d8', '1234567890') (\d表示一个数字)(r'6\d8'开头的r表示后面的字符串是原生字符串,原生字符串里面\不视为转义字符,下同)
2.2 字符集
元字符虽然已经定义了一些分类,但远远不能满足形式多样的需求,比如判断一个字符串是否是合法的8进制数字,里面的每个数字范围应该是0-7,这个时候就要用到字符集了,表示方式为[0-7]。二进制数只有0和1这2个字符,可以用[01]表示([10]和[01]意思一样)。需要注意的是[0-7]表示0至7的闭区间,而[07]只表示0和7两个字符的集合。对于16进制的字符集合我们可以用[0-9A-Fa-f]表示。同样[0-9]和元字符\d的意思相同。所以上面例子的代码也可以写成:
match = re.search('6[0-9]8', '1234567890')
2.3 限定符
在上面的例子里面我们演示了如何查找开头是6,结尾是8,中间是1个数字的子串,假设现在搜索条件改为中间是任意多个数字、中间是指定的N个数字,或者中间是满足一个范围M-N个数字的子串呢?
这个时候我们就要学习 正则表达式 中的限定符了:
代码/语法 | 说明 |
---|---|
? | 重复0次或1次 |
+ | 重复1次或更多次 |
* | 重复0次或更多次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n,m} | 重复n到m次 |
下面我们来构造一些使用到限定符的正则表达式
(1)匹配6开头,8结尾,中间含有2个数字的子串
'6\d{2}8'
(2)匹配6开头,8结尾,中间含有5,至10个数字的子串
'6\d{5,10}8'
(3)匹配6开头,8结尾,中间含有任意个数字(可以为0个)的子串
'6\d*8'
2.4 分支条件
有时候我们需要匹配2种不同的条件,这个时候我们就可以使用分支条件表达式。例如电话号码存在本地市话不加区号的,外地加区号的,以及手机号码等多种情况。我们来写一个表示8位固化以及11位手机号码的正则表达式:
'\d{8}|\d{11}'说明:{8}和{11}分别表示重复8次和重复11次。这里不能写成'\d{8,11}',因为{8,11}表示重复8至11次,9次、10次都满足要求。
2.5 反义
反义类似集合的补集。针对常用的元字符有对应的反义字符:
代码/语法 | 说明 |
---|---|
\W | 匹配任意不是字母,数字,下划线,汉字的字符 |
\S | 匹配任意不是空白符的字符 |
\D | 匹配任意非数字的字符 |
\B | 匹配不是单词开头或结束的位置 |
[^abc] | 匹配除了a、b、c以外的任意字符 |
例如我们需要匹配6开头,8结尾,中间含有1个非6非8的子串,可以这样写正则表达式:
'6[^68]8'
2.6 分组(子表达式)
在前面2.3节中我们讲了限定符,其作用是重复前面的字符一定次数。但是在前面举的例子中,限定符只能重复前面的一个字符,如果我们要重复前面的多个字符时,就需要用到分组了。分组用一对小括号表示。例如要查找686868这样的字串,我们可以看到68重复了3次,我们可以这样写正则表达式:
'(68){3}'分组还有其他一些妙用,我们讲在下面一一讲述。
2.7 后向引用分组
在上一小节我们介绍了分组,其实分组是可以在分组之后的表达式中被引用的。每一个分组都有一个编号(也可以显示指定不给某个分组分配编号)。例如表达式:
'(\d)\1'表示查找两个数字相同的字串,如'11','22'这样的字串。注意和表达式'(\d){2}'区分,'(\d){2}'可以匹配任意的2个数字字符的组合,如'23',‘45’等。
其中的‘\1’是引用第一个分组的内容,第一个分组就是(\d)。
我们也可以指定对某些分组不给予编号,或者对某些分组给予自定义命名。如下表:
(exp) | 匹配exp,并捕获文本到自动命名的组里 |
(?<name>exp) | 匹配exp,并捕获文本到名称为name的组里,也可以写成(?'name'exp) |
(?:exp) | 匹配exp,不捕获匹配的文本,也不给此分组分配组号 |
2.8 零宽断言(肯定性断言)
有时候我们想搜索在字串的前面或者后面满足一定条件的字串。例如C语言中定义的16进制数值,像0x004010af这样的字串,我们想提取其中真正表示16进制数值的部分,即‘004010af’,这个时候我们就要使用零宽断言。
(?=exp) | 要求得到的子串后面内容为exp,此表达式放在要匹配内容的后面 |
(?<=exp) | 要求得到的子串前面内容为exp,此表达式放在要匹配内容的前面 |
match = re.search('((?<=0x)|(?<=0X))[0-9a-fA-F]*', '0x004010af')从‘004010af0X’中提取'004010af‘的正则表达式代码如下:
match = re.search('[0-9a-fA-F]*((?=0x)|(?=0X))', '004010af0X')注意以上两个例子代码中零宽断言表达式出现的位置。
2.9 负向零宽断言(否定性断言)
上面的零宽断言,我们是要求在搜索字符串的前后要满足怎样的条件,如果我们要求在搜索字符串的前后不能是什么样的字符串时,我们就需要使用 负向零宽断言 了。
(?!exp) | 要求得到的子串后面内容不是exp,此表达式放在要匹配内容的后面 |
(?<!exp) | 要求得到的子串前面内容不是exp,此表达式放在要匹配内容的前面 |
match = re.search('[0-9]*((?!1)(?!;)(?!0))', '00402001;')得到的结果是:‘0040’
[0-9]*((?!1)(?!;)(?!0))表示任意长度的数字组成的子串,但是字串后面(不属于字串)不能是0、1和';'。‘0040’后面是‘2’,满足要求,往后更长的字符串都不满足要求。
注意:在上面2节的例子中,分别展示了 零宽断言(肯定)的分支条件(|表示)表示和负向零宽断言(否定)的多条件同时满足表示方式。
代码:
match = re.search('((?<!1)(?<!0)(?<!^))[0-9]*', '00402001')得到的结果是:‘02001’
((?<!1)(?<!0)(?<!^))[0-9]*表示匹配的子串不在字符串的开头,且前面一个字符(不属于得到的字串)不是0、1。
2.10 注释
(?#comment)表示注释。
2.11 贪婪与懒惰
正则表达式在遇到“限定符”的时候,默认是采用贪婪匹配的方式,也就是尽可能匹配更多的字符。例如:
match = re.search('[0-9]+', '00402001')得到的结果是:00402001
因为+限定符表示1个或更多个,在贪婪模式下会匹配尽可能多的字符。如果我们讲代码改为:
match = re.search('[0-9]+?', '00402001')得到的结果是:0
这里+后面的?,表示的意思是”懒惰匹配“,也就是匹配满足条件但尽可能少的字符。
*? | 重复任意次,但尽可能少重复 |
+? | 重复1次或更多次,但尽可能少重复 |
?? | 重复0次或1次,但尽可能少重复(无实际意义) |
{n,m}? | 重复n到m次,但尽可能少重复 |
{n,}? | 重复n次以上,但尽可能少重复 |
2.12 转义
我们注意到在元字符和限定符里面,我们使用了
. | ^ | $ | ? | + | ? |
match = re.search(r'\++', '00+++40n2001')得到的结果是:+++
前面的\+表示我们需要匹配真正的+,后面一个+表示匹配一个或多个。
更多的转义字符:
代码/语法 | 说明 |
---|---|
\a | 报警字符(打印它的效果是电脑嘀一声) |
\b | 通常是单词分界位置,但如果在字符类里使用代表退格 |
\t | 制表符,Tab |
\r | 回车 |
\v | 竖向制表符 |
\f | 换页符 |
\n | 换行符 |
\e | Escape |
\0nn | ASCII代码中八进制代码为nn的字符 |
\xnn | ASCII代码中十六进制代码为nn的字符 |
\unnnn | Unicode代码中十六进制代码为nnnn的字符 |
\cN | ASCII控制字符。比如\cC代表Ctrl+C |
三 练习
1 匹配不超过55,且尾数不能是2,4的数字子串:
'5[013]|[0-4]?[01356789]'
2 写一个判断给定字符串表示的是0到255之间的数字的正则表达式:
'25[0-5]|2[0-4]\d|[01]?\d?\d'
3 写一个可匹配IP地址的正则表达式:
'(25[0-5]|2[0-4]\d|[01]?\d?\d\.){3}(25[0-5]|2[0-4]\d|[01]?\d?\d)'
4 判断一个字符串中的每一个字符是不是只出现了一次:
''^(?:(.)(?!.*?\1))+$'
5 说明下面的正则表达式所匹配的字符串:
0x[\dA-Fa-f]+|(?:(?:0|[1-9]\d*)(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?
分析:
0x[\dA-Fa-f]+ 表示16进制整数
“+” 代表重复1次或更多次
“*” 表示重复0次或更多次
“?” 表示重复0次或1次
“?:” 放在分组的开头,表示此分组不捕获匹配的文本,也不给此分组分配编号
“|” 的优先级低, ()里面是一个分组整体
此正则表达式表示的是:16进制整数 或者 10进制整数、小数
可以使用在线的正则表达式分析网站进行图解分析http://www.regexper.com/
四 一些补充
1 正则表达式中的”平衡组/递归匹配“概念在python的re模块中并没有实现。平衡组用于匹配成对(可嵌套)出现的字符。想了解更多的内容可以阅读http://www.deerchao.net/tutorials/regex/regex.htm#balancedgroup。
2 向后引用分组的例子’(\d)\1‘,如果要改成’\1(\d)‘是不行的。也就是不能在分组还没有确定之前就引用分组。
3 一些示例对应的代码:
import re
def OutputRet(match):
if match:
print "find:" + match.group()
print "match.start:", match.start()
print "match.end:", match.end()
else:
print "not find!!!"
#====================================== MAIN =======================================
print 'starting...'
m_str = '50 51 52 53 54 55 56 57 58 150 050 5 05 0 8'
match = re.findall(r'\b(0*5[013]|0*[0-4]?[01356789])\b', m_str)
print match
print 'end!'
print 'starting...'
m_str = '255 256 254 250 150 050 350 355 50 5 05 0 1000'
match = re.findall(r'\b(25[0-5]|2[0-4]\d|[01]?\d?\d)\b', m_str)
print match
print "end!"
print 'starting...'
m_str = 'bcd0api-ujz.;914nefg'
match = re.match(r'^(?:(.)(?!.*?\1))+$', m_str)
OutputRet( match )
print 'end!'
print 'starting...'
ma = r'^(0x[\dA-Fa-f]+|(?:(?:0|[1-9]\d*)(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)$'
m_str = '1234.'
match = re.match(ma, m_str)
OutputRet( match )
print 'end!'
print 'starting...'
ma = r'(\d)\1'
m_str = '1223248'
match = re.search(ma, m_str)
OutputRet( match )
print 'end!'
五 参考资料
1 deerchao的博客 http://deerchao.net/tutorials/regex/regex.htm(正则表达式30分钟入门教程)
2 http://www.regexper.com/ 图解js正则表达式的网站