
PHP正则怎么写?从基础匹配到捕获分组
在 PHP 中,正则表达式常用于检查输入格式、提取文本片段、批量替换内容和拆分字符串。它适合描述“哪些字符、按什么顺序、出现多少次”,但不适合承担所有文本解析工作:复杂 HTML、嵌套语言结构以及需要精确处理编码和语法的内容,通常应使用对应的解析器。
PHP 通过 preg_* 函数调用 PCRE 正则引擎。写正则时,除了理解模式本身,还要留意 PHP 字符串的引号规则、分隔符、返回值和编码选项。下面从可以运行的小例子开始,逐步介绍常用语法、捕获分组和容易忽略的边界。
一、PHP 正则表达式的基本结构
PHP 的正则模式通常包含分隔符、模式内容和可选修饰符。分隔符用来标记模式的起止位置,常见的有斜杠 /、井号 # 和波浪号 ~。例如 /cat/i 中,/ 是分隔符,cat 是模式,i 表示忽略大小写。
<?php
$text = 'A cat and a CAT';
if (preg_match('/cat/i', $text) === 1) {
echo "找到了\n";
}
preg_match() 找到一次匹配时返回 1,没有找到时返回 0,发生错误时返回 false。因此,不要把它只当作普通布尔值使用;需要区分匹配与错误时,应用严格比较:
<?php
$result = preg_match('/dog/', 'a cat');
if ($result === false) {
echo '正则执行失败';
} elseif ($result === 1) {
echo '匹配成功';
} else {
echo '没有匹配';
}
模式定界符可以选与文本内容较少冲突的字符。例如匹配网址路径时,使用 ~ 可以少写一些转义:
<?php
$path = '/docs/php/regex';
var_dump(preg_match('~^/docs/~', $path));
如果模式内容本身包含分隔符,需要转义,或者换一个分隔符。下面两个模式效果相同:
<?php
$url = 'https://example.com/a';
var_dump(preg_match('/https:\/\/example\.com/', $url));
var_dump(preg_match('~https://example\.com~', $url));
选择合适的分隔符只是为了可读性,不会改变匹配规则。分隔符也可以成对使用,例如 {...};不过初学时使用 /.../ 或 ~...~ 更容易看懂。
二、常用字符、字符类和量词
正则中的普通字符通常匹配自身。特殊字符则有专门含义,常见的包括 .、^、$、*、+、?、{}、[]、() 和 |。如果要按字面意思匹配特殊字符,需要用反斜杠转义,例如 \. 匹配句点。
| 写法 | 含义 | 示例 |
|---|---|---|
. | 匹配一个字符,默认不匹配换行符 | a.c 可匹配 abc |
\d | 匹配一个数字字符 | \d{4} |
\w | 匹配“单词字符”,通常包括字母、数字和下划线 | \w+ |
\s | 匹配空白字符 | \s* |
[abc] | 匹配方括号中的任意一个字符 | [aeiou] |
[^abc] | 匹配不在方括号中的一个字符 | [^0-9] |
常用量词控制前一个元素出现的次数:* 表示零次或多次,+ 表示一次或多次,? 表示零次或一次;{3} 表示恰好三次,{2,5} 表示两到五次,{2,} 表示至少两次。
<?php
$tests = ['7', '42', '12345', '123456'];
foreach ($tests as $value) {
$ok = preg_match('/^\d{1,5}$/', $value);
echo $value . ': ' . ($ok === 1 ? '符合' : '不符合') . "\n";
}
上例只检查由 1 到 5 个数字组成的字符串。这里的 ^ 和 $ 是行首、行尾锚点,但 $ 在某些情况下也允许匹配字符串末尾换行符之前的位置。若要要求整个输入从开始到绝对结尾都符合,通常使用 \A 和 \z:
<?php
$pattern = '/\A\d{1,5}\z/';
var_dump(preg_match($pattern, '123')); // int(1)
var_dump(preg_match($pattern, "123\n")); // int(0)
这一区别在做输入校验时很重要。只用 ^ 和 $,并不总能表达“整个字符串都必须符合”的严格要求。多行修饰符 m 会改变 ^ 和 $ 的含义,使它们可以匹配每一行的开头和结尾;它与是否让点号匹配换行无关。
点号默认不匹配换行。修饰符 s 会使点号也能匹配换行,但它不改变锚点行为。例如:
<?php
$text = "first\nsecond";
var_dump(preg_match('/first.second/', $text)); // int(0)
var_dump(preg_match('/first.second/s', $text)); // int(1)
正则默认倾向于尽可能多地匹配,这称为贪婪匹配。给量词加上 ? 可以使其尽可能少匹配:
<?php
$text = '<b>one</b><b>two</b>';
preg_match('/<b>.*<\/b>/', $text, $greedy);
preg_match('/<b>.*?<\/b>/', $text, $lazy);
echo $greedy[0] . "\n"; // <b>one</b><b>two</b>
echo $lazy[0] . "\n"; // <b>one</b>
这个例子只用于说明量词行为,不建议用正则解析真实 HTML。HTML 允许嵌套、属性和多种等价写法,简单的点号模式很容易在结构稍有变化时失效。
三、字符类与 Unicode 边界
字符类 [...] 用来限定某一个位置可以出现的字符。范围写法如 [0-9]、[a-z] 和 [A-Z];在字符类开头使用 ^ 表示取反。例如 [^<>] 匹配一个既不是小于号也不是大于号的字符。
<?php
$pattern = '/\A[A-Za-z0-9_-]{3,20}\z/';
foreach (['alice_1', 'a-b', '中文'] as $name) {
echo $name . ': ';
echo preg_match($pattern, $name) === 1 ? "符合\n" : "不符合\n";
}
\w 不应被误解为“任意语言中的文字”。在不启用 Unicode 属性时,它主要按 PCRE 的字母、数字和下划线规则工作;把它用来验证中文姓名或任意 Unicode 用户名,可能得不到预期结果。若确实需要按 Unicode 字母分类,可以启用 u 模式并使用 Unicode 属性,例如 \p{L} 表示 Unicode 字母类别:
<?php
$pattern = '/\A\p{L}+\z/u';
var_dump(preg_match($pattern, '张三')); // int(1)
var_dump(preg_match($pattern, '张三2')); // int(0)
u 表示按 UTF-8 处理模式和主题字符串。输入必须是有效的 UTF-8;无效字节序列可能导致匹配失败并产生错误。启用 u 也不等于完成所有 Unicode 规范化工作:视觉上相同的文本可能使用不同的字符序列,是否需要规范化应由业务规则决定。
\b 是“单词边界”,它根据单词字符与非单词字符的交界判断,并不等于中文里的词语边界。比如用 \bcat\b 查找英文单词 cat 通常合理,但用它分割中文句子往往不是合适的做法。需要中文分词时应使用分词工具或更符合数据结构的处理方式。
四、捕获分组:不只检查,还能提取
圆括号 () 有两个常见用途:把多个元素组合起来,以及捕获匹配到的内容。preg_match() 会把完整匹配和捕获到的分组放进结果数组。
<?php
$text = '订单号:AB-2024-0315';
$pattern = '/([A-Z]{2})-(\d{4})-(\d{4})/';
if (preg_match($pattern, $text, $matches) === 1) {
echo $matches[0] . "\n"; // AB-2024-0315
echo $matches[1] . "\n"; // AB
echo $matches[2] . "\n"; // 2024
echo $matches[3] . "\n"; // 0315
}
数组索引 0 是完整匹配,后续数字索引按捕获分组的顺序排列。分组数量或顺序变化时,数字索引容易受影响;PHP 支持命名捕获组,便于按字段名读取:
<?php
$text = '订单号:AB-2024-0315';
$pattern = '/(?<prefix>[A-Z]{2})-(?<year>\d{4})-(?<serial>\d{4})/';
if (preg_match($pattern, $text, $matches) === 1) {
echo $matches['prefix'] . "\n";
echo $matches['year'] . "\n";
echo $matches['serial'] . "\n";
}
命名分组的常见写法是 (?<name>...);在 PHP 字符串或 HTML 中要注意尖括号的转义。分组还可以不捕获:(?:...) 仍然能将内部内容作为一个整体使用,但不会额外占据捕获组编号。这在只需要控制量词或选择范围时很有用。
<?php
$pattern = '/\A(?:https?|ftp):\/\/[A-Za-z0-9.-]+\z/';
var_dump(preg_match($pattern, 'https://example.com')); // int(1)
在复杂正则中,优先考虑使用非捕获分组,或给需要读取的分组命名。这样能减少因插入新分组而导致编号变化的问题。需要匹配完整文本还是其中一部分,也要说清楚:不加锚点时,preg_match() 默认可以在字符串中找到一段符合模式的内容。
五、preg_match 与 preg_match_all
preg_match() 通常用于查找第一个匹配。要提取所有匹配,可以使用 preg_match_all():
<?php
$text = '联系人:[email protected],[email protected]';
$pattern = '/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/';
$count = preg_match_all($pattern, $text, $matches);
echo "找到 {$count} 个地址\n";
print_r($matches[0]);
preg_match_all() 返回匹配次数,错误时返回 false。它的结果数组受 $flags 参数影响,默认按捕获组组织。提取复杂结构时可以先用上面的例子观察结果,避免想当然地假设数组形状。
示例中的邮箱模式只用于演示提取,不代表完整的邮箱地址标准校验。真实邮箱地址规则比常见的“用户名@域名”模式复杂,域名是否合法、地址是否真实可用,也不是单靠正则就能确认。注册场景通常应做合理的基础格式检查,再通过验证邮件确认可达性。
六、替换与分割
preg_replace() 可将匹配内容替换为指定文本。替换字符串可以引用捕获组,例如 $1 表示第一个捕获组;使用 ${1} 能在紧接数字时明确边界。
<?php
$text = '2024-03-15';
$pattern = '/\A(\d{4})-(\d{2})-(\d{2})\z/';
$replacement = '${2}/${3}/${1}';
$result = preg_replace($pattern, $replacement, $text);
echo $result; // 03/15/2024
preg_replace() 通常返回替换后的字符串;发生错误时返回 null。可以通过可选的第四个参数获得替换次数:
<?php
$text = 'cat dog cat';
$count = 0;
$result = preg_replace('/cat/', 'fox', $text, -1, $count);
echo $result . "\n"; // fox dog fox
echo $count . "\n"; // 2
第三个参数可以是字符串或字符串数组;限制替换次数的参数默认是不限次数,使用时应根据实际需求控制。不要把用户提供的替换文本直接当作正则模式,也不要忽略返回值。若用户输入的内容只是要被当作普通文本查找,应先使用 preg_quote() 转义模式元字符。
<?php
$needle = 'price.10';
$text = 'price.10 priceX10';
$pattern = '/' . preg_quote($needle, '/') . '/';
echo preg_replace($pattern, '[命中]', $text);
// [命中] priceX10
preg_quote() 的第二个参数可以传入所用的分隔符,让该分隔符也被转义。它适合把字面文本安全地嵌入正则模式,但不应被理解成通用的输出编码函数,也不能替代 HTML、SQL 或命令行场景各自需要的安全处理。
preg_split() 根据模式拆分字符串,适合分隔规则不只是单个固定字符的场景:
<?php
$line = 'apple, orange; pear';
$parts = preg_split('/[,;]\s*/', $line);
print_r($parts);
如果只需按固定字符串分隔,explode() 往往更简单;正则并不会因为功能更强就总是更合适。
七、PHP 字符串引号与反斜杠
正则模式通常包含大量反斜杠,而 PHP 源码本身也会解析字符串转义。新手常遇到的问题是:看到正则里的 \d,就机械地写成双引号字符串中的 "\\d"。双引号字符串会处理变量替换和部分反斜杠转义;单引号字符串的转义规则更少,写正则时通常较直观。
<?php
$pattern1 = '/\d+/'; // 单引号字符串,常用写法
$pattern2 = "/\\d+/"; // 双引号字符串,需要为 PHP 转义反斜杠
var_dump(preg_match($pattern1, '123'));
var_dump(preg_match($pattern2, '123'));
单引号字符串并非“任何内容都原样保留”:其中的反斜杠和单引号仍有特定转义规则。构造动态模式时,不要靠拼接来猜测层层转义是否正确;先明确字符串最终传给 PCRE 的内容,并用小样本测试。
八、边界与常见错误
1. 混淆字符匹配与格式校验
/\d{4}/ 可以在 abc1234xyz 中找到四位数字,但它不能证明整个字符串只有四位数字。做完整输入校验时应加上适当锚点,通常用 \A 和 \z。
2. 忘记转义点号
. 默认匹配任意非换行字符。匹配邮箱地址或域名中的字面点号时,应写 \.;否则模式可能意外接受其他字符。
3. 使用错误的字符范围
[0-9] 是数字字符范围,[A-z] 却不是“所有大小写英文字母”:ASCII 中 Z 与 a 之间还包含方括号等符号。需要英文字母时写 [A-Za-z]。
4. 以为忽略大小写会处理所有语言规则
修饰符 i 用于忽略大小写,但跨语言的大小写转换和特殊字符规则并不总等同于简单的 ASCII 大小写比较。涉及多语言文本时,需要结合 Unicode 模式和业务要求验证测试样例。
5. 忽略错误返回值
模式语法错误、无效 UTF-8 输入等情况可能使函数返回错误值。若把 false 当成“没有匹配”继续执行,问题往往会被掩盖。对关键操作应显式检查返回值,并记录必要的错误信息。
<?php
$result = preg_match('/[/', 'anything');
if ($result === false) {
echo '匹配失败,PCRE 错误码:' . preg_last_error() . "\n";
}
preg_last_error() 返回最近一次 PCRE 操作的错误码。若运行环境支持,也可以通过 preg_last_error_msg() 获取更易读的说明;该函数在 PHP 8.0 起提供。排查时应在失败后立即读取错误信息,避免被后续正则调用覆盖。
6. 让量词造成过多回溯
诸如 (a+)+ 这样的嵌套重复,在某些不匹配输入上可能让回溯量急剧增长。外部输入很长、模式结构复杂时,需关注这种风险,避免使用大量嵌套的贪婪量词。可以通过明确字符范围、缩小输入长度、重新设计模式或使用合适的原子结构来降低不必要的回溯。
PCRE 有回溯与递归深度等资源限制,具体数值受 PHP 和 PCRE 版本、运行环境及配置影响,不应依赖某个固定默认值。PHP 的 pcre.backtrack_limit 和 pcre.recursion_limit 等配置会影响执行限制;调整上限不是修复低效模式的替代方案。处理不可信输入时,应同时控制输入长度、审查模式复杂度,并对失败和资源耗尽做好处理。
九、正则与安全:能做什么,不能做什么
正则可作为输入格式检查的一部分,例如检查日期文本是否符合某种书写形状、用户名是否只包含允许字符。但“格式符合”不等于“业务值有效”。例如 99-99-9999 可能满足某个日期外形,却不是有效日期;数值范围、日期合法性和业务约束仍应由专门逻辑验证。
正则也不能代替 HTML 转义、SQL 参数化或命令执行安全措施。检查输入中“不包含某个危险字符串”通常不足以构成可靠的安全策略;不同上下文有不同的语法和转义规则。输出 HTML 时使用适当的输出编码,访问数据库时使用预处理语句,执行系统命令时尽量避免拼接命令。
同样,不要用简单正则来解析任意 HTML。HTML 有嵌套结构、注释、属性、实体和格式差异,用正则做非常有限的已知片段提取或许可行,但不能保证完整解析。需要遍历或修改 HTML 文档时,应使用 PHP 的 DOM 等解析工具。
十、一个完整的输入检查示例
下面示例检查一个由英文字母、数字、下划线和连字符组成的标识符,长度为 3 到 20 个字符。它先检查整个字符串,再处理正则错误;长度和允许字符都是示例业务规则,不是通用用户名标准。
<?php
function isValidIdentifier(string $value): bool
{
$pattern = '/\A[A-Za-z0-9_-]{3,20}\z/';
$result = preg_match($pattern, $value);
if ($result === false) {
throw new RuntimeException(
'正则执行失败,错误码:' . preg_last_error()
);
}
return $result === 1;
}
$inputs = ['alice_1', 'ab', 'name with space', 'valid-name'];
foreach ($inputs as $input) {
echo $input . ': ';
echo isValidIdentifier($input) ? "有效\n" : "无效\n";
}
把规则写进模式之前,先用自然语言明确边界:是否允许空字符串,长度按字节还是字符计算,是否允许非 ASCII 字符,是否需要大小写不敏感,输入是否可能含换行。正则本身不能替你决定这些业务问题。
十一、编写和调试正则的实用步骤
- 先描述输入规则。明确要匹配的字符、顺序、次数,以及是否要求整串匹配。
- 从最小模式开始。先验证固定部分,再逐步添加字符类、量词和分组。
- 准备正反例。测试正常输入、边界长度、额外前后缀、空字符串、换行和非 ASCII 文本。
- 检查返回值与捕获结果。用
var_dump()查看数组结构,确认分组索引与预期一致。 - 处理动态文本。用户输入若作为字面查找内容嵌入模式,使用
preg_quote()并指定分隔符。 - 限制复杂度。避免不必要的嵌套重复,限制输入长度,对不可信数据测试最坏情况。
正则最适合表达边界明确、规模适中的文本模式。掌握分隔符、字符类、量词、锚点和捕获组后,许多常见的检查与提取任务都能清楚实现;遇到嵌套语法、复杂 Unicode 规则或安全敏感的业务判断时,则应让专用解析器和业务逻辑承担相应工作。



