在 PHP 中使用 preg_* 系列函数处理中文、日文、emoji 等多语言文本时,默认的 PCRE 引擎按单字节解析,会直接导致多字节 UTF-8 字符被拆碎、匹配失败或乱匹配。开启 Unicode 模式(/u 修饰符)后,PCRE 按 UTF-8 编码单元处理,并结合 \p{} / \P{} 语法才能正确命中多语言内容。

为什么要开 /u
PHP 的 preg_* 底层是 PCRE 库,未加 /u 时:
-
.只匹配单字节,一个中文汉字占 3 字节,会被拆成 3 次匹配 -
\w、\d、\s只认 ASCII 范围,中文、全角数字、全角空格全部不匹配 -
字符类
[一-龥]这种”汉字区间”写法依赖具体编码顺序,UTF-8 下不可靠且覆盖不全
// 不加 /u:匹配不到
preg_match('/中文/', '中文测试', $m); // ✅ 纯字面量碰巧能中(字节一致)
preg_match('/^.$/', '中', $m); // ❌ . 只吃到 1 字节
preg_match('/^\w+$/', '中文test', $m); // ❌ 中文部分不认// 加 /u 后
preg_match('/^.$/u', '中', $m); // ✅ $m[0] = "中"
preg_match('/^\w+$/u', '中文test', $m); // ❌ \w 仍只认 ASCII,还是失败注意:
/u只解决”按 UTF-8 逐字符走”,不解决”\w 是否包含汉字”——后者要靠\p{}。
/u 修饰符的基本用法
把 u 加在定界符后面即可,和其他修饰符(i、s)叠加无冲突:
$text = 'PHP 是世界上最好的语言?';
if (preg_match('/^PHP(.+)$/u', $text, $m)) {
echo $m[1]; // " 是世界上最好的语言?"
}前置条件:传入的 $text 本身必须是合法 UTF-8,否则 PCRE 会报 PCRE_ERROR_BADUTF8,preg_last_error() 返回 PREG_BAD_UTF8_ERROR。生产代码里建议先用 mb_check_encoding($text, 'UTF-8') 兜底。
Unicode 属性 \p{} 才是多语言主力
/u 开了之后,PCRE 支持 Unicode Character Properties,语法:
-
\p{Category}—— 匹配某类字符 -
\P{Category}—— 反向(大写 P) -
\X—— 匹配一个”扩展字形簇”,能完整吃掉 emoji 组合(👨👩👧👦 这种 ZWJ 拼接的)
常用类别对照:
|
写法 |
含义 |
|---|---|
|
|
任意语言的字母(含中文、日文、阿拉伯文等) |
|
|
任意文字的数字(含全角 123) |
|
|
汉字(CJK Unified Ideographs) |
|
|
平假名 |
|
|
片假名 |
|
|
货币符号(¥ $ €) |
|
|
Emoji(PCRE 10+ / PHP 7.4+ 才稳) |
|
|
所有空白分隔符(含全角空格、NBSP) |
实战:提取字符串里的汉字
$text = 'Tom 说:「こんにちは、世界!」然后笑了😄';
preg_match_all('/\p{Han}+/u', $text, $m);
print_r($m[0]);
// ["说", "然后笑了"]比老的 /[\x{4e00}-\x{9fa5}]/u 区间写法覆盖更全(生僻字、扩展 B~G 区都吃得到)。
实战:混语文本里只留”字母 + 数字 + 空格”
$text = 'ABC 123 中文 🎌';
$clean = preg_replace('/[^\p{L}\p{N}\p{Z}]+/u', '', $text);
echo $clean; // "ABC 123 中文 "(emoji 和 🎌 被干掉,全角数字因 \p{N} 保留)这里
\p{Z}能吞掉全角空格(U+3000),比\s在/u下更宽(\s在 Unicode 模式也只扩了一点点,不全)。
几个容易踩的坑
1. emoji 不是单个 \p{L}
普通 emoji ✅ 是 1 码点,但 👨👩👧👦、🏳️🌈 这种是多个码点用 ZWJ(U+200D)拼的。\p{Emoji} 只匹配其中一段,想整段吞用 \X:
$text = '一家👨👩👧👦去公园';
preg_match_all('/\X/u', $text, $m);
// 每个元素是一个"视觉字符",👨👩👧👦 不会被拆2. 中文标点不在 \w
即便开了 /u,\w = [A-Za-z0-9_],汉字也不在内。想”单词”含汉字,得自己拼:
// 近似"多语言单词":字母 / 汉字 / 数字 / 下划线
preg_match_all('/[\p{L}\p{Han}\p{N}_]+/u', $text, $m);3. u 和 s 一起用时,. 仍不吃 \n 以外的垂直空白?
s 让 . 吃 \n,但 Unicode 里还有 \r、\v 等——不过这个和 /u 无关,是 PCRE 的老行为,记一下就行。
4. 性能
\p{Han} 这类属性匹配比区间 [\x{4e00}-\x{9fa5}] 略慢一点(PCRE 要查 Unicode 表),但差的是纳秒级,换来的是正确性,没必要省。
什么时候不用 /u
-
确定文本就是纯 ASCII(比如邮箱校验、URL 校验),开了也没坏处但无意义
-
超大文本 + 高频循环场景,可以用
mb_ereg_*或先mb_split拆块再preg_,避免单次正则把 UTF-8 逐字节扫太重
常见组合速查
// 整串是否纯汉字
preg_match('/^\p{Han}+$/u', $s);
// 是否含中日韩(CJK 统一表意 + 扩展 A~G,\p{Han} 已覆盖)
preg_match('/\p{Han}/u', $s);
// 去掉所有 emoji(PCRE 支持 \p{Emoji} 的版本)
$noEmoji = preg_replace('/\p{Emoji}/u', '', $s);
// 拆成"视觉字符"数组(等效 mb_str_split 但能正确处理 ZWJ emoji)
preg_match_all('/\X/u', $s, $m);
$chars = $m[0];PHP 正则处理多语言文本的核心就两条:/u 保证按 UTF-8 走,\p{} 保证按 Unicode 属性认字。剩下的是按业务挑类别(Han / Hiragana / N / Z)拼pattern,比硬写码点区间稳得多。

