- 本篇博客的前置知识:正则表达式高阶技巧之转义的介绍(使用python实现)
元字符的转义
- 在已经了解了字符串与正则表达式的关系后,再来学习正则表达式中的元字符的转义。元字符是在正则表达式具有特殊含义的字符,如果要匹配“元字符”自身则必须要进行转义,也就是在元字符之前添加
\
。如下表展示各种结构的转义:
结构 | 记法 | 转义 | 说明 |
字符组 | [ ] | \[ ] | 只对开括号进行转义即可 |
. | \. | ||
- | \- | [a\-b]等价于[-ab],匹配a、b、- 三个字符 | |
量词 | * + ? | \* \+ \? | |
*? +? ?? | \*\? \+\? \?\? | ||
{m,n} | \{m,n} | 只对开花括号转义 | |
括号 | (....) | \(....\) | 开、闭括号都需要进行转义 |
多选结构 | | | \| | 竖线(或)需要转义 |
括号和多选结构 | (..|..) | \(..\|..\) | 开、闭括号和都需要进行转义 |
断言 | ^ $ | \^ \$ | |
替换引用 | $num | \$或$$ | 在替换的replacement字符串转义 |
- 从上表可以看出,对称出现的元字符在转义时并不是“对称”的,比如与开方括号
[
对应的闭方括号]
,与开花括号{
对应的闭花括号}
,这两个字符是否是元字符,取决于之前是否出现了开方括号或开花括号,如果有,则作为元字符出现;否则为,仅仅作为普通字符出现。如下表解释此问题:
正则表达式 | 解释 | 可匹配字符串 |
---|---|---|
[ab] | 字符组[ab] | a或者b |
\[ab] | 字符[、字符a、字符b、字符] | [ab] |
ab] | 字符a、字符b、字符] | ab] |
- 在在字符组内部的闭括号
]
在任何情况都是需要转义的,否则类似于[]]
的正则表达式会出现二义性,造成解读错误,所有能匹配字符a、字符b、字符]的字符组应该写成[ab\]]
,而不是[ab]]。如上,括号内部的任何闭括号)
都要转义,比如包含ab
和c)
的多选结构的正则表达式就应该写成(ab|c\))
,而不是(ab|c))
彻底消除元字符的特殊含义
- 在某些情况是需要消除所有元字符的特殊含义,使其全部作为普通字符。这种情况经常发生在需要处理用户输入的场合:用户输入某个字符串,需要根据这个字符串进行正则查找
- 假设某个文件中包含多个文本片断,用空格隔开,现在需要查找包含用户输入内容的片段:用户输入cat,查找包含cat的行,直接思路为
^.*cat.*$
(使用多行模式,同时总不能指定为单行模式),假如用户输入的内容保存在变量userInput中,就应该使用"^.*"+userInput+".*$"
得到需要查找用的正则表达式 - 如上,用户输入cat,这样做肯定时没问题。如果用户输入
ca*t
,得到的正则表达式为^.*ca*t.*$
,本意是查找包含字符串catt的行,但是在正则表达式为元字符,正则表达式ca*t
可以匹配cat、caaat、cabct
,但就是不能匹配ca*t
,就是发生错误 - 更为麻烦的是:如果碰到恶意用户可能会输入
a(b*b*)b*
之类的恶意字符串,这样的正则表达式匹配起来会消耗服务器大量资源,这也就是所说的表达式拒绝服务攻击,严重时可能会导致服务器瘫痪 - 为了解决上述的问题,一些语言中的正则表达式提供了特殊的结构,彻底来消除元字符的特殊含义,提供真正的“安全”的表达式(也就是普通的字符串),在python中可以使用escape方法,如下举例:
import re
re.search(re.escape("ca*t"),'cat') is not None
re.search("ca*t",'cat') is not None
- 我们可以查看escape方法的返回值如下:
- 我们在以后获取用户输入的值时,需要仔细辨别并消除用户输入字符串中元字符的特殊含义,是不可忽略的步骤
字符组的转义
在正则表达式中,如果需要使用表示作为元字符的普通字符串(如*、?等等),就是需要转义的,特殊的是,常见的元字符出现在字符组内部基本上都不算元字符,也就是说,他们在字符组内部出现时,不需要转义,如下测试:
import re
re.search(r"[*]",'*') is not None
re.search(r"[?]",'?') is not None
re.search(r"[(]",'(') is not None
在之前也有提到过,字符组有自己的元字符规定,也有相应的转义规定:在字符组内部,只有三个字符需要转义
- 一个是闭方括号
]
,如果不是作为字符组结束标志的闭方括号,则必须写成\]
,如[0\]9]
,它可以匹配的字符是0、]、9
- 一个是横线
-
,如果不是用于范围表示法(比如[0-9]
),则必须写成\-
,如[0\-9]
,它可以匹配的字符是0、-、9
,如果它紧跟在开方括号之后,也可以不需要使用转义,[0\-9]
和[-09]
是等价的,在这里更推荐使用后一种表示形式,更加简洁清晰 - 还有一个需要转义的字符是
^
,如果它不是用于排除型字符组([^ab]
),则必须写成\^
,如[\^ab]
,它可以匹配除^、a、b
之外的任何字符,如果他不是紧跟在方括号之后,也不用转义。[\^ab]、[a^b]、[ab^]
这三个是完全等价的,在这里更推荐使用后两种表示形式,更加简洁清晰
如下举例:
import re
# 未转义的]
re.search(r"[0]9]", ']') is not None
# 转义的]
re.search(r"[0\]9]", ']') is not None
# 未转义的-
re.search(r"[0-9]", '-') is not None
# 转义的-
re.search(r"[0\-9]", '-') is not None
re.search(r"[-09]", '-') is not None
# 未转义的^
re.search(r"[^ab]",'^') is not None
# 转义的^
re.search(r"[\^ab]",'^') is not None
re.search(r"[a^b]",'^') is not None
re.search(r"[ab^]",'^') is not None
正则表达式的处理形式
在之前的学习中我们主要使用的是python语言,其具体的办法是调用re这个包(package)中的方法(函数),比如re.search()、re.findall()、re.sub()
。选择合适的函数,将正则表达式和字符串传入,这是一种常见的方法
函数式处理
- 在函数式处理中,正则表达式的常见操作(查找、替换等)都有对应的函数,执行这些操作时,调用对应的函数,将正则表达式和字符串作为参数传入即可,Python、PHP是函数式处理的典型代表,如下:
import re
# 查找
re.findall(r"\d+", '12 45 66 wy')
# 替换
re.sub(r"\d+", r'[\g<0>]', '12 45 66 wy')
面向对象式处理
- Java与.NET之类的语言的正则表达式采取不同的处理方式:在进行正则表达式处理之前,必须生成专门的正则表达式对象(在不同的语音里,对象所属的类名不同),再调用此对象的成员函数
比较
- 同样是进行查找操作,如果我们使用函数式处理,只需要调用对应的函数:但使用面向对象式处理,需要逐步生成各种对象,再调用对象自身的方法。看起来,使用后者是要麻烦很多的,采用这种处理方式有什么好处呢?
- 之前在正则转义的介绍中讲过,正则表达式并不等于字符串,即便是正则表达式是以字符串的形式给出的,再进行正则表达式操作之前,必须首先生成专用的“正则表达式对象”,函数式处理隐去了生成过程,感觉更加直接;面向对象式处理则暴露了生成的过程,感觉更加细致
- 只要执行正则表达式操作,就会产生正则表达式对象;所以,面向对象式处理的代码虽然较为繁琐,但是如果在后续的的处理中如果需要重复使用某些正则表达式,它的效率往往是比函数式处理更高的,在面向对象式的处理中,可以将已经生成的对象作为变量保存起来,就不必重复生成了
- 面向对象式处理的步骤分明、多次使用的效率更高,而函数式处理的好处是方便顺手、代码简洁;最好的办法是根据应用的场合不同,采取不同的处理方式;如果正则表达式只是单次使用,则使用函数式处理即可;如果正则表达式需要重复使用,则选择面向对象式处理。实际上,许多的编程语言也提供了两种不同的设计,比如在Python中也提供了一些面向对象式处理的方法,Java与.NET也提供了一些函数式处理
- 在Python中进行面向对象式处理,可以先调用
re.compile()
生成专门的RegexObject对象(也就是我们所说的正则表达式对象),在进行正则表达式操作时,可以把这个对象作为参数,传递给相应的函数,另一方面,re中的各个函数也可作为对象自身的成员方法,我们也可以使用已经生成好的RegexObject对象调用这些方法,如下测试:
import re
# 函数式
re.findall(r"\d+", '12 66 wy')
# 面向对象式
# 1.生成re对象
digitRegex = re.compile(r"\d+")
# 1.1调用re.findall方法
re.findall(digitRegex, '12 66 wy')
# 1.2直接调用re对象的方法
digitRegex.findall('12 66 wy')