正则指引——PHP

PHP

PHP内建了三套各自独立的正则引擎,分别是preg、ereg、mb_ereg。不过日常使用最多的是preg(preg表示PCRE Regex,PCRE表示Perl Compatible RegularExpression,常见的正则表达式写法都是兼容Perl的)​,本章的讲解也以preg为准

1、预备知识

PHP的正则表达式是以字符串文字指定的,不过又不同于普通的字符串文字,如果用一段字符串文字表示正则表达式,文字的首位必须加上分隔符(delimeter)​,通常使用的分隔符是斜线/,在4.0.4以上的版本中,也可以采用()、{}、<>、​这些成对的符号。不过()、{}、​等符号都是正则表达式中常用的元字符,如果用作分隔符,用作元 字符时就也必须转义,非常烦琐,而/在正则表达式中没有特殊含义,所以一般推荐用/做分隔符,本章中也是如此。

你可能注意到了,上面的字符串文字是用单引号标注的。在PHP中,字符串既可以用双引号标注,也可以用单引号标注。两者的主要区别在于,双引号字符串可以插值,而单引号字符串不能;另外,双引号字符串会处理字符串转义,而单引号字符串不会。

如果不需要插值,使用双引号字符串的处理效率就不如单引号字符串,再加上双引号字符串用作正则表达式时还需要处理字符串转义,所以本章表示正则表达式时,都使用单引号字符串直接写出正则文字(除非是PHP 7中新出现的\u{xxxx}转义序列,它必须使用双引号字符串,否则编译不通过)​,而字符串文字则使用双引号字符串,方便识别\r、\n之类常见的转义序列。

下面介绍正则表达式匹配的函数preg_match( p a t t e r n , pattern, pattern,subject)。它接收两个参数,其中pattern是正则表达式,subject是需要匹配的文本;返回的是匹配发生的次数---如果能找到匹配则立刻停止,返回1,否则返回0;如果匹配过程中发生错误,则返回false。需要注意的是,它并不检查整个subject能否由pattern匹配。

注:因为PHP可以把int类型作为布尔判断的条件,0等价于false,其他值等价于true。在下面的代码里,为了形象起见,如果只关心preg_match()的返回值是否为0,则使用true和false来标识。

在正则表达式两端添加\A和\z,就可以判断整个subject能否由pattern匹配。

在有些PHP代码中,使用双引号字符串来表示正则表达式,某些时候,情况看起来有点奇怪。

尽管PHP的正则表达式用字符串文字给出,但它与常见的字符串不完全一样,主要是转义规则有所区别---如果某个转义序列可以由字符串识别,则对其进行转义处理;否则,将整个转义序列"原封不动"地保存下来。在处理正则表达式时,​"原封不动"保存下来的文字,会作为正则文字,只进行正则转义处理。

这种现象可能很难理解:/\d/和/\d/看起来不同,却表示同样的正则表达式。所以,我推荐的做法是​:正则表达式尽量使用单引号字符串,比如'/\d/'或'/\A/';如果必须使用双引号字符串,尽量写为"/\d/"或"/\A/",因为在大多数语言中都可以这么写。

关于mb_string

用过PHP的人大都知道PHP中的mb_string,它用来处理多字节字符构成的字符串(multibyte-string)​,不会将多字节字符拆散为多个单字节字符。比如用mb_strpos()来计算偏移值,得到偏移值的单位就是字符,而不是字节。

相应的,mb_string中也提供了正则表达式函数,比如mb_ereg_match()、mb_ereg_replace()、mb_regex_encoding()等,其功能类似对应的preg函数,具体情况请参考http://www.php.net/manual/en/ref.mbstring.php。

按照文档说明,在设定mb_regex_encoding()之后,可以正确处理非Unicode编码的正则表达式,但实际使用中往往容易出现各种奇怪的问题(例如上面提到的正则的错误匹配,即便设定了GBK编码,也会出现)​,所以一般不推荐这样使用正则表达式。

PHP 7与Unicode

历史上,PHP一直没有很好的办法来处理Unicode。比如我们知道"正"字的Unicode码值是U+6B63,但是没法在代码里通过\u6B63这样的方式来指定它。PHP虽然提供了\x的转义序列,但只支持两位十六进制数,所以\u6B63会被解释为3个字符:k(码值为6B的字符)​、6、3。

这个问题在PHP 7中得到了很好的解决,新提供的\u{xxxx}转义序列完美支持了通过码值指定Unicode字符的方式。而且转义序列不限于4位,也就是说,超出BMP之外的其他Unicode字符(包括Emoji)也可以通过这种方式来指定。所以如果使用PHP 7,最好统一使用\u{xxxx}转义序列。同时要注意的是,一旦使用了这种转义序列,就必须使用双引号字符串。

2、正则功能详解

2.1、列表

下表列出了PHP中的正则功能。

2.2、字符组

PHP中的正则引擎可以正确识别ASCII编码,但无法正确识别GBK编码,因此无法避免GBK编码环境中的"错误匹配"问题​。

解决的办法是采用Unicode编码环境(以Unicode编码保存源文件)​,并在正则表达式末尾的分隔符之后加上模式修饰符u,它指定Unicode模式,也就是按UTF-8编码解释正则表达式。

只有在指定Unicode模式之后,点号才能准确匹配多字节字符。

PHP的正则表达式中(注意,不是字符串中)支持使用\x{hex}通过码值表示字符,其中的hex是4位十六进制数,所以可以用\\x{4E00}-\\x{9FFF}来匹配汉字字符,但是此时一定不能忘记u修饰符。

如果使用的是PHP 7,也可以直接使用新提供的\u{hex},此时可以不使用u修饰符,但必须使用双引号字符串。

在PHP中也可以直接使用POSIX字符组,所有的POSIX字符组都只能匹配ASCII字符,即便指定了Unicode模式也是这样。

2.3、Unicode属性

从PHP 5.1.0开始,PHP支持使用Unicode属性,方法是\p{N}和\P{N},其中N为Unicode Property​,而且此时一定不能忘记u修饰符。​

\p{N}可以匹配"任何一个具备指定Unicode Property的字符"​,而\P{N}匹配"任何一个不具备指定UnicodeProperty的字符"​,以表示大写字母的Lu为例。

\P{N}也可以等价表示为\p{^N}:

因为PHP中可以使用Unicode Script(☞129)​,所以可以用\p{Han}来匹配中文字符。

2.4、字符组简记法

在PHP中,\d、\D、\w、\W、\s、\S都采用字符组简记法的ASCII匹配规则,即便指定了Unicode模式,也如此。

2.5、单词边界

在PHP中,\w采用ASCII匹配规则,所以\b能匹配位置的某一侧必须出现0-9a-zA-Z_,另一侧不能出现0-9a-zA-Z_

前面讲过,PHP处理双引号字符串时,会将字符串文字中无法识别的转义序列保留下来,所以"/\d/"和"/\d/"是没有差别的。值得注意的是,"/\b/"和"/\b/"也是没有差别的,因为PHP的字符串转义并不能识别转义序列\b。

2.6、行起始/结束位置

在PHP的正则表达式中,^匹配的是"行开始的位置"​,在默认情况下,它只能匹配"整个字符串的开始位置"​;如果指定使用多行模式(Multiline Mode)​,^可以匹配字符串内部的文本行的开始位置;而\A无论在什么情况下都匹配"整个字符串的开始位置"​。

在默认情况下,PHP中的$、\Z、\z匹配的是整个字符串的结束位置(如果结束位置有换行符,则$和\Z匹配这个换行符之前的位置)​;使用多行模式会影响到$的匹配,这时候它可以匹配文本内部行的结束位置;如果要进行严格准确的验证,则应当使用\z。

2.7、环视

PHP中的肯定顺序环视(?=...)和否定顺序环视(?!...)内可以嵌套任意形式的子表达式。

PHP的逆序环视中能使用的表达式,必须匹配固定长度的文本,否则会报错。如果表达式中出现了多选结构,而各个分支能匹配的文本长度不一致,那么这个多选结构必须位于表达式的顶层,而且只能出现竖线|,类似bull|donkey,而不能出现括号,比如(bull|donkey)。

2.8、匹配模式

下表列出了PHP中常用的匹配模式。

PHP中指定匹配模式的方式有两种:在结尾分隔符之后加上模式对应的字母,或者在正则表达式的开头用(?modifier)的方式表示。注意:在PHP中没有通过预定义常量指定模式的方法。

其中Unicode模式有些特殊,它只能在末尾分隔符之后使用,不能使用(?modifier)的记法。

PHP也支持用(?-modifier)结束某个模式的作用范围。

2.9、纯文本模式

在\Q和\E之内,所有的元字符和特殊结构都失去特殊意义,只能匹配它们对应的字符本身。

2.10、捕获分组的引用

在PHP中,如果要在正则表达式内部引用捕获分组,则应当使用\num记法,其中num为对应捕获分组的编号。

如果要在替换时引用捕获分组,则应当使用 n u m 记法,不过更好的办法是使用 num记法,不过更好的办法是使用 num记法,不过更好的办法是使用{num}记法,这样可以避免二义性。

如果希望在replacement字符串中表示$字符,则必须使用\$转义。

PHP支持命名分组,分组的记法是(?P...),在表达式中引用的记法为(?P=name)(在5.2.2版本以后可以使用\k或者\k'name',在5.2.4版本之后可以使用\k{name}和\g{name})​,在替换中,却只能通过数字编号来引用。

需要指出的是,如果进行正则表达式替换,replacement中不能使用命名分组,只能使用数字编号分组。

3、正则API简介

3.1、PREG常量说明

preg系列函数有可能用到预定义的常量,所以先介绍这些常量。

3.1.1、PREG_PATTERN_ORDER

仅用于preg_match_all(),在匹配结果的matches数组中,matches0保存每次匹配成功时捕获的全部文本(也就是编号为0的捕获分组匹配的文本)​;matches1保存每次匹配成功时编号为1的捕获分组匹配的文本;依此类推。

3.1.2、PREG_SET_ORDER

仅用于preg_match_all(),在匹配结果的matches数组中,matches0保存每一次匹配成功时,整个表达式以及各捕获分组匹配的文本;matches1保存第2次匹配成功时,整个表达式以及各捕获分组匹配的文本;依此类推。

3.1.3、PREG_SPLIT_OFFSET_CAPTURE

在PHP 4.3.0以上版本中可用,仅用于preg_split()中。如果设定了这个常量,每次匹配之后,会返回后面的字符串的偏移值。请注意,返回值是一个数组,其中每个元素都是一个数组,包含偏移值为0的匹配的字符串,以及它在文本中的偏移值,从1开始。

3.1.4、PREG_OFFSET_CAPTURE

在PHP 4.3.0以上版本中可用,其功能类似PREG_SPLIT_OFFSET_CAPTURE,但它用于preg_match()和preg_matchall(),如果此时指定PREG_SPLIT_OFFSET_CAPTURE,则会报错。

3.1.5、PREG_SPLIT_NO_EMPTY

在preg_split()中使用它,返回结果中不会包含空字符串。

3.1.6、PREG_SPLIT_DELIMI_CAPTURE

在PHP 4.0.5以上版本中可用,在preg_slipt()中使用它,则会捕获正则表达式中的捕获括号所匹配的文本。

下面几个常量都是preg_last_error()返回的。

3.1.7、PREG_NO_ERROR

在PHP 5.2.0以上版本中可用,如果没有错误,则返回它。

3.1.8、PREG_INTERNAL_ERROR

在PHP 5.2.0以上版本中可用,如果PCRE发生了内部错误,则返回它。

3.1.9、PREG_BACKTRACK_LIMIT_ERROR

在PHP 5.2.1以上版本中可用,如果尝试匹配时超过了回溯限制,则返回它。

3.1.10、PREG_RECURSION_LIMIT_ERROR

在PHP 5.2.0以上版本中可用,如果匹配时超过了递归限制,则返回它。

3.1.11、PREG_BAD_UTF8_ERROR

在PHP 5.2.0以上版本中可用,如果在UTF-8模式下正则表达式中出现了不合法的UTF-8数据,则返回它。

3.1.12、PREG_BAD_UTF8_OFFSET_ERROR

在PHP 5.3.0以上版本中可用,如果在UTF-8模式下起始偏移值对应的不是有效的UTF-8码值,则返回它。

3.1.13、PCRE_VERSION

在PHP 5.2.4以上版本中可用,返回PCRE版本号和发布日期。

3.2、preg_quote

这个函数用来"包装"字符串str。如果需要把这个字符串用作正则表达式,又要保证其中元字符都没有特殊含义(比如处理用户输入的内容)​,则可以使用它。

虽然在PHP中使用正则表达式时必须在首尾加上分隔符(delimiter)​,但分隔符可以使用多种字符。所以,preg_quote()中的delimiter是可选参数;尽管一般用斜线/做分隔符,但在默认情况下,str中的斜线并不会转义,除非显式指定使用/作为分隔符。

如果要由某个字符串生成正则表达式,同时保证其中不包含任何元字符,可以用下面的办法(别忘了加上分隔符)​。

也可以直接在字符串两端加上\Q和\E。

3.3、preg_ grep

这个函数用来"筛选"数组,input数组中所有能由正则表达式pattern匹配的字符串会被筛选出来,重新组成一个数组,作为结果返回。​

如果可选参数flags设定为常量PREG_GREP_INVERT,则进行"反向筛选"​,也就是说,input中所有pattern不能匹配的字符串都被筛选出来,重新组成一个数组,作为结果返回。

flags参数在PHP 4.2.0以后才加入。

3.4、preg_match

这是很常用的函数,它在subject中查找pattern的匹配,返回匹配的次数。但是,它会在第一次匹配成功之后停下来,直接返回1;如果始终不能匹配成功,则返回0;如果发生错误,则返回false。

根据preg_match()的返回值,可以方便地进行数据验证,但是注意在正则表达式的首尾加上\A和\z,确保它验证的是整个字符串。下面以验证6到8位数字的字符串为例。

可选参数matches用来保存匹配结果,如果匹配成功,它包含一个元素,即首次匹配的结果,否则数组为空。

如果pattern中出现了捕获分组,则在matches数组中与捕获分组编号对应的元素保存该捕获分组匹配的文本。

可选参数flags控制匹配结果中是否保存匹配发生的位置,如果设置为常量PREG_OFFSET_CAPTURE,则保存匹配发生的位置,否则可以设置为0。

应该注意的是,位置的单位是字节,而不是字符。

可选参数offset控制匹配从subject的哪个位置开始。

注意:这里位置的单位也是字节,而不是字符。

flags参数和PREG_OFFSET_CAPTURE都是PHP 4.3.0之后加入的,offset参数则是PHP 4.3.3之后加入的。

3.5、preg_match_all

这个函数与preg_match()相似,不同的是,它可以一次性找出正则表达式pattern在字符串subject中所有的匹配,返回匹配的次数;如果始终无法匹配,则返回0;如果出错,则返回false。在preg_match_all()中,同样也可以用matches数组来保存匹配结果。

如果pattern中出现了捕获型括号,则matches的每个元素都是数组,它保存对应编号的捕获分组在历次匹配中匹配到的文本。

可以看到,matches数组的第0个元素对应两次匹配时整个pattern匹配的文本(也就是第0个捕获分组匹配的文本)​,第1个元素对应两次匹配时pattern中第1个捕获分组匹配的文本,第2个元素对应两次匹配时pattern中第2个捕获分组匹配的文本。这是默认的方式,flags显式设定为PREG_PATTERN_ORDER也是这样。

如果你在其他语言中使用过正则表达式,可能会觉得这样非常别扭,因为一般的逻辑中,匹配结果集中的每个元素对应到"每次匹配"的结果,而不是"每个分组匹配"的结果。如果希望采用更符合惯例的逻辑,需要将flags设定为PREG_SET_ORDER。

flags参数也可以设定为PREG_OFFSET_CAPTURE,这样matches的结果相比PREG_PATTERN_ORDER,就多出了匹配发生的位置信息(同样,单位是字节)​。

PREG_OFFSET_CAPTURE都是PHP 4.3.0之后加入的,而offset参数则是PHP 4.3.3之后加入的,如果你使用的PHP版本不够高,可能无法使用这两项功能。

3.6、preg_last_error

这个函数返回最近一次调用preg系列方法时出现的错误,如果没有出错,则返回PREG_NO_ERROR,其他错误可以参考"PREG常量说明"​。

3.7、preg_replace

这个函数用来进行正则表达式替换,将subject中的pattern替换为replacement。pattern用于匹配需替换文本的正则表达式,可以是单个字符串,也可以是字符串数组;replacement 表示希望替换成的文本,可以在其中引用匹配的相关信息。如果要引用分组,推荐使用${num}的记法,这种记法没有二义性,不会引起混淆。

如果要在replacement中使用$符号,则必须使用\$转义。

subject表示要进行替换操作的对象,它可以是单个字符串,也可以是字符串数组。如果是字符串,则返回的是字符串;如果是数组,则返回的是数组,同时数组中的每个元素都会进行替换操作。

另外还有两个可选参数,其中limit在PHP 4.0.2以上版本中可用,表示pattern在subject中最多能匹配的次数,默认值为-1,表示不限次数;count在PHP 5.1.0以上版本中可用,如果传入了这个参数,匹配完成后会将它赋值为匹配实际发生的次数。

3.8、preg_replace_callback

这个函数的功能类似preg_replace(),不同的是,它不会简单地把pattern能匹配的文本替换成其他文本,而是通过callback函数来处理,这样大大增加了灵活性,callback接收的参数是一个match数组(而不是字符串)​,返回字符串。下面的代码借助这个函数,把所有单词统一为首字母大写格式。

如果觉得这样很麻烦,也可以用create_function()定义匿名函数。

在PHP 5.3.0以上版本中还可以直接定义匿名函数。

3.9、preg_filter

这个函数类似preg_replace(),唯一的区别在于,它返回的是确实发生过查找-替换的字符串:如果subject是字符串,且发生了查找-替换,则返回替换之后的字符串,否则返回NULL;如果subject是数组,且其中有元素发生了查找-替换,则返回这些发生了查找-替换的元素构成的数组,否则返回空数组。

PHP 5.2.0之后提供了另一个函数filter_var(),它通过预定义的过滤器来判断字符串的值,进行常用的过滤,而不需要显式用到正则表达式,如果判断成功则返回字符串,否则返回false。常见的过滤器包括:

  • FILTER_VALIDATE_EMAIL:验证是否为E-mail
  • FILTER_VALIDATE_FLOAT:验证是否为浮点型
  • FILTER_VALIDATE_INT:验证是否为整型
  • FILTER_VALIDATE_IP:验证是否是合法IP
  • FILTER_VALIDATE_URL:验证是否是合法URL

以下是示例代码:

3.10、preg_split

这个函数用来切分字符串,返回切分所得的字符串数组。pattern匹配的文本作为切分的分隔,而subject 则是切分的字符串。

limit是可选参数,它设定pattern匹配的次数。如果设定了limit,那么返回数组包含limit个元素,也就是说,最多进行limit-1次切分。如果limit设为-1、0、null,都表示"不设定上限"​,也就是"切分尽可能多"​。

flags是可选参数,用于控制返回的数据,还可以选择常量PREG_SPLIT_NO_EMPTY,它要求返回数组忽略空字符串。

有可能引起混淆的是limit和PREG_SPLIT_NO_EMPTY同时作用的情况,limit设定的并不是"返回数组的长度"​,而是"pattern匹配的次数"​,所以并不会"先全部切分,再过滤数组"​。

3.11、preg_replace_callback_array

这是PHP 7新增的函数,功能与preg_replace_callback()类似。在preg_replace_callback()中,可以定义一个函数来对匹配结果进行复杂的替换操作,但有些时候单个函数仍然不够,我们希望在替换操作中进行更复杂的操作,preg_replace_callback_array()就可以做到这点,它传入一组函数,可以对匹配结果进行分门别类的处理。下面的代码借助这个函数,把所有T字母开头的单词变为首字母大写,其他字母开头的单词变为全大写。

4、常见的正则操作举例

4.1、验证

4.2、提取

逐个提取

一次性提取

4.3、替换

简单替换

在replacement中使用$

4.4、切分

相关推荐
Claire_881 小时前
企业文件管理系统选型技术框架与主流产品对比分析
开发语言·php
(Charon)2 小时前
【C++】:使用 mutex + deque 实现一个简单的 LockedQueue
开发语言·c++
COOLMO研究AI2 小时前
Python 如何给 AI API 实现断路器模式:防止雪崩与保护本地服务
人工智能·python·php
星轨初途2 小时前
LeetCode 热题 100——day10 和为 K 的子数组
开发语言·c++·算法·leetcode
sycmancia2 小时前
Qt——自定义控件温度计
开发语言·qt
UQR2 小时前
一、Java基础高频面试题
java·开发语言
AC赳赳老秦2 小时前
网页公开附件自动采集:OpenClaw 批量下载页面内嵌 Word/Excel 附件,统一格式后结构化入库
java·python·word·php·excel·deepseek·openclaw
MoonBit月兔2 小时前
MoonBit 受邀亮相 SPLASH/ISSTA 2026,与 Java、Rust、Eiffel、D 语言核心贡献者同场交流
开发语言·后端·rust
必须会一定会2 小时前
用纯 HTML/JS 做一个 AI 需求澄清器:把模糊想法转换成可执行任务书
开发语言·前端·javascript·人工智能·html·ai编程