正则表达式概念源于 《神经网事件的表示法》论文中。
正则表达式就是用某种模式去匹配一类字符串的一种公式。
正则表达式的实现由多种引擎:
- 非确定性有穷自动机 NFA
- 确定性有穷自动机 DFA
- ...
PHP中有两套正则函数:
- PCRE库提供的函数, 以
preg_
为前缀来命名。 - POSIX扩展提供的函数, 以
ereg_
为前缀命名。
PHP5.3 之后, 就不推荐使用POSIX正则函数库, 如程序中使用了会报 Deprecated
级别的错误。其实使用或不适用 POSIX
正则函数库二者本质上没有太大差别, 主要是一些表现形式、语法和扩展功能的差别。
php中, 一个正则表达式分为三个部分: 分隔符、表达式和修饰符:
- 分隔符: 可以是除了字母、数字、反斜线及空白字符外的任何字符(比如/、!、#、%、|、~等)。经常使用的分隔符是正斜线(/)、hash符号(#)以及取反符号(-)。考虑到可读性, 为了避免反斜线混淆, 一般不适用正斜线做分隔符。
- 表达式: 由一些特殊字符和非特殊的字符串组成, 比如
[a-z0-9_-] + @[a-z0-9_-.]+
可以匹配一个简单的电子邮件字符串。 - 修饰符: 用于开启或者关闭某种功能/模式。
测试工具
- RegexTester
- Firefox 扩展 Regular Expression Tester
元字符
元字符(Meta-Characters)
是正则表达式中具有特殊意义的专用字符, 用来规定其前导字符(即位于字符串前面的字符)在目标对象中出现的模式。
元字符 | 描述 |
---|---|
. | 匹配除换行符以外的任意字符 |
\w | 匹配字母或数字或下划线或汉子 |
\s | 匹配任意空白符 |
\d | 匹配数字 |
\b | 匹配单词的开始或结束 |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结束 |
- | 表示范围 |
[] | 匹配括号中的任意一个字符 |
*、+、? | 量词 |
例子:
- 匹配以字母"a"开头的单词:
\ba\w*\b
- 第一个\b匹配单词的开始
- a表示单词由a开头
- \w* 任意数量的字母或数字
- \b 单词的结束
以上正则表达式可以匹配到: adandon
、action
、a
- 匹配一个或多个连续的数字
\d+
: 能匹配到0
、1
、555
。+
和 *
的区别在于, *
是重复0到任意次, +
则匹配至少一次或更多次。
- 匹配刚好6个字符的单词:
b\w{6}\b
: 能匹配到action
、123456
、ste_ph
正则表达式中, 单词指不少于1个的连续字母或数字
^
、$, 匹配字符串的开头和结束, 类似于 \b
但是如果不是完整字符串匹配成功, 而是字符串的部分匹配成功, 整个表达式也是无法匹配成功的。
正则表达式中的量词
限定符代码/语法 | 描述 |
---|---|
* | 重复0次或更多次 |
+ | 重复1次或更多次 |
? | 重复0次或1次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n,m} | 重复n 到 m 次 |
需要注意 * 和 ? , 通配符里面也有这两个符号, 要注意它们之间的区别。
如果要匹配没有预定义元字符的字符集合, 只需要在方括号中列出它们:
[aeiou]
: 匹配英语中的元音字符。
转义字符
如果要匹配元字符本身, 如果直接使用元字符, 会被解释为元字符, 这个时候就需要使用\
来转义元字符。
使用\Q
和\E
也可以忽略正则表达式元字符.
\d+\Q.$.\E$
:
- 先匹配一个或多个数字
- 紧接着匹配一个
.
, 然后再匹配一个$
, 再然后一个.
, 最后是字符串末尾
通过上面的案例可以看出, \Q 和 \E中的内容会被当作普通字符来匹配, 而不会被解释为元字符。
反义
有时候, 查找的字符不属于某个字符类, 或者表达式和以知相反, 这时需要用到反义。
常用反义
常用反义 | 描述 |
---|---|
\W | 匹配任意不是字母、数字、下划线、汉子的字符 |
\S | 匹配任意不是空白符的字符 |
\D | 匹配任意非数字的字符 |
\B | 匹配不是单词开头或结束的位置 |
[^x] | 匹配除了x以外的任意字符 |
[^aeiou] | 匹配除了aeiou这几个字母以外的任意字符 |
^ 表示开头, 但是如果当^ 存在字符组[] 中时, 就代表非。
分支
分支就是存在多种可能的分配情况。
如果要匹配cat 或者 hat, 可以写成[ch]at
, 但是如果要匹配cat、hat、fat、toat, 就不能用字符组来匹配了, 因为字符组只支持单个字符, 这个时候可用分支形式:(c|h|f|to) at
。
单字符的情况, 字符组效率会更改。所以能用字符组, 就不使用分支。
使用分支时, 千万要注意各个条件的顺序。
\d{5} | \d{5}-\d{4}
: 这个正则表达式就只能匹配5位数的邮编和9位数邮编的前5为。将两个分支反过来才是正常的
分组
重复单个字符只需要在字符后面加上限定符, 但是想重复多个字符该怎么办? 可以用小括号指定子表达式, 然后规定这个子表达式的重复次数。
常用分组语法
类别 | 代码/语法 | 描述 |
---|---|---|
捕获 | (exp) | 匹配exp, 并捕获文本到自动命名的组里 |
. | (?<name>exp) | 匹配exp, 并捕获文本到名称为name的组里, 也可以写成(?'name'exp) |
. | (?:exp) | 匹配exp, 不捕获匹配的文本, 也不给此分组分配组号 |
零宽断言 | (? = exp) | 匹配exp前面的位置 |
. | (?<=exp) | 匹配exp后面的位置 |
. | (?!exp) | 匹配后面跟的不是exp的位置 |
. | (?<!exp) | 匹配前面不是exp的位置 |
注释 | (?#comment) | 提供注释辅助阅读, 不对正则表达式的处理产生任何影响 |
简单的IP地址匹配表达式:
(\d{1,3}\.){3}\d{1,3}
由于IP地址中每个数字不能超过255, 所以上面的正则其实是有问题的。如果能用算数比较, 或许能简单的解决这个问题, 但是正则表达式没有提供任何关于数学的任何功能, 所以只能使用冗长的分组, 选择, 字符类来表述一个正确的ip地址:
((2[0-4]\d|25[0-5]|[01]? \d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]? \d\d?)
默认每个分组会自动拥有一个组号, 规则是: 从左向右, 以分组的左括号为标志, 第一个出现的分组, 其组号为1, 第二个为2, 以此类推; 分组0对应整个表达式。
也可以自己指定子表达式祖名:
?<Word> \w+
可以把尖括号换成单引号:
?'Word' \w+
反向引用
反向引用用于重复搜索前面某个分组匹配的文本。
\b(\w+)\b\s+\1\b
\1 代表匹配分组1匹配的文本
也可以写成:
\b(?<Word>\w+)\b\s + \k<Word>\b
用\k<Word>
来匹配自定义分组名
环视
断言用来声明一个应该为真的事实。正则中, 只有当断言为真时才会继续进行匹配。
断言匹配的是一个事实, 而不是内容。
1. 顺序肯定环视(? = exp)
零宽度正预测先行断言, 又称顺序肯定环视, 断言自身出现位置的后面能匹配表达式exp。
\b\w+ (?=ing\b)
: 匹配以ing结尾单词前面部分
用上面正则查找下面的句子, 会匹配sing 和 danc
I'm singing while you're dancing.
2. 逆序肯定环视(? <= exp)
零宽度正回顾后发断言, 又称逆序肯定环视, 断言自身出现位置的前面能匹配表达式exp。
(?<=\bre)\w + \b
: 匹配以re开头的后半部分
用上面正则查找下面的句子, 会匹配 "ading"
reading a book
3. 顺序否定环视(?!exp)
零宽度负预测先行断言, 又称顺序否定环视, 断言此位置后面不能匹配表达式exp。
- 匹配三位数字, 而且这3为数字后面不能是数字:
\d{3}(?!\d)
- 匹配不能包含联系字符串abc的单词:
\b((?!abc)\w) + \b
4. 逆序否定环视(?<!exp)
零宽度负回顾后发断言, 又称逆序否定环视, 可以用(?<!exp)断言此位置的前面不能匹配表达式exp。
(?<![a-z])\d{7}
: 匹配前面不是小写字母的7为数字
(?<=<(\w+)>).*(?=<\/\1>)
: 匹配不包含属性的简单HTML标签内的内容
贪婪/懒惰匹配模式
当正则表达式中包含内接受重复的限定符时, 通常的行为是(在使整个表达式能够得到匹配的前提下)匹配尽可能多的字符。
a.*b
用来匹配以a开始, 以b结束最长字符串, 如果用来搜索aabab, 它会匹配整个字符串。
如果需要匹配尽可能少的字符, 也就是懒惰匹配。
a.*?b
将前面的贪婪匹配转化为懒惰匹配模式, 只要在后面加上一个问号。
正则另外一条规则: 最先开始的匹配拥有最高优先权。
懒惰限定符
懒惰限定符代码/语法 | 描述 |
---|---|
*? | 重复任意次, 但尽可能少重复 |
+? | 重复1次或更多次, 但尽可能少重复 |
?? | 重复0次或1次, 但尽可能少重复 |
{n,m}? | 重复n到m次, 但尽可能少重复 |
{n,}? | 重复n次以上, 但尽可能少重复 |
懒惰匹配模式的原则是, 在匹配和不匹配都可以的情况下, 优先不匹配, 记录备选状态, 并将匹配控制交给正则表达式的下一个匹配符。当后面的匹配失败时, 回溯, 进行匹配。
在一定情况下, 使用懒惰模式可以减少回溯, 提高效率。
构造正则表达式
在理解正则过程中, 通常是由简到繁的过程, 理解正则内部间的关系, 就可以把复杂的正则拆分成多个小块来理解
1) 正则表达式的逻辑关系
正则表达式之间的关系可以简单用 与、或、非 来表述。
逻辑关系 | 描述 |
---|---|
与 | 在某个位置, 某些元素(字符、字符组或子表达式) 必须出现 |
或 | 在某个位置, 某个元素或许会出现, 或许不出现; 或长度和出现次数不固定, 或者是某几个元素中的一个 |
非 | 在某个位置, 某些元素不出现 |
2) 运算符优先级
正则表达式从左到右进行计算, 并遵循优先级顺序, 这与算术表达式非常类似。下面表列出了正则运算符的优先级顺序, 优先级从上到下, 由高到低排列。
运算符 | 描述 |
---|---|
\ | 转义符 |
(), (?:), (?=), [] | 括号和中括号 |
*, +, ?, {n}, {n,}, {n, m} | 限定符 |
^, $, \anymetacharacter, anycharacter | 定位点和序列 |
替换 |
3) 正则表达式的常用模式
模式(Pattern Modifiers
) 就是可以改变表达式行为的字符, 用来关闭或打开某些特殊功能, 习惯上又称正则修饰符。
- 忽略大小写模式 (i)
if (preg_match('% <div>gg<\/div>%i', "<div>gG</Div>", $arr)) {
echo "匹配成功:" . $arr[0];
} else {
echo "匹配失败";
}
忽略大小写是正对整个表达式而言, 而不仅仅是欲匹配的部分。如果只想修饰部分表达式, 可以使用PCRE的内部选项——"局部修饰符"
如: #ab(?i)c#
只能匹配abc 和 abC, 而不会匹配Abc;
- 多行模式(m)
正则表达式默认开始^
和结束$
只是针对正则字符串, 如果修饰符加上m, 开始和结束将会指定字符串的每一行: 即每一行的开头就是^
, 结尾就是$
。
m表示多行匹配, 而不是跨行匹配。仅当表达式中出现^、$中至少一个元字符且字符串有换行符\n时, m修饰符才起作用, 否则会被忽略。
$str = "this is reg
Reg
this is
regexp turtor, oh reg";
if(preg_match_all('%.*reg$%mi', $str, $arr)) {
echo "匹配成功";
var_dump($arr);
} else {
echo "匹配失败";
}
在预想中, 使用了m多行匹配, 应该会匹配到第一行和第四行。
匹配成功 array(1) {
[0] => array(1) {
[0] => string(20) "regexp turtor, oh reg"
}
}
但是实际上只匹配了一行。这里即使去掉m结果也是一样, 这说明$只能表示最后一行。把正则表达式改为:
%^t.*%mi
结果为:
匹配成功 array(1) {
[0] => array(1) {
[0] => string(20) "regexp turtor, oh reg"
}
}
匹配到2行, 去掉m只匹配到一行。可见即使加了m修饰符, 也不是将这个字符串都匹配, 这就是跨行和多行的区别。
使用 m 一定要注意换行符是否真的有效
- 点号通配模式(s)
作用是使正则表达式里的点号元字符可以匹配换行符, 如果灭有这个修饰符, 点号不匹配换行符。
$str = "this is reg
Reg
this is
regexp turtor, oh reg";
if(preg_match_all('%this.*?reg%i', $str, $arr)) {
echo "匹配成功";
var_dump($arr);
} else {
echo "匹配不成功";
}
如果正则改为$this.*?reg%is
, 那么匹配结果就不同了
匹配成功 array(1){
[0] => array(2) {
[0] => string(11) "this is reg"
[1] => string(13) "this is reg"
}
}
- 懒惰模式(U)
相当于前面提到的"?", 表示懒惰匹配
- 结尾限制(D)
如果使用$限制结尾字符, 则不允许结尾有换行符, 例如下面表达式匹配"abc"、"abs\n" 这样的字符, 即忽视结尾的换行:
%abc$%
如果使用D 模式, 限定其不可有换行, 必须以abc结尾:
%abc$%D
- 支持UTF-8转义表达(u)
启用PCRE中与perl不兼容的额外功能, 模式字符串被当成UTF-8。
正则表达式的效率和优化
正则表达式可以看作描述字符串匹配的算法代码, 本质上说是一种有限状态机在计算机中的表示方法。
1. 使用字符组代替分支条件。
2. 优先选择最左端的匹配结果。
3. 标准量词是匹配优先的。
4. 谨慎使用点号元字符, 尽可能不要用星号和加号这样的任意量词。
php的PCRE扩展提供了两个设置项:
- pcre.backtrack_limit //最大回溯数
- pcre.recursion_limit //最大嵌套数
默认backtarck_limit 是10万, recursion_limit限制最大正则嵌套层数。在正则表达式的使用中, 应尽量避免回溯次数过多的情况。
5. 能用懒惰匹配就坚决不使用贪婪匹配
6. 尽量使用字符串函数处理代替
7. 合理使用括号
每使用一个普通括号, 而不是非捕获行括号(?:...), 就会保留一部分内存等着再次访问。这样的正则表达式、无限次的运行次数, 无异于一根根堆积的稻草, 迟早会将骆驼压死。
8. 起始、行描点优化
能确定起始位置, 使用^能提高匹配的速度。使用$标记结尾, 正则引擎则会从符合条件的长度开始匹配, 略过目标字符串许多可能的字符。