Skip to content

正则模式修饰符

概述

PCRE 模式修饰符(Modifiers)用于改变正则表达式的匹配行为。修饰符附加在正则表达式分隔符之后,可以组合使用。本章详细讲解每个修饰符的含义、使用场景以及注意事项。

基础概念

修饰符速查表

修饰符名称含义
iPCRE_CASELESS不区分大小写
mPCRE_MULTILINE多行模式(^$ 匹配每行)
sPCRE_DOTALL. 匹配包括换行符在内的所有字符
xPCRE_EXTENDED忽略空白和注释
uPCRE_UTF8启用 UTF-8 模式
APCRE_ANCHORED锚定模式(从开头匹配)
DPCRE_DOLLAR_ENDONLY$ 仅匹配字符串末尾
JPCRE_INFO_JCHANGED允许重复命名子模式(PHP 7.2+)

语法与代码

i — 不区分大小写

php
<?php

declare(strict_types=1);

preg_match('/hello/i', 'HELLO', $match);     // 匹配
preg_match('/hello/i', 'Hello World', $match); // 匹配

// HTTP 方法匹配
if (preg_match('/^(get|post|put|delete)$/i', 'GET')) {
    echo '合法的 HTTP 方法';
}

m — 多行模式

php
<?php

declare(strict_types=1);

$text = "line1\nline2\nline3";

// 不使用 m:^ 只匹配字符串开头
preg_match_all('/^line\d/', $text, $matches);
// 只匹配 'line1'

// 使用 m:^ 匹配每行开头
preg_match_all('/^line\d/m', $text, $matches);
// 匹配 'line1', 'line2', 'line3'

// $ 同理
preg_match_all('/line\d$/m', $text, $matches);
// 匹配 'line1', 'line2', 'line3'

s — 单行模式(DOTALL)

php
<?php

declare(strict_types=1);

$html = "<div>\n    content\n</div>";

// 不使用 s:. 不匹配换行符
preg_match('/<div>(.*?)<\/div>/', $html, $match); // 不匹配

// 使用 s:. 匹配换行符
preg_match('/<div>(.*?)<\/div>/s', $html, $match);
// 匹配整个 <div>...</div>

x — 扩展模式(添加注释)

php
<?php

declare(strict_types=1);

// x 修饰符 - 忽略模式中的空白和 # 注释
$pattern = '/
    ^(?P<protocol>https?)  # 协议
    :\/\/                  # ://
    (?P<host>[^\/]+)       # 主机名
    (?P<path>\/.*)?        # 路径(可选)
$/x';

preg_match($pattern, 'https://example.com/path', $matches);
echo $matches['protocol'];  // https
echo $matches['host'];      // example.com

u — UTF-8 模式

php
<?php

declare(strict_types=1);

// 处理中文等 Unicode 字符必须使用 u 修饰符
preg_match('/^[\p{Han}]+$/u', '你好世界', $match);  // true

// 中文姓名验证
preg_match('/^[\p{Han}]{2,4}$/u', '张三', $match);   // true
preg_match('/^[\p{Han}]{2,4}$/u', '欧阳修', $match);  // true

// Emoji 匹配
preg_match('/[\x{1F600}-\x{1F64F}]/u', 'Hello 😀', $match); // true

内联修饰符

php
<?php

declare(strict_types=1);

// (?i) 启用,(?-i) 禁用
preg_match('/(?i)hello(?-i) world/', 'HELLO world', $match);  // 匹配
preg_match('/(?i)hello(?-i) world/', 'hello WORLD', $match);  // 不匹配

// (?s) DOTALL,(?m) 多行
// 组合使用
preg_match('/(?ism)^hello.world$/m', "hello\nworld", $match);

A — 锚定模式 & D — $ 仅匹配末尾

php
<?php

declare(strict_types=1);

// A 修饰符 - 隐式添加 ^
preg_match('/hello/A', 'hello world', $match);  // 匹配
preg_match('/hello/A', 'say hello', $match);    // 不匹配

// D 修饰符 - $ 仅匹配真正的字符串末尾
preg_match('/world$/', "world\n", $match);     // 匹配(\n 前)
preg_match('/world$/D', "world\n", $match);    // 不匹配
preg_match('/world$/D', "world", $match);      // 匹配

详细说明

修饰符组合

php
<?php

// 常用组合
'/pattern/i'    // 不区分大小写
'/pattern/msu'  // 多行 + DOTALL + UTF-8(HTML/XML 中文处理)
'/pattern/ix'   // 不区分大小写 + 扩展模式(可加注释)
'/pattern/su'   // DOTALL + UTF-8(中文多行文本)

PCRE2 特性(PHP 7.3+)

PHP 7.3+ 使用 PCRE2 库,支持更丰富的 Unicode 属性和字符类。

修饰符选择建议

  • 中文处理加 u
  • HTML 处理加 s
  • 多行文本加 m
  • 复杂正则加 x
  • 通常组合使用 /msu

实战示例

多行日志解析

php
<?php

declare(strict_types=1);

function parseLogLines(string $log): array
{
    $lines = preg_split('/\r?\n/', $log, -1, PREG_SPLIT_NO_EMPTY);
    $pattern = '/^(?P<level>DEBUG|INFO|WARN|ERROR)\s+(?P<time>[\d\-: ]+)\s+(?P<message>.+)$/';

    $results = [];
    foreach ($lines as $line) {
        if (preg_match($pattern, trim($line), $match)) {
            $results[] = [
                'level' => $match['level'],
                'time' => trim($match['time']),
                'message' => $match['message'],
            ];
        }
    }
    return $results;
}

$log = "INFO  2024-01-15 10:00:00 Service started
ERROR 2024-01-15 10:05:00 DB connection failed";
print_r(parseLogLines($log));

PHP 8.2 修饰符变更

php
<?php

declare(strict_types=1);

// PHP 8.2: $ 锚点不再匹配字符串末尾的换行符
// 以前:/test$/ 匹配 "test\n"
// PHP 8.2+:/test$/ 不匹配 "test\n"(需要 /test$/D 或 /\z/)

$text = "test\n";

// PHP 8.2+
preg_match('/test$/', $text, $match);    // 不匹配
preg_match('/test\z/', $text, $match);   // 不匹配
preg_match('/test$/D', $text, $match);   // 不匹配
preg_match('/test\Z/', $text, $match);   // 匹配(\Z 匹配末尾或末尾 \n 之前)

注意事项

u 修饰符与中文

处理 UTF-8 字符串时,务必使用 u 修饰符。缺少它会导致正则表达式按字节匹配中文字符,产生错误结果。

\A 和 \z 不受 m 影响

\A\z 始终匹配字符串的开头和结尾,不受 m 修饰符影响。需要始终匹配字符串边界时使用 \A\z

最佳实践

  1. 中文加 u:所有涉及中文的正则都加 u
  2. HTML 加 s:跨行匹配 HTML 标签
  3. 多行加 m:逐行匹配
  4. 复杂正则加 x:添加注释提高可维护性
  5. 显式指定修饰符:不依赖默认行为

修饰符组合实例

php
<?php

declare(strict_types=1);

// i + u - 不区分大小写 + UTF-8
preg_match('/hello/iu', '你好HELLO世界', $match);  // 匹配 HELLO

// s + u - DOTALL + UTF-8(多行中文匹配)
$html = "<div>你好\n世界</div>";
preg_match('/<div>(.*?)<\/div>/su', $html, $match);
// 匹配成功

// m + s + u - 多行 + DOTALL + UTF-8
$text = "<div>\nContent\n</div>\n<span>\nText\n</span>";
preg_match_all('/<(div|span)>(.*?)<\/\1>/msu', $text, $matches, PREG_SET_ORDER);
// 两个匹配

// x + u - 扩展模式 + UTF-8(中文正则添加注释)
$pattern = '/
    ^                         # 字符串开头
    [\p{Han}]+                # 中文名(2-4个字)
    [\s]                      # 空格
    [\p{Latin}]+               # 英文名
    $                         # 字符串结尾
/xu';

preg_match($pattern, '张三 Alice', $match);  // 匹配

内联修饰符详解

php
<?php

declare(strict_types=1);

// (?i) 启用不区分大小写
// (?-i) 禁用不区分大小写
// 作用范围:从当前位置到所在组结束或整个模式

// 示例:只有 URL 部分不区分大小写
$pattern = '/href=(?i)(https?:\/\/[^\s"]+)(?-i)\s/';
preg_match($pattern, 'href=HTTPS://EXAMPLE.COM ok', $match);
// 匹配(只有 URL 部分忽略大小写)

// (?s) 启用 DOTALL
// (?-s) 禁用 DOTALL
$pattern = '/<p>(?s)(.*?)<\/p>/';
preg_match($pattern, "<p>Line1\nLine2</p>", $match);

// (?m) 启用多行模式
// (?-m) 禁用多行模式

// (?x) 启用扩展模式
// (?-x) 禁用扩展模式
$pattern = '/
    \d+    (?# 数字)
    [a-z]+  (?# 小写字母)
/x';

// (?J) 允许重复命名组(PHP 7.2+)
$pattern = '/(?J)(?P<word>\w+)\s+(?P<word>\w+)/';
preg_match($pattern, 'hello world', $match);
// $match['word'] 可能是 'world'(后面的覆盖前面的)
// 不推荐使用,仅在极特殊场景有用

修饰符与编码

php
<?php

declare(strict_types=1);

// u 修饰符对不同编码的影响
$utf8Text = '你好世界 Hello 世界你好';

// 不使用 u - 按字节匹配(错误)
preg_match_all('/\w+/', $utf8Text, $matches);
// 可能返回乱码

// 使用 u - 按 UTF-8 字符匹配(正确)
preg_match_all('/\w+/u', $utf8Text, $matches);
// ['你好世界', 'Hello', '世界你好']

// 注意:\w 在 u 模式下只匹配 ASCII 单词字符
// 要匹配所有语言的"单词",使用 \p{L}\p{N}_
preg_match_all('/[\p{L}\p{N}_]+/u', $utf8Text, $matches);
// ['你好世界', 'Hello', '世界你好']

// u 修饰符下的 . 可以匹配多字节字符
preg_match('/./u', '你', $match);  // 匹配 '你'(一个中文字符)

修饰符性能影响

php
<?php

declare(strict_types=1);

// 不同修饰符对性能的影响
// u 修饰符:轻微性能开销(UTF-8 引擎)
// i 修饰符:几乎无开销
// s 修饰符:无开销
// m 修饰符:几乎无开销
// x 修饰符:解析时开销(运行时无影响)
// A 修饰符:可能略微加速(提前终止)

// 性能敏感场景建议:
// 1. 纯 ASCII 文本不使用 u
// 2. 能用 str_replace 解决的不用 preg_replace
// 3. 复杂正则使用 A 锚定
// 4. 多行处理避免使用 m,改用 \A 和 \z

PHP 8.2 修饰符变更

PHP 8.2 中,$ 锚点在非多行模式下不再匹配字符串末尾的 \n。此行为变更影响了 $ 的默认行为,与 D 修饰符类似。

m 修饰符与 $

使用 m 修饰符时,$ 匹配每行末尾(在 \n 之前)。如果需要严格匹配字符串末尾,使用 \z 而非 $

修饰符在 VitePress 中的使用

php
<?php

declare(strict_types=1);

// 在处理 Markdown 内容时,常用修饰符组合

// 解析代码块(需要 s + u)
$markdown = "```php\nHello 世界\n```";
preg_match('/```(\w+)\n(.*?)\n```/su', $markdown, $match);

// 解析 frontmatter(需要 s + m)
$content = "---\ntitle: Test\n---\n\nContent here";
preg_match('/^---\s*\n(.*?)\n---\s*\n(.*)$/s', $content, $match);

修饰符与 preg_match_all

php
<?php

declare(strict_types=1);

// 多行日志解析使用 m + u
$log = "[2024-01-15] ERROR: Database failed\n[2024-01-15] INFO: Service started";

preg_match_all(
    '/^\[([^\]]+)\]\s+(\w+):\s+(.+)$/mu',
    $log,
    $matches,
    PREG_SET_ORDER
);

foreach ($matches as $m) {
    echo "[{$m[1]}] {$m[2]}: {$m[3]}\n";
}

常用修饰符组合速记

  • 中文文本:/u
  • 多行文本:/mu
  • HTML/XML:/su
  • 中文多行 HTML:/msu
  • 复杂正则(带注释):/xu
  • 大小写不敏感:/iu

参考链接