正则表达式由两种基本字符类型组成:原义(正常)文本字符和元字符。
元字符使正则表达式具有处理能力。元字符既可以是放在[]
中的单个字符([a]
表示匹配单个小写字符a
),也可以是字符序列([a-d]
表示匹配a、b、c、d
之中的任意一个字符,而\w
表示热议英文字母和数字以及下下划线)
字符组
字符组就是一组字符,在正则表达式中,它表示在同一个位置可能出现的各种字符。
写法:[ab]
、[314]
、[#.?]
基本用法
[...]
1 | preg_match('/[123]/', '2') ==> 1 |
范围表示法(range)
[x-y]
表示x
到y
整个范围内的字符。
如[0123456789]
表示为[0-9]
,[abcdefghijk]
表示为[a-k]
1 | preg_match('/[0-9]/', '6') ==> 1 |
字符组简记法(shorthands)
提供比范围表示法更简洁的表示方法,如\d
表示[0-9]
,\w
表示[0-9a-zA-z_]
php中支持的字符组简记:
\d
所有的数字,即[0-9]
\D
所有的非数字,与\d
互斥\w
所有的单词字符(字符、数字、下划线),即[0-9a-zA-Z_]
\W
所有的非单词字符,与\W
互斥\s
所有的空白字符,包括空格、制表符、回车符、换行符等空白字符\S
所有的非空白字符,与\s
互斥
1 | preg_match('/\d/', '8') ==> 1 |
元字符与转义
在范围表示法中,字符组中的横线-
不能匹配横线字符,而是用来表示范围,这类字符叫做元字符。
元字符除了-
还有[、]、^、$
…,他们都有特殊的意义。
当元字符想要表示普通字符的含义时(如-
就只想表示横线字符),就需要转义处理(在元字符前加反斜线\
)。
对于-
,有一个例外情况,就是当它紧跟着字符组的左方括号[
时,它就表示普通横线字符,此时不用转义。
1 | preg_match('/[0\-9]/', "-") ==> 1 |
仔细看上面第一个表达式和最后两个表示式。这里要注意:
- 在php中,字符串既可以用单引号标注,也可以用双引号标注。
两者的主要区别在于双引号字符串可以解析变量,而但引号字符串不能。
另外,双引号字符串会处理字符转义,而单引号字符串不会(唯独会转义\
) - 正则表达式是以字符串的方式提供的。在php中,双引号字符串本身也有关于转义的规定(如
"\\"
、'\r'
、"\t"
等),因此"0\\-9"
与'0\-9'
是等价的。 - 那么最后一个表达式为什么也可以匹配呢?
这是因为,尽管php的正则表达式用字符串文字给出,但它与常见的字符串不完全一样 —— 如果某个转义序列可以有字符串识别,则对其进行转义处理;否则,将整个转义序列“原封不动”地保存下来。
最后一个表达式的\-
并没有什么特殊意义,即将其原样保存
排除型字符组(Negated Character Class)
在方括号[…]
中列出希望匹配的所有字符叫做“普通字符组”。在开方括号[
之后紧跟一个脱字符^
,写作[^…]
,表示“在当前位置,匹配一个没有列出的字符”。例如,[^0-9]
匹配非数字字符。
1 | preg_match('/[^0-9][0-9]/', "A1") ==> 1 |
排除型字符组中的紧跟着开方括号[
的脱字符^
也是元字符,如果要匹配尖括号字符,需要进行转义处理。但是,不紧跟着开方括号[
的^
就是普通字符,不需要转义。
1 | preg_match('/[^0-9]/', '0') ==> 0 |
量词
首先看看^
和$
两个特殊字符
^
放在正则表达式的开头,表示“定位到字符串的起始位置”;$
用在正则表达式的末尾,表示“定位到字符串的结束位置”。
1 | preg_match('/\w\d/', '1a2b') ==> 1 匹配到的是 a2 |
量词的一般形式
如果要匹配一个邮政编码(6位数字),目前能写出来的正则表达式是^\d\d\d\d\d\d$
。\d
重复6次的写法很不科学,正则表达式肯定会有更方便的写法,也就是量词。量词的通用形式是{m,n}
(注意,,
后面不能有空格),它限定之前的元素能够出现的次数,m
是下限,n
是上限。其他常见的量词形式有:
量词 | 说明 |
---|---|
{n} |
之前的元素必须出现n次 |
{m,n} |
之前的元素最少出现m次,最多出现n次 |
{m,} |
之前的元素最少出现m次,出现次数无上限 |
{0,n} |
之前的元素可以不出现,也可以出现,最多出现n次 |
1 | preg_match('/^\d{6}$/', '100010') ==> 1 |
常用量词
正则表达式还有三个常用的量词,分别是+
、?
、*
:
常用量词 | {m,n} 等价形式 |
说明 |
---|---|---|
* | {0,} | 可能出现,也可能不出现,出现次数没有上限 |
+ | {1,} | 至少出现1次,出现次数没有上限 |
? | {0,1} | 出现0次或1次 |
点号
点号.
是与量词搭配比较多得一个字符。一般情况下,点号.
可以匹配除了换行符\n
以外的任意字符。
1 | preg_match('/^.$/', 'z') ==> 1 |
如果要使达到.
能匹配的字符包含换行符\n
的效果,可以使用自制通配符\s\S
或\w\W
、\d\D
。
1 | preg_match('/^[\s\S]$/', "\n") ==> 1 |
使.
能匹配的字符包含换行符\n
的另外一种方法是指定正则匹配时使用单行模式。
在php中,可以使用模式修饰符和预定义常量两种方法来指定单行模式。关于正则匹配的模式,后面会详细介绍。
1 | preg_match('/(?s)^.$/', "\n") ==> 1 模式修饰符 |
匹配优先量词、忽略优先量词
先看一个例子:
很多语言中,都可以使用/*...*/
来注释代码,如果是一个支持语法高亮的文本编辑器就要能够提取/*...*/
注释块。很easy的,我们可以写出如下正则表达式:
1 | $str = '/*this is a comment*/ /*this is another comment*/'; |
可以看到,这个正则表达式出了点小问题,它把两个注释块匹配出来了,如果两个注释块中间有代码,那么代码也会匹配出来。
这是因为,我们介绍的*、+、?
都是匹配优先量词(greedy quantifier,也称贪婪量词)。
匹配优先量词是指在拿不准是否要匹配的时候,优先尝试匹配。因此,$str
中间的*/ /*
都被.*
匹配了。
正则表达式中利用忽略优先量词来解决上述问题。与*、+、?
对应的忽略优先量词的形式是*?、+?、??
。
忽略优先量词在不确定是否要匹配时选择“不匹配”的状态。
还是以提取注释块的代码为例:
1 | preg_match('/\/\*.*?\*\//', $str, $arr); |
匹配优先量词 | 忽略优先量词 | 限定次数 |
---|---|---|
* | *? | 可能出现,可能不出现,出现次数没有上限 |
+ | +? | 至少出现1次,没有上限 |
? | ?? | 出现0次或1次 |
{m,n} | {m,n}? | 出现次数大于等于m,小于等于n |
{m,} | {m,}? | 至少出现m次,没有上限 |
{0,n} | {0,n}? | 出现0次-n次 |
php中有指定非贪婪匹配模式的模式修饰符
和预定义常量
,与忽略优先量词
是一样的效果:
1 | // 默认贪婪匹配 |
括号
分组
上面介绍了量词,例子中量词都只能控制它前面的字符或字符组。那么量词能否控制连续的字符或字符组呢,如控制一个单词hello
出现或者不出现。这就要用到正则表达式的分组功能(子表达式),使用圆括号(...)
实现分组(子表达式)。
1 | // 量词限定前面一个字符 |
多选结构
多选结构(alternative)的形式是(...|...)
,在括号内以竖线|
分开多个子表达式,这些子表达式也叫多选分支(option)。
例如,匹配常见的11位手机号
1 | preg_match('/(13[0-9]|15[0-356]|18[025-9])\d{8}/', '18521510001'); ==> 1 |
引用分组
正则表达式会保存每个(...)
分组匹配的文本,这种功能叫捕获分组(capturing group)。在需要直接使用子表达式的时候非常有用
例如,提取<a>
标签中的地址和描述文本
1 | preg_match('/<a\s+href="([^\'"\s]*)">(.*?)<\/a>/', '<a href="github.com">visit github</a>', $arr); |
正则表达式替换时也支持捕获分组。php中支持\num
和$num
的形式替换,但是num
不能大于10;另一种形式${num}
可以大于10。
例如,日期的替换
1 | preg_replace('/(\d{4})-(\d{2})-(\d{2})/', '$1年$2月$3日', '2015-08-25'); ==> 2015年08月25日 |
非捕获分组
正则表达式默认会保存每个(...)
匹配的文本,前面利用这个特性可以实现一些有用的功能。但是,有时候正则表达式比较复杂,(...)
会出现的比较多,而此时仅仅是想实现分组或者多选的功能,而不需要捕获分组;同时,大量不需要的捕获分组可能会影响性能。
为了解决这种问题,正则表达式提供了非捕获分组(non-capturing group),它的形式是(?:...)
。
1 | // 捕获分组 |
断言
正则表达式中的有些结构不匹配真正的文本,只负责判断在某个位置左/右侧的文本是否符合要求,这种结构称为断言(assertion)。
常见的断言有三类:单词边界、行起始/结束位置、环视。
单词边界
单词边界(word boundary),记为\b
。
它匹配的是单词边界(一边是单词字符,另一边不是单词字符)的位置,而不是字符。
1 | // 单词边界 |
这类匹配位置而不匹配字符的元素叫做锚点(anchor),下一节要介绍的^
、$
等也是锚点。
行起始/结束位置
^
:字符串的开始位置$
:字符串的结束位置
1 | $str = 'first line |
如果指定了多行模式(Multiline Mode),^
、$
分别可以匹配行起始位置、行结束位置。
关于模式,将在后面详细介绍。指定多行模式的方式是使用模式修饰符(?m)
:在正则表达式之前加上(?m)
;或者是预定义常量的方式:/.../m
。
环视
环视(look-around)用来“停在原地,四处张望”,它本身也不匹配任何字符,用来限定它旁边的文本满足某种条件。
名字 | 记法 | 含义 |
---|---|---|
肯定顺序环视 | (?=...) |
向右看看,右边出现了环视中的内容才匹配 |
否定顺序环视 | (?!...) |
向右看看,右边不出现环视中的内容才匹配 |
肯定逆序环视 | (?<=...) |
向左看看,左边出现了环视中的内容才匹配 |
否定逆序环视 | (?<!...) |
向左看看,左边不出现环视中的内容才匹配 |
比如在字符串格式化时匹配%s
,要求%s
的前面一个字符不能是%
:
1 | preg_match('/(?<!%)%s/', "%s"); ==> 1 |
匹配模式
前面的内容中已经出现过了单行模式、多行模式、非贪婪模式。
匹配模式是指匹配时使用的规则。常用的匹配模式还有不区分大小写模式、注释模式。
在开始介绍具体的模式之前,先介绍php中模式的两种具体实现/.../{modifier}
和...(?{modifier})...
:
模式修饰符 | /.../{modifier} |
...(?{modifier})... |
---|---|---|
示例 | /<tr>.*<\/tr>/s |
<tr>(?s).*<\/tr> |
名称(php手册) | 模式修饰符 | 模式内修饰符 |
名称(正则指引) | 预定义变量 | 模式修饰符 |
作用范围 | 整个表达式 | 不在分组(子表达式)中时,对它后面的全部正则表达式起作用;如果在分组(子表达式)中,则对它分组中的剩余部分起作用。在没有分组,且放在整个正则表达式最前面的时候相当于/.../{modifier} |
支持程度 | 支持所有模式修饰符 | 支持部分模式修饰符 |
其他编程语言 | 可能不支持 | 一般都支持 |
不区分大小写模式
在html中是不区分大小写的,例如<td>
和<tD>
、<Td>
、<TD>
的作用是一样的。如果要从网页中提取<td>
,不使用匹配模式的表达式应该是这样:<[tT][dD]>
由于<td>
标签只有两个字符,所以上面的写法还可以接受。但是如果标签是<script>
呢?这里,就需要使用不区分大小写模式:/.../i
或...(?i)...
。上面的正则表达式可以进一步写为:/<td>/i或(?i)<td>
1 | preg_match('/(?i)<td>/', '<tD>', $arr); |
单行模式
形式:/.../s
或...(?s)...
作用:点号可以匹配换行符
多行模式
与单行模式没有关系。影响^
和$
的匹配
形式:/.../x
或...(?m)...
注释模式
(?#...)
的方法可以在正则表达式中添加注释
1 | // (?#...)注释 |
非贪婪
形式:/.../U
或...(?U)...
1 | // 默认贪婪匹配 |
Unicode
要匹配中文等Unicode字符,最好是指定Unicode模式修饰符/.../u
。如果不指定会有两个问题
- GBK编码环境下,中文不能匹配
- 无法利用
[\x{4e00}-\x{9fff}]
匹配中文
1 | // 指定unicode模式 |
相关函数
在PHP中,有两套正则表达式函数库。
1.POSIX
扩展的正则表达式函数(ereg_
)。
2.Perl
兼容的正则表达式函数(preg_
)。
Perl
兼容的函数库是后加的函数库,功能更加强大,另外JavaScript
和 Perl
语言里面也会兼容这种模式,所以这里学习一下 Perl
语言兼容的函数库。
php中 PCRE 正则共有如下函数:preg_filter
— 执行一个正则表达式搜索和替换preg_grep
— 返回匹配模式的数组条目preg_last_error
— 返回最后一个PCRE正则执行产生的错误代码preg_match_all
— 执行一个全局正则表达式匹配preg_match
— 执行匹配正则表达式preg_quote
— 转义正则表达式字符preg_replace_callback_array
— Perform a regular expression search and replace using callbackspreg_replace_callback
— 执行一个正则表达式搜索并且使用一个回调进行替换preg_replace
— 执行一个正则表达式的搜索和替换preg_split
— 通过一个正则表达式分隔字符串
常用的是preg_match
、preg_match_all
、preg_replace
这三个
preg_match
语法:preg_match($pattern, $subject [, &$matches [, $flags = 0 [, $offset = 0]]])
=> int
功能:执行正则匹配,搜索subject
与pattern
给定的正则表达式的一个匹配.
参数:
- pattern
string
要搜索的模式(正则表达式) - subject
string
被正则匹配的字符串 - matches
string
如果传入了这个参数,它将被用来存放搜索结果$match[0]
包含完整模式匹配到的文本,$match[1]
包含第一个捕获分组匹配到的文本,以此类推 - flags
int
flags
可以设置为PREG_OFFSET_CAPTURE
(1)
如果设置为1,对于每一个出现的匹配返回时会附加字符串偏移量(相对于目标字符串的)。
注意:这会改变填充到matches
参数的数组,使其每个元素成为一个由 第0个元素是匹配到的字符串,第1个元素是该匹配字符串 在目标字符串subject
中的偏移量。 - offset
int
通常,搜索从目标字符串的开始位置开始。可选参数offset
用于指定从目标字符串的某个位置开始搜索(单位是字节)。
如果offset
比subject
的长度还要大则返回FALSE
。
返回值:
返回pattern
的匹配次数。 它的值将是0次(不匹配)或1次,因为preg_match()
在第一次匹配后将会停止搜索。
如果发生错误将返回 FALSE
。
preg_match_all
语法:preg_match_all($pattern, $subject[, &$matches[, $flags = PREG_PATTERN_ORDER[, $offset = 0 ]]])
=> int
功能:
搜索subject
中所有匹配pattern
给定正则表达式的匹配结果并且将它们以flag
指定顺序输出到matches
中.
在第一个匹配找到后, 子序列继续从最后一次匹配位置搜索.
与 preg_match
用法完全一致,不过它匹配的是所有的信息。
返回值:
返回完整匹配次数(可能是0),或者如果发生错误返回FALSE
。
preg_replace
语法:preg_replace ($pattern, $replacement, $subject[, $limit = -1[, &$count]])
=> mixed
功能:
搜索subject
中匹配pattern
的部分,以replacement
进行替换。
参数:
- pattern
string
array
要搜索的模式。可以使一个字符串或字符串数组。
可以使用一些PCRE修饰符。 - replacement
string
array
用于替换的字符串或字符串数组。
如果这个参数是一个字符串,并且pattern
是一个数组,那么所有的模式都使用这个字符串进行替换。
如果pattern
和replacement
都是数组,每个pattern
使用replacement
中对应的 元素进行替换。如果replacement
中的元素比pattern
中的少, 多出来的pattern
使用空字符串进行替换。 - subject
string
array
要进行搜索和替换的字符串或字符串数组。
如果subject是一个数组,搜索和替换回在subject
的每一个元素上进行, 并且返回值也会是一个数组。 - limit
int
每个模式在每个subject
上进行替换的最大次数。默认是 -1(无限)。 - count
int
如果指定,将会被填充为完成的替换次数。
返回值:
如果subject
是一个数组,preg_replace()
返回一个数组,其他情况下返回一个字符串。
如果匹配被查找到,替换后的subject
被返回,其他情况下 返回没有改变的subject
。如果发生错误,返回NULL
。
模式修饰符(PRCE)
PHP提供了一些模式修饰符,模式修饰符中的空格,换行符会被忽略,其他字符会导致错误。
1 | i 忽略大小写,匹配不考虑大小写 |