玩转PHP中的正则表达式
检验用户输入、解析用户输入和文件内容,以及重新格式化字符串
级别: 中级
正则表达式提供了一种处理文本的强大方法。使用正则表达式,您可以对用户输入进行复杂的检验、解析用户输入和文件内容,以及重新格式化字符串。PHP 为用户提供了使用 POSIX 和 PCRE 正则表达式的简单方法。本教程将讨论 POSIX 和 PCRE 之间的差异,并介绍如何使用正则表达式和 PHP V5。
开始之前
了解通过本教程可学到哪些内容以及如何更好地利用本教程。
关于本教程
正则表达式提供了一种处理文本的强大方法。使用正则表达式,您可以对用户输入进行复杂的检验、解析用户输入和文本内容,以及重新格式化字符串。
目标
本教程将集中介绍使用 POSIX 和 PCRE 正则表达式的简单方法,使您熟练掌握 PHP 的正则表达式。我们将探讨 POSIX 和 PCRE 之间的差异,还会介绍如何使用正则表达式和 PHP V5。通过学习本教程,您将了解使用正则表达式的方法、时机和理由。
系统需求
您可以在任何安装了 PHP 的类 Microsoft® Windows® 或类 UNIX® 系统(包括 Mac OS X 和 Linux®)上完成本教程。由于我们所介绍的内容均为 PHP 内置插件,因此只需在系统中安装 PHP 即可,无需安装其他软件。
开始
何为正则表达式?
几年前,我对 Web
表单的输入框做了一些有趣的检验。用户将在此表单中输入电话号码。随后,此电话号码会按用户键入的形式打印在用户的广告中。按照要求,美国的电话号码可以几种方式输入:可以是
(555) 555-5555,也可以是 555-555-5555,但不能接受 555-5555 这样的形式。
您或许会感到奇怪,为什么我们不抛开所有的非数字字符,只保证剩余的字符总数为 10 呢?这种方法确实可行,但无法阻止用户输入 !555?333-3333 这样的内容。
以一名 Web 开发者的眼光来看,这种情况带来了一项有趣的挑战。我可以编写例程来检查各种不同格式,但我希望能够找到一种解决方案,假如用户随后认可 555.555.5555 这样的格式,这种解决方案能具备一定的灵活性。
这正是正则表达式(简称为 regex)的适用场景。之前我已经将它们剪切并粘贴到了应用程序中,但从未发现任何难以理解的语法问题。Regex 看上去非常像数学表达式。当您看到一个形如 2x2=4 的表达式时,您通常会想到 “2 乘以 2 等于 4”。正则表达式与之非常类似。阅读过本文后,当您看到一个这样的正则表达式 ^b$ 时,您就会告诉自己:“一行的开头是 b,随后就是行尾”。不仅如此,您还会意识到在 PHP 中使用正则表达式有多么简单。
使用 regex 的时机
在有规则可循时,您应使用 regex
来完成搜索和替换操作,但不必具有需要找到或替换的确切字符。举例来说,在上文中提到的电话号码的例子中,用户定义了表明所输入电话号码的格式的规则,但并未定义电话号码中所包含的数字。这同样适用于有大批用户输入的场景。美国州名缩写可限制为两个从
A 到 Z 的大写字母。这里也可使用正则表达式,您可简单地将表单中的文本或用户输入限制为字母表中的字母,而无需考虑大小写和长度问题。
不宜使用 regex 的时机
正则表达式功能强大,但也有一些缺陷。其中之一就是要求具备读写表达式的相关技能。如果您决定在应用程序中包含正则表达式,就应该对其进行完整的注释。这样,此后如果有其他人需要更改表达式,即可在不中断功能的情况下完成更改。另外,如果您对于使用正则表达式不够熟悉,可能会发现它们难于调试。
为避免出现这些难题,在更简单的内置功能足以很好地解决问题时不要使用正则表达式。
POSIX 与 PCRE
PHP 支持两种正则表达式的实现:Portable Operating System Implementation(POSIX)和
Perl-Compatible Regular Expression(PCRE)。这两种实现提供了不同的特性,但它们在 PHP
中使用起来一样简单。您所使用的 regex 风格取决于您过去在 regex 使用方面的经验和使用习惯。有一些证据表明,PCRE 表达式的速度比
POSIX 表达式要略微快一点,但在绝大多数应用程序中,这一差别体现得不是那么明显。
在本文的示例中,各 regex 方法的语法都包含在注释中。在函数语法中,regex 为 regex
参数,所搜索的字符串为 string
。括号中的参数是可选的,由于本教程主要介绍基础内容,故不会给出全部可选参数的介绍。
正则表达式语法
尽管 POSIX 和 PCRE 实现在对某些特性和字符类的支持方面有所不同,但它们的语法是相同的。每个正则表达式都是由一个或多个字符、特殊字符(有时也称为元字符)、字符类和字符组构成的。
POSIX 和 PCRE 使用相同的通配符 —— 在 regex 中以通配符来表示 “此处可为任意内容”。通配符字符为一个英文句号或点(.
)。若要查找英文句号或点,可使用转义字符 /
: /.
。下文中所讨论的其他特殊字符也是如此,例如行锚(line anchor)和限定符。如果一个字符在正则表达式中有特殊含义,那么必须通过转义才能表达其原本的文字含义。
行锚 是特殊的元字符,与一行的开头和结尾相匹配,但不会捕获任何文本(参见表 1)。例如,如果某一行以字母 a
开头,那么表达式 ^a
中的行锚不会捕获字母 a
,而是匹配行的开头。
表 1. 行锚
锚 | 描述 |
---|---|
^ |
匹配一行的开头 |
$ |
匹配一行的结尾 |
限定符 应用于紧接于其前的表达式(参见表 2)。使用限定符,您可以指定在一次搜索中查找到一个表达式的次数。例如,表达式 a+
将一次或多次地查找到字母 a
。
表 2. 限定符
限定符 | 描述 |
---|---|
? |
限定符之前的表达式可被查找到 0 次或 1 次 |
+ |
限定符之前的表达式可被查找到 1 次或多次 |
* |
限定符之前的表达式可被查找到任意次(含 0 次) |
{n} |
限定符之前的表达式仅可被查找到 n 次 |
{n,m} |
限定符之前的表达式可被查找到 n 次到 m 次之间 |
在 regex
中,捕获文本并在替换和搜索操作中引用该文本是一项非常有用的特性(参见表 3)。通过使用捕获功能,您可以执行搜索,来查找重复的单词和闭合的
HTML 及 XML
标记。如果您在替换时使用了捕获功能,那么可以将找回的文本置入替换字符串内。后面将给出一个示例,展示如何以超链接替换电子邮件地址。
表 3. 分组与捕获
字符类 | 描述 |
---|---|
() |
分组字符,并能够捕获文本 |
POSIX 字符类
POSIX 正则表达式遵循一些使其可为许多 regex 实现所用的标准(参见表 4)。例如,如果您正在编写一条 POSIX 正则表达式,您可以在 PHP 中使用它、可以通过 grep
命令使用它,也可以通过许多支持正则表达式的编辑器使用它。
表 4. POSIX 字符类
字符 | 描述 |
---|---|
[:alpha:] |
匹配包含字母与数字的字符 |
[:digit:] |
匹配任意数字 |
[:space:] |
匹配任意空白 |
POSIX 匹配
有两个使用 POSIX 正则表达式搜索字符串的函数,即 ereg()
和 eregi()
。
ereg()
ereg()
方法为特定正则表达式搜索字符串。如果未找到任何匹配项,则返回 0,因此您可以给出如下测试:
清单 1. ereg() 方法
正则表达式 [-[:digit:]]{12}
查找 12 个为数字或连字符的字符。就处理电话号码而言,这有些粗略,您也可将其改写成这样的形式:^[0-9]{3}-[0-9]{3}-[0-9]{4}$
。(在 regex 中,[0-9]
和 [:digit:]
实际上是完全相同的,您可能更愿意使用 [0-9]
的形式,因为它更短些。)这种作为替代方案的表达式显然更为精确。它会查找行的开头(^
),后接一组 3 个数字([0-9]{3}
)、一个连字符(-
)、另外一组 3 个数字、另外一个连字符、一组 4 个数字,然后是行的结尾($
)。当您手工编写表达式时,这会使您了解到正则表达式要处理的问题的复杂程度如何,从而有助于预测出使用表达式搜索或替换的数据类型。
eregi()
eregi()
方法类似于 ereg()
,不同之处在于它对大小写不敏感。它将返回一个包含所找到的匹配项长度的整数,但您很可能会将其用于条件语句中,如下所示:
清单 2. eregi() 方法
执行此示例时,将输出 Found match!
,这是因为在忽略大小写的搜索中找到了 hello。如果您使用的是 ereg
,搜索将失败。
POSIX 替换
ereg_replace()
和 eregi_replace()
这两种方法用于在文本中进行替换,具有 POSIX 正则表达式的特性。
ereg_replace()
您可以使用 ereg_replace()
方法以 POSIX 正则表达式语法进行大小写敏感的替换。如下示例描述了如何替换带有超链接的字符串内的电子邮件地址:
清单 3. ereg_replace() 方法
这是一条用于匹配电子邮件地址的正则表达式的不完整版本,但它展示了与 str_replace()
等其他普通替换函数相比,ereg_replace()
的强大之处。在使用正则表达式时,您可定义搜索的规则,而不是搜索文字字符。
eregi_replace()
除忽略大小写之外,eregi_replace()
函数与 ereg_replace()
是完全相同的:
清单 4. eregi_replace() 函数
本例将 banana
替换为 pear
,替换操作忽略了大小写。
PCRE 字符类
由于 PCRE 语法支持更短的字符类和更多的特性,因此它比 POSIX 语法更为强大。表 5 列出了 PCRE 中支持而在 POSIX 表达式中没有的部分字符类。
表 5. PCRE 字符类
字符类 | 描述 |
---|---|
/b |
词边界,查找词的开始和结尾 |
/d |
匹配任意数字 |
/s |
匹配任意空白,如 tab 或空格 |
/t |
匹配一个 tab 字符 |
/w |
匹配包含字母与数字的字符 |
PCRE 匹配
PHP 中的 PCRE 匹配函数与 POSIX 匹配函数类似,但如果您习惯使用 POSIX 表达式,那么 PCRE 匹配函数的一项特性可能会使您感到棘手:PCRE 函数要求表达式以分隔符开始和结束。在绝大多数示例中,分隔符都是一个 /
,可在引号内表达式的开始和结尾处看到。务必牢记,此分隔符并非表达式的一部分。
在 PCRE 中的最后一个分隔符后,您可添加一个修饰符来更改正则表达式的行为。举例来说,i
修饰符使正则表达式对大小写不敏感。这是与 POSIX 方法的一项重要差异,在 POSIX 中,您需要按照对大小写敏感性的需求来调用不同的方法。
preg_grep()
preg_grep()
方法返回一个数组,其中包含通过正则表达式在其中找到匹配项的另外一个数组的全部项目。如果您有一个较大的值集,并希望对其进行搜索以查找匹配项,那么该方法非常有用。下面是一个示例:
清单 5. preg_grep() 方法
在本例中,正则表达式 ^/d+$
查找行的开始(^
)和结尾($
)之间包含一个或多个数字(/d+
)的数组的所有元素。
preg_match()
preg_match()
函数使用 PCRE 在字符串中查找匹配项,它需要两个参数:regex 和字符串。您可以选择提供一个将由匹配项填充的数组、允许您修改匹配操作行为的标志,还可提供字符串中开始查找匹配项的位置(offset
)。示例如下:
清单 6. offset 方法
本例使用了正则表达式 ^[a-z]+$
,在行的开始(^
)和结尾($
)之间搜索可查找到一次或多次的([a-z]+
)、从 a
到 z
的任意字母。
preg_match_all()
preg_match_all()
函数为在字符串中查找到的全部匹配项构建一个数组。下例构建了一个包含句子中全部词的数组:
清单 7. preg_match_all() 函数
正则表达式 /b/w+/b
在词边界(/b
)间查找可找到一次或多次的(/w+
)单词字符。每个词都将置入输出数组 $arrayout
的一个数组元素中。
PCRE 替换
在 PHP 中进行 PCRE 替换与 POSIX 替换类似,不同之处在于使用的是 preg_replace()
而非 ereg_replace()
和 eregi_replace()
。
preg_replace()
preg_replace()
函数使用 PCRE 进行替换。它需要这样几个参数:正则表达式、替换表达式和原始字符串。您还可以选择提供希望的最大替换数,以及以所完成的替换数填充的变量。示例如下:
清单 8. preg_replace() 函数
本例快速演示了捕获部分文本及使用反向引用 的方法,如 //1
。这些反向引用会插入圆括号内所匹配的任意文本中,在本例中,//1
匹配第 1 组 (/d{3})
。
在示例中,您可使用 substr
将电话号码分割开来,而对字符串只需进行少量更改,要依靠 substr
来可靠地捕获正确文本会更加困难。
如果字符串的形式可为 (555)5555555
,您可将表达式修改为 ^(?(/d{3}))?(/d{3})(/d{4})$
以查找任意圆括号。
结束语
PHP 为正则表达式提供了两种语法:POSIX 和 PCRE。本教程对 PHP 中支持 POSIX 和 PCRE 正则表达式的主要函数进行了高度概述。
使用正则表达式,您可以定义规则来进行更强大的搜索和替换操作 —— 大大超越了文字的搜索与替换。