RegExp 正则表达式基础笔记
正则表达式(英语:Regular Expression,在代码中常简写为:regex、regexp 或 RE)描述某种规则的表达式。在开发中,常用于查找匹配指定规则的字符串。最近在翻阅《PHP核心技术与最佳实践》时,当中有涉及到正则表达式相关的章节,这里主要根据文中的内容做一下归纳笔记。
正则表达式的定义
正则表达式:用某种模式去匹配一类字符串的一种公式。通俗地讲,就是用一个“字符串”描述一个特征,然后验证另外一个“字符串”是否符合这个特征的公式。
现代编程语言基本上都已经从语法上支持正则表达式,只不过,每种语言对正则表达式的支持有所不同。其中 Perl、.NET 对正则表达式的支持最为强大,而 JavaScript 对正则表达式的支持较为朴素。
造成不同语言间的支持度不同的最主要原因是:有多种引擎(如:非确定有限状态自动机(NFA)、确定有限状态自动机(DFA))可以实现正则表达式。所以,在 Perl、.NET 中可以运行的正则表达式不一定能在 JavaScript 中运行,就是这样原因。
有关状态机的其他资料:谈谈状态机。
PHP 中的正则函数
PHP 中有两套正则函数,功能差不多:
- 以
preg_为前缀命名,由 PCRE 库提供的函数。
PCRE(Perl Compatible Regular Expression,兼容 Perl 的正则表达式)由 Philip Hazel 于 1997 年开发。现代编程语言和软件中一般都使用 PCRE 库。
- 以
ereg_为前缀命名,由 POSIX 扩展提供的函数。
POSIX(Portable Operating System Interface of UNIX,UNIX 可移植操作系统接口)风格的正则表达式,定义了 BRE(Basic Regular Expression,基本型正则表达式)和 ERE(Extended Regular Express,扩展型正则表达式)两大流派。通常,在 UNIX 的一些工具和较老的软件中会使用 POSIX 风格的正则。
自 PHP 5.3 以后,不再推荐使用 POSIX 正则函数库。
测试工具的使用
可以选择 POSIX、PCRE 两种风格,可生成 PHP 正则表达式使用代码示例。
正则表达式的组成
在 PHP 中,一个正则表达式分为三个部分:分隔符、表达式、修饰符。
- 分隔符:除字母、数字、反斜线、空白字符外的任意字符(如:/、!、#、%、~ 等)。考虑到可读性,避免与反斜线 混淆,一般不推荐使用正斜线 / 做分隔符。
- 表达式:由一些特殊字符和非特殊字符组成(如:[a-z0-9_-]+@[a-z0-9_-.]+,匹配一个简单电子邮件字符串)。
- 修饰符:用于开启或关闭某种功能/模式。
元字符
元字符(Meta-Characters):正则表达式中具有特殊意义的专用字符,(部分元字符)用于规定其前导字符(即:位于元字符前面的字符)在目标字符串中的出现模式。
正则表达式中有很多元字符,其中常用元字符,如下:
| 元字符 | 描述 |
|---|---|
. | 匹配除换行符外的任意字符 |
\w | 匹配字母、数字、下划线、汉字 |
\s | 匹配任意空白符(Tab、Space 按键) |
\d | 匹配数字 |
\b | 匹配单词的开始、结束位置 |
^ | 匹配字符串的开始位置 |
$ | 匹配字符串的结束为止 |
- | 表示范围,如:[a-z],字母 a 到 z 范围的所有字母 |
[] | 匹配括号中的任意一个字符(字符组中的元字符转义规则有所不同) |
*、+、? | 量词限定符 |
量词限定符
量词限定符:用于规定其前导字符重复的次数。
正则表达式中的量词限定符,如下:
| 限定符代码/语法 | 描述 |
|---|---|
* | 重复 0 次以上 |
+ | 重复 1 次以上 |
? | 重复 0 次或 1 次 |
{n} | 重复 n 次 |
{n,} | 重复 n 次以上 |
{n,m} | 重复 n 到 m 次 |
正则表达式的匹配规则
字符组
匹配没有预定义元字符的字符集(如:元音字母 a、e、i、o、u),只需在方括号里列出它们,如下:
// 匹配任何一个英文元音字母
[aeiou]
// 匹配任意一个标点符号(.、?、!)
[.?!]
// 匹配 cat、cot、cut 三个单词,而不是 coast 单词
c[aou]t转义
由于元字符具有特殊意义,如果想匹配元字符本身,那么应该使用反斜线 \ 来取消元字符的特殊意义,这就叫做转义。
在 PHP 中,可以使用反斜线 \ 转义,也可以使用 \Q 和 \E 忽略正则表达式中元字符的特殊意义,如下:
// 匹配 123.$. 字符串
\d+\.\$\.$
// 匹配 123.$. 字符串,同上正则表达式等价
\d+\Q.$.\E$同时,由于字符组里匹配的是单个字符,所以量词元字符都不需要再转义,即使再使用反斜线 \ 转义也是多余的,如下:
// 匹配 cat、cot、cut、c?t、c*t、c)t 字符串
c[aou\?\*\)]t
// 匹配 cat、cot、cut、c?t、c*t、c)t 字符串,同上正则表达式等价
$pattern = "#c[aou?*)]t#";当然,字符组也是可以正常匹配元字符,如下:
// 匹配 c1d、c2d 等字符串
c[\d]d 如果在字符组中想要匹配反斜线 \ 本身,可以使用转义,如下:
// 匹配反斜线 \ 本身
[\\]对于字符组来讲,只要反斜线没有被转义,那么元字符还是具有特殊意义的。
反义
有时候,查找的字符不属于某个字符集、或表达式与已知定义相反(如:匹配除数字外的任意一个字符),这时需要用到转义。
常用反义如下:
| 常用反义 | 描述 |
|---|---|
\W | 匹配非字母、数字、下划线、汉字的任意字符 |
\S | 匹配非空白符的任意字符 |
\D | 匹配非数字的任意字符 |
\B | 匹配非单词开头或结尾的位置 |
[^x] | 匹配除 x 外的任意字符 |
[^aeiou] | 匹配除 aeiou 外的任意字符 |
反义有一个比较明显的特征,就是和一些已知元字符相反,并且为大写形式。同时,注意使用反义,因为反义扩大了匹配范围。
分支
分支:就是存在多种可能的匹配情况。
分支与字符组的区别:
- 字符组:只能匹配一个字符。
- 分支:可以匹配多个字符。
例如:
// 字符组:只能匹配 cat、hat 单词
[ch]at
// 分支:可以匹配 `cat`、`hat`、`toat` 单词
(c|h|to)at其中括号里的表达式将视作一个整体,字符 | 表示分支,即可能存在多种情况,可以匹配多个字符串。
注意!匹配分支条件时,将从左到右测试每个条件,如果满足某个分支条件,则不再考虑剩余条件。
分组
重复单个字符只需在目标字符后面加上量词元字符。如果需要重复多个字符的话,则需要使用小括号指定子表达式,然后规定这个子表达式的重复次数,也可以对这个子表达式进行其他操作。这就是分组。
常用分组语法,如下:
捕获
| 代码/语法 | 描述 |
|---|---|
(exp) | 匹配 exp,并捕获文本到自动命名的组里 |
(?<name>exp) | 匹配 exp,并捕获文本到名称为 name 的组里,也可以写成(?'name'exp) |
(?:exp) | 匹配 exp,不捕获匹配的文本,也不给此分组分配组号 |
组号分配的过程是要从左向右扫描两遍:第一遍只给未命名组分配,第二遍只给命名组分配。因此,所有命名组的组好都大于未命名的组号。可以使用语法 (?:exp) 剥夺一个分组对组号分配的参与权。
零宽断言
| 代码/语法 | 描述 |
|---|---|
(?=exp) | 匹配 exp 前面的位置 |
(?<=exp) | 匹配 exp 后面的位置 |
(?!exp) | 匹配后面不是 exp 的位置 |
(?<!exp) | 匹配签名不是 exp 的位置 |
注释
| 代码/语法 | 描述 |
|---|---|
(?#comment) | 提供注释辅助阅读,不对正则表达式的处理产生任何影响 |
反向引用
反向引用用于重复搜索前面某个分组匹配的文本。例如:
// 匹配 "go go"、"kitty kitty" 字符串
\b(\w+)\b\s+\1\b其中,1 代表分组 1 捕获的内容。
也可以给分组命名,例如:
// 匹配 "go go" 字符串,其中 Word 为分组 1 的命名(分组 1 仍然有效)
(?P<Word>\w+)\s(?P=Word)环视
- 顺序肯定环视 (?=exp)
零宽度正预测先行断言,又称:顺序肯定环视,断言自身位置的后面能够匹配表达式 exp。
例如:
// 匹配 I'm singing while you're dancing. 句子中的 singing、dancing 单词
\b\w+(?=ing\b)- 逆序肯定环视 (?<=exp)
零宽度正回顾后发断言,又称:逆序肯定环视,断言自身位置的前面能够匹配表达式 exp。
例如:
// 匹配 reading a book. 句子中的 reading 单词
(?<=\bre)\w+\b- 顺序否定环视 (?!exp)
零宽度负预测先行断言,又称:顺序否定环视,断言自身位置的后面不能匹配表达式 exp。
例如:
// 匹配 3 位数字,并且 3 位数字的后面不能是数字。例如:匹配 333g 字符串中的 333 数字。
\d{3}(?!\d)
// 匹配不包含连续字符串 abc 的单词。例如:匹配 "I can say abcdefg." 字符串中的 I、can、say 单词。
\b((?!abc)\w)+\b- 逆序否定环视 (?<!exp)
零宽度负回顾后发断言,又称:逆序否定环视,断言自身位置的签名不能匹配表达式 exp。
例如:
// 匹配前面不是小写字母的 7 位数字
(?<![a-z])\d{7}
// 匹配不包含属性的简单 HTML 标签内的内容
(?<=<(\w+)>).*(?=<\/\1>)环视相当于对所在位置附加一个条件,难点就在于找到这个位置。这点解决了,环视就没什么难点了。
贪婪/懒惰匹配模式
贪婪模式:当正则表达式中包含量词元字符时,通常是(在表达式能够得到匹配的前提下)匹配尽可能多的字符。例如:使用正则表达式 a.*b 搜索 "aabab" 字符串时,它会匹配整个字符串 aabab。
懒惰模式:匹配尽可能少的字符。
例如:
// 匹配以 a 开始、以 b 结束的最短字符串
a.*?b把上述表达式应用于 aabab 时,最先会匹配到 aab,然后是 ab 这两组字符串。尽管都是匹配尽可能少的字符,只不过,比懒惰/贪婪规则的优先级更高的是:最先开始的匹配拥有最高优先权,所以是优先匹配到 aab 然后再匹配到 ab 字符串。
常用懒惰限定符如下:
| 代码/语法 | 描述 |
|---|---|
*? | 重复任意次,但尽可能少重复 |
+? | 重复 1 次以上,但尽可能少重复 |
?? | 重复 0 次以上,但尽可能少重复 |
{n,m}? | 重复 n 到 m 次,但尽可能少重复 |
{n,}? | 重复 n 次以上,但尽可能少重复 |
懒惰匹配的原理:在匹配和不匹配都可以的情况下,优先不匹配,记录备选状态,并将匹配控制交给正则表达式的下一个匹配字符。当后面的匹配失败时,回溯,进行匹配。
关于回溯及正则表达式效率相关内容,可以查看《精通正则表达式》。
在一定情况下,使用懒惰模式可以减少回溯,提高效率。
正则表达式的运算
运算符优先级
正则表达式从左到右进行计算,并遵循优先级顺序,类似于算术表达式。
以下为各种正则表达式运算符的优先级顺序,其中优先级从上到下、从高到低。
| 运算符 | 描述 |
|---|---|
\ | 转义 |
(), (?:), (?=), [] | 括号和中括号 |
*, +, ?, {n}, {n,}, {n,m} | 量词元字符 |
^, $, \anymetacharacter, anycharacter | 定位点、元字符、任意字符 |
丨 | 分支 |
常用模式
- 忽略大小写模式(i)
在忽略大小写模式(Case Insensitive)下,正则匹配将忽略目标输入的大小写,例如:
$ptn = '#<div>gg<\/div>#i';
$str = '<div>gG</Div>';
if (preg_match($ptn, $str, $arr)) {
echo '匹配成功:', $arr[0], PHP_EOL;
}
// 匹配成功:<div>gG</Div>注意!!!忽略大小写模式是针对整个表达式,如果只想修饰部分表达式的话,可以使用 PCRE 的内部选项 - 局部修饰符。如下:
$ptn = '#ab(?i)c#';
$strs = array('abC', 'abc', 'aBc', 'Abc');
foreach ($strs as & $str) {
if (preg_match($ptn, $str, $arr)) {
echo '匹配成功:', $arr[0], PHP_EOL;
} else {
echo '匹配失败:', $str, PHP_EOL;
}
}
// 匹配成功:abC
// 匹配成功:abc
// 匹配失败:aBc
// 匹配失败:Abc其中,(?i) 只对它后面的字符 c 起作用,
- 多行模式(m)
正则表达式的开始符(^)和结束符($)默认是针对整个输入内容匹配。而在多行模式(Multiline)下,开始符(^)和结束符($)将会针对输入内容的每一行匹配。
例如:
$ptn = '#.*reg$#mi';
$str = "this is reg
Reg
this is
regexp turtor, or reg";
if (preg_match_all($ptn, $str, $arr)) {
echo '匹配成功:', PHP_EOL;
var_dump($arr);
}
/*
匹配成功:
array(1) {
[0]=>
array(3) {
[0]=>
string(11) "this is reg"
[1]=>
string(3) "Reg"
[2]=>
string(21) "regexp turtor, or reg"
}
}
*/当没有多行模式 m 时,结束符 $ 将只匹配最后一行的 "reg" 字符串。如下:
$ptn = '#.*reg$#i';
$str = "this is reg
Reg
this is
regexp turtor, or reg";
if (preg_match_all($ptn, $str, $arr)) {
echo '匹配成功:', PHP_EOL;
var_dump($arr);
}
/*
匹配成功:
array(1) {
[0]=>
array(1) {
[0]=>
string(21) "regexp turtor, or reg"
}
}
*/使用多行模式时,需要注意目标内容中的换行符是否有效。比如:"n" 与 'n' 字符串的转义问题。
- 点号通配模式(s)
点号通配模式的作用是:使正则表达式里的点号 . 可以匹配换行符。
例如,没有点号通配模式下,只匹配非跨行内容:
$ptn = '#this.*?reg#i';
$str = 'this is reg
Reg
this is
regexp turtor, oh reg';
if (preg_match_all($ptn, $str, $arr)) {
echo '匹配成功:', PHP_EOL;
var_dump($arr);
}
/*
匹配成功:
array(1) {
[0]=>
array(1) {
[0]=>
string(11) "this is reg"
}
}
*/点好通配模式下,可以匹配跨行内容:
$ptn = '#this.*?reg#is';
$str = 'this is reg
Reg
this is
regexp turtor, oh reg';
if (preg_match_all($ptn, $str, $arr)) {
echo '匹配成功:', PHP_EOL;
var_dump($arr);
}
/*
匹配成功:
array(1) {
[0]=>
array(2) {
[0]=>
string(11) "this is reg"
[1]=>
string(11) "this is
reg"
}
}
*/点号通配模式一般适用于抓取一些文档时,由于存在不可见换行符,如果直接使用 . 就可能存在问题。- 懒惰模式(U)
懒惰模式相当于 ? 懒惰匹配,如下:
$ptn1 = '#\[url\](.*)\[url\]#U';
$ptn2 = '#\[url\](.*?)\[url\]#'; // 两个正则表达式等价- 结尾限制(D)
使用结尾限制,则不允许结尾有换行。如下:
$ptn = '#abc$#D';
$str = "abc\n";
if (preg_match($ptn, $str, $arr)) {
echo '匹配成功:', $arr[0], PHP_EOL;
} else {
echo '匹配失败:', $str, PHP_EOL;
}
/*
匹配失败:abc
*/如果没有结尾限制的话,是可以忽略结尾的 \n 换行符。如下:
$ptn = '#abc$#';
$str = "abc\n";
if (preg_match($ptn, $str, $arr)) {
echo '匹配成功:', $arr[0], PHP_EOL;
} else {
echo '匹配失败:', $str, PHP_EOL;
}
/*
匹配成功:abc
*/- 支持 UTF-8 转义表达(u)
u 修饰符启用 PCRE 中与 Perl 不兼容的额外功能,模式字符串被当成 UTF-8。自 PHP 4.3.5 起开始检查模式的 UTF-8 合法性,如下:
$ptn = '#^[\x{4e00}-\x{9fa5}]+$#u';
$str = "中文字符串";
if (preg_match($ptn, $str)) {
echo '匹配成功:该字符串全是中文', PHP_EOL;
} else {
echo '匹配失败:该字符串不全是中文', PHP_EOL;
}
// 匹配成功:该字符串全是中文