
PHP正则怎么写:从基础语法到常见匹配示例
PHP 正则表达式常用于验证输入格式、提取文本片段、查找内容和批量替换。PHP 的 preg_* 函数使用 PCRE(Perl Compatible Regular Expressions)引擎。掌握几种常见语法后,通常不需要写很复杂的表达式;更重要的是明确匹配边界、处理编码,并避免让不可信输入控制正则表达式。
本文示例面向 PHP 8.x,使用单引号字符串保存正则,以减少 PHP 字符串本身的转义干扰。示例均可放进 PHP 文件运行。HTML 中的代码已按页面要求转义。
一、先理解正则在 PHP 中的写法
PHP 的正则表达式需要用分隔符包起来,常见写法是:
$pattern = '~表达式~修饰符';
例如,匹配一串数字:
<?php
$pattern = '~[0-9]+~';
$text = '订单编号 5831';
if (preg_match($pattern, $text, $matches) === 1) {
echo $matches[0];
}
输出为 5831。这里的 ~ 是分隔符,不属于表达式本身。斜杠 /、井号 # 等也可以作为分隔符。选择不容易出现在表达式中的字符,往往能少写转义。例如,匹配 URL 或路径时,选用 ~ 通常比用 / 方便。
分隔符若出现在表达式中,需要转义,或者改用其他分隔符。下面两种写法都能匹配字符串中的斜杠:
$pattern1 = '/a\/b/';
$pattern2 = '~a/b~';
不要把分隔符和修饰符混为一谈:表达式写在两个分隔符之间,修饰符写在结束分隔符之后,例如 ~abc~i。
二、基础语法:字符、字符类和重复次数
1. 普通字符与元字符
多数普通字符按字面含义匹配,例如 cat 匹配文本里的连续字符 “cat”。以下字符通常有特殊含义,称为元字符:
. ^ $ * + ? { } [ ] \ | ( )
例如,点号 . 默认表示“除换行符外的任意一个字符”,并不是字面上的句点。要匹配句点本身,应写 \.。匹配文本 a+b 时,加号也要转义:
$pattern = '~a\+b~';
反斜杠同时受正则语法和 PHP 字符串语法影响。使用单引号字符串时,大多数反斜杠会原样保留,通常比较直观;如果用双引号,像 \n 这样的内容可能先被 PHP 解释。无论使用哪种字符串,都要确认最终传给 preg_* 函数的模式符合预期。
2. 字符类
方括号表示从一组字符中匹配一个字符:
[abc] 匹配 a、b、c 中的一个
[a-z] 匹配一个小写英文字母
[0-9] 匹配一个数字
[^0-9] 匹配一个非数字字符
字符类中的连字符可能表示范围;若要匹配普通连字符,可将它放在字符类开头或结尾,或者转义。下面的模式匹配一个由 ASCII 英文字母、数字、下划线组成的字符串:
$pattern = '~^[A-Za-z0-9_]+$~';
字符类的取反只作用于一个字符。比如 [^0-9] 表示一个非数字字符,不表示“后面所有内容都不是数字”。此外,\d、\w、\s 分别常用于数字、单词字符和空白字符。它们的具体范围可能受 PCRE 模式和修饰符影响;需要精确限定 ASCII 字符时,直接写 [0-9]、[A-Za-z] 往往更明确。
3. 数量词
数量词描述前一个字符、字符类或分组重复多少次:
* 零次或多次
+ 一次或多次
? 零次或一次
{3} 恰好三次
{2,5} 两到五次
{2,} 至少两次
例如,匹配一个由一位或多位数字组成的片段:
$pattern = '~[0-9]+~';
数量词默认是贪婪的,会尽可能多地匹配,同时仍让整个表达式成功。下面的代码会匹配两个尖括号之间尽可能长的一段内容:
<?php
$text = '<first> and <second>';
preg_match('~<.*>~', $text, $matches);
var_dump($matches[0]);
结果是 <first> and <second>。如果想尽量少匹配,可以在数量词后加 ?,例如 .*?:
preg_match('~<.*?>~', $text, $matches);
var_dump($matches[0]);
这次结果是 <first>。非贪婪并不等于“自动正确”:若内容可跨行、包含嵌套标记,仍要设计准确的边界。解析复杂 HTML 通常应使用 HTML 解析器,而不是试图用一个正则完整处理。
三、锚点与边界:匹配片段还是验证整个字符串
默认情况下,preg_match() 会在字符串任意位置查找匹配。因此,模式 [0-9]+ 会在 abc123xyz 中找到 123。如果是在验证输入,就要明确是否要求整个字符串都符合格式。
^ 常用于表示字符串或行的开头,$ 常用于表示字符串或行的结尾。默认模式下,它们分别锚定整个 subject 的开头和结尾;开启多行模式 m 后,它们也会匹配每一行的边界。需要严格验证完整字符串时,可使用绝对起止锚点 \A 和 \z:
<?php
$value = '5831';
if (preg_match('~\A[0-9]{1,8}\z~', $value) === 1) {
echo '格式符合';
} else {
echo '格式不符合';
}
这里要求字符串全部由 1 至 8 位 ASCII 数字组成。使用 \z 可以避免把字符串末尾换行符前的位置误当成严格的结束位置。验证之前还要明确空字符串是否有效、允许的长度是多少,以及是否接受空白字符;不要依赖正则替你猜测业务规则。
\b 表示单词边界,通常依据“单词字符”和“非单词字符”的转换判断。它适合简单的英文单词查找,但不等同于中文分词边界。例如,要在文本中找独立英文单词 cat,可以写:
$pattern = '~\bcat\b~i';
如果目标是中文词语、用户名边界或业务定义的分隔符,应把边界条件明确写出来,而不是直接假设 \b 符合语言习惯。
四、分组、选择与捕获
圆括号可以把多个字符组成一个整体,也可以捕获匹配内容:
(ab)+ 匹配一个或多个连续的 ab
cat|dog 匹配 cat 或 dog
选择符 | 的优先级较低。若只希望某一部分二选一,建议加分组。例如,匹配完整的 color 或 colour:
$pattern = '~colou?r~';
这里 u? 表示字母 u 可有可无。若分支更复杂,使用分组通常更易读:
$pattern = '~\A(?:cat|dog)\z~';
(...) 是捕获组,会把内容放进匹配结果;(?:...) 是非捕获组,只用于组合语法,不额外占用捕获编号。以下示例提取日期中的年月日:
<?php
$text = '发布日期:2025-03-08';
$pattern = '~([0-9]{4})-([0-9]{2})-([0-9]{2})~';
if (preg_match($pattern, $text, $matches) === 1) {
echo '年:' . $matches[1] . PHP_EOL;
echo '月:' . $matches[2] . PHP_EOL;
echo '日:' . $matches[3] . PHP_EOL;
}
$matches[0] 是完整匹配,$matches[1]、$matches[2] 等对应捕获组。捕获编号多了容易维护困难,可以使用命名捕获组:
<?php
$pattern = '~(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})~';
$text = '2025-03-08';
if (preg_match($pattern, $text, $matches) === 1) {
echo $matches['year'] . '-' . $matches['month'] . '-' . $matches['day'];
}
这个例子只检查字符格式,并未验证日期真实存在。例如,2025-99-99 也符合“4 位数字-2 位数字-2 位数字”的结构。日期是否合法,应进一步使用日期 API 进行校验。
五、常见 PHP 正则函数
1. preg_match:查找一次
preg_match($pattern, $subject, $matches) 在 subject 中查找一次。返回值为 1 表示找到,0 表示未找到,发生正则执行错误时返回 false。因此不要只写真假判断后就忽略错误;在需要诊断的代码中,应使用严格比较。
<?php
$result = preg_match('~[0-9]+~', '编号 42', $matches);
if ($result === false) {
echo '正则执行失败:' . preg_last_error_msg();
} elseif ($result === 1) {
echo '找到:' . $matches[0];
} else {
echo '没有匹配';
}
2. preg_match_all:查找全部匹配
preg_match_all() 用于提取所有匹配项。比如从文本中取出所有连续数字:
<?php
$text = 'A12 B305 C7';
$count = preg_match_all('~[0-9]+~', $text, $matches);
if ($count !== false) {
var_export($matches[0]);
}
结果数组包含 12、305、7。一旦使用捕获组,结果数组的结构还会受 $flags 影响;处理复杂提取逻辑时,先检查实际结果结构,再编写后续代码。
3. preg_replace:替换匹配内容
preg_replace() 可把匹配部分替换成文本。下面移除文本里的连续空白,并将其替换为一个空格:
<?php
$text = "PHP\t正则\n表达式";
$result = preg_replace('~\s+~', ' ', $text);
echo $result;
如果正则可能出错,preg_replace() 会返回 null;不要把执行失败与“替换结果为空字符串”混为一谈。也可以使用 preg_replace_callback(),根据捕获内容动态生成替换结果,例如计算、格式转换等场景。
4. preg_split:按模式分割
要按一个或多个逗号、分号或空白字符分割文本,可以使用:
<?php
$text = 'apple, banana; orange';
$parts = preg_split('~[,;\s]+~', $text, -1, PREG_SPLIT_NO_EMPTY);
var_export($parts);
-1 表示不限制分割次数,PREG_SPLIT_NO_EMPTY 会省略空片段。若输入格式有引号、转义分隔符或嵌套结构,简单分割正则可能不够,应使用专门的解析器。
六、常用匹配示例
1. 验证简单用户名格式
下面将规则定义为:长度 3 至 20 个字符,只允许 ASCII 英文字母、数字和下划线。
<?php
$username = 'dev_2025';
$pattern = '~\A[A-Za-z0-9_]{3,20}\z~';
if (preg_match($pattern, $username) === 1) {
echo '格式有效';
} else {
echo '格式无效';
}
它只做格式检查,不保证用户名未被占用,也不意味着该值适合作为 SQL、HTML 或命令参数。不同输出环境仍需使用对应的参数化接口或转义方式。
2. 提取 URL 中的主机名要谨慎
URL 的完整语法包括协议、认证信息、IPv6 地址、端口、路径和编码等情况。用一个简短正则“验证所有 URL”容易漏掉边界。若目标只是解析常见 URL 的组成部分,应优先使用 PHP 的 URL API,而不是自己拼接复杂模式:
<?php
$url = 'https://example.com/path?q=php';
$parts = parse_url($url);
if ($parts !== false) {
echo $parts['scheme'] ?? '';
echo PHP_EOL;
echo $parts['host'] ?? '';
}
parse_url() 用于解析 URL 结构,不负责确认远端地址可访问,也不等于完整的安全校验。若要限制可访问的协议、主机或网络地址,还需要按业务要求单独检查。
3. 匹配 HTML 标签并不等于解析 HTML
面对固定、简单、受控的片段,可以用正则做有限的文本提取;但真实 HTML 可能包含属性中的引号、注释、实体、嵌套元素和格式变化。不要把“匹配到一段看起来像标签的文本”当作可靠的 HTML 解析。需要遍历或修改 HTML 时,应使用 DOM 等解析工具。
七、Unicode 与中文匹配
PCRE 的 u 修饰符让模式和 subject 按 UTF-8 处理。若要匹配中文字符,可以先用它验证一个具体的中文文本:
<?php
$text = '你好,PHP';
$pattern = '~\A你好,PHP\z~u';
$result = preg_match($pattern, $text);
var_dump($result);
使用 u 的前提是模式和输入数据都是有效的 UTF-8。无效 UTF-8 可能导致匹配失败并产生错误。中文字符的范围也不是简单的一个连续区间就能涵盖所有汉字、扩展区文字和兼容字符;如果业务要求精确限制字符集合,应先定义需要支持的 Unicode 范围,并根据 PHP 与 PCRE 版本验证行为。
没有 u 时,正则通常按字节处理 UTF-8 字符串。比如点号默认匹配一个字符,但在非 Unicode 模式下,对多字节 UTF-8 文本的“字符”理解可能与预期不同。处理中文时,明确编码、开启 u 并为输入做编码校验,比只在表达式里写中文更重要。
还要区分字符数和字节数。PHP 字符串长度、某些偏移量以字节计;开启 UTF-8 模式并不会让所有 PHP 字符串 API 自动改为按 Unicode 字符计数。需要计算用户感知字符长度时,应考虑 mbstring 等合适工具。
八、常见修饰符及其边界
i:忽略大小写,适合不区分大小写的字母匹配。m:多行模式,改变^和$对行边界的处理。s:让点号.也匹配换行符。u:按 UTF-8 模式处理表达式和 subject。x:允许在模式中加入格式化空白和注释,但空格等字符需要按规则转义或放入字符类。
修饰符不是越多越好。比如 s 会扩大点号的匹配范围,m 会改变锚点行为,组合使用时应通过边界用例确认实际结果。多行修饰符也不会自动把整个 subject 拆成多次独立匹配;它只是影响锚点的含义。
九、正则安全:不要用它代替输入处理和权限控制
1. 格式验证不是安全过滤
正则可以检查一个值是否符合预先定义的格式,但不能因此保证值在所有上下文中都安全。验证通过的字符串仍可能包含 SQL、HTML、Shell 或 URL 中具有特殊意义的内容。安全处理应根据使用场景选用正确机制:数据库使用参数化查询,输出到 HTML 时使用适当的 HTML 编码,执行命令时避免拼接用户输入。
例如,验证“只能输入数字”可以用于业务格式检查,却不能替代 SQL 参数绑定。安全边界应建立在正确的 API 和权限控制上,而不是寄希望于某条正则挡住所有危险输入。
2. 防止灾难性回溯
某些含有相互重叠重复结构的模式,在特定输入上会尝试大量可能路径,导致执行时间明显增加。此类风险常被称为灾难性回溯,可能被用于拒绝服务。模式中出现嵌套的贪婪重复,例如 (a+)+,就值得检查;但风险取决于完整模式、输入和引擎行为,不能只靠搜索某个符号判断。
降低风险的做法包括:限定 subject 长度;减少含糊的重复结构;在设计明确时使用字符类或更严格的边界;为用户提交的复杂模式设置合理的执行约束;用接近最坏情况的长输入做测试。PCRE 的回溯和递归相关限制可通过 PHP 配置及环境进行管理,具体设置名称和默认值可能随 PHP、PCRE 版本和部署环境变化,应查看当前运行环境的配置,不要假定存在一个适用于所有服务器的固定数值。
如果模式本身由用户提供,不能把它直接交给 preg_* 函数并认为安全。应评估是否真的需要允许用户编写正则,并考虑限制模式能力、执行时间和输入规模。
十、常见错误与排查办法
- 分隔符没有闭合。 模式缺少结尾分隔符,或内部出现未转义的分隔符时,PHP 会报告编译错误。检查模式两端以及分隔符是否冲突。
- 漏掉需要转义的元字符。 想匹配字面句点却写成
.,会匹配任意字符。先判断字符在当前位置是否有特殊含义。 - 验证时没有锚定。
[0-9]+只要找到一段数字就算匹配;严格验证应根据需求使用\A、\z限定完整字符串。 - 把匹配失败和错误混为一谈。 对
preg_match()的返回值区分1、0和false;发生错误时可查看preg_last_error(),PHP 8.0 及以上还可使用preg_last_error_msg()。 - Unicode 模式下数据编码不一致。 有
u不代表任何字节序列都是有效 UTF-8;应确认数据来源和编码转换过程。 - 把格式校验误当语义校验。 正则检查出日期形状,不表示日期真实存在;检查出电子邮件外形,也不表示地址可投递。业务语义需要额外验证。
- 忽略空值、空格和换行。 需求中要说明是否允许前后空白、空字符串和末尾换行;必要时明确调用
trim(),而不是让表达式意外接受或拒绝。
调试时可以先缩小表达式,从一小段已知输入开始,逐步增加字符类、量词和边界。对无法编译的模式,先用 PHP 检查返回值并记录错误;对结果不符合预期的模式,编写成功、失败、空值、换行和非 ASCII 字符等测试用例。把规则说明、示例输入和预期输出一起保存,能显著降低以后修改时引入回归问题的概率。
结语
PHP 正则的基本工作方式可以概括为:使用分隔符包住 PCRE 表达式,按需要添加修饰符,再通过 preg_match()、preg_match_all()、preg_replace() 或 preg_split() 执行操作。写模式时优先确定目标、边界和字符范围;处理完整输入时明确锚点,处理中文时核对 UTF-8,处理不可信输入时关注长度、回溯和正确的安全 API。简单而可解释的模式,通常比一条难以维护的“万能正则”更可靠。




