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)

环视

  1. 顺序肯定环视 (?=exp)

零宽度正预测先行断言,又称:顺序肯定环视,断言自身位置的后面能够匹配表达式 exp。

例如:

// 匹配 I'm singing while you're dancing. 句子中的 singing、dancing 单词
\b\w+(?=ing\b)
  1. 逆序肯定环视 (?<=exp)

零宽度正回顾后发断言,又称:逆序肯定环视,断言自身位置的前面能够匹配表达式 exp。

例如:

// 匹配 reading a book. 句子中的 reading 单词
(?<=\bre)\w+\b
  1. 顺序否定环视 (?!exp)

零宽度负预测先行断言,又称:顺序否定环视,断言自身位置的后面不能匹配表达式 exp。

例如:

// 匹配 3 位数字,并且 3 位数字的后面不能是数字。例如:匹配 333g 字符串中的 333 数字。
\d{3}(?!\d)

// 匹配不包含连续字符串 abc 的单词。例如:匹配 "I can say abcdefg." 字符串中的 I、can、say 单词。
\b((?!abc)\w)+\b
  1. 逆序否定环视 (?<!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;
}

// 匹配成功:该字符串全是中文

思维导图

mind-regular-expression-basic-notes.png

参考资料

添加评论

验证码: