
前言字符串是 PHP 里使用频率最高的类型但「会用」和「用对」之间差距很大。几个典型症状用substr()截取中文标题结果末尾出现乱码方块strpos()明明找到了却判成没找到因为返回的是0升级到 PHP 8.2 之后日志里突然刷出Deprecated: Using ${var} in strings is deprecatedexplode(, $s)直接抛异常中断脚本。这些问题的共同点是PHP 的字符串本质是字节序列byte string而不是字符序列。所有单字节函数strlen、substr、str_split、strtoupper都在字节层面工作遇到 UTF-8 中文就会出错——一个汉字占 3 个字节substr($s, 0, 5)很容易切在字符中间产生无效的字节序列。本文按「字面量与插值 → 常用函数分类 → 中文的正确处理 → PHP 8.2 的相关变化」四部分展开最后给出一份可直接运行的示例脚本最低PHP 8.2。需要先把版本对应清楚避免在老版本上写出跑不起来的代码str_contains()、str_starts_with()、str_ends_with()是PHP 8.0引入的不是 8.2mb_str_split()是PHP 7.4引入的真正属于PHP 8.2的字符串相关变化是${}插值被弃用、utf8_encode()/utf8_decode()被弃用以及strtolower()等大小写函数不再受区域设置locale影响。标题里的 PHP 8.2 指的是运行环境版本文中会逐个标注每个特性的真实引入版本。一、四种字面量与插值规则写法变量插值转义序列适用场景单引号...不解析只认\和\\绝大多数固定字符串性能最好双引号...解析认\n、\t、\x41、\u{...}等需要插值或转义时HeredocEOT解析同双引号多行文本、SQL、HTML 模板NowdocEOT不解析无多行且含大量$的文本正则、模板双引号里的插值有两种写法其中一种已经弃用?php declare(strict_types1); $name world; echo hello $name\n; // ✅ 简单插值最常用 echo hello {$name}\n; // ✅ 花括号形式能处理表达式与数组下标 echo hello ${name}\n; // ❌ PHP 8.2 起弃用Deprecated: Using ${var} in strings${name}这种写法在 PHP 8.2 中被标记为弃用原因是它与可变变量语法容易混淆。迁移成本极低把${name}换成{$name}即可但如果字符串拼在 SQL 或模板里要留意它是否出现在被转义的上下文中。{$...}的真正价值在于处理复杂表达式$user [name Ada, tags [a, b]]; echo 名字: {$user[name]}第一个标签: {$user[tags][0]}\n; // 如果写 $user[name] 会直接报语法错误二、常用函数按用途分类用途单字节函数对应多字节版本备注求长度strlenmb_strlen中文必须用后者截取substrmb_substr前者会切出半个汉字查找位置strpos/strrposmb_strpos/mb_strrpos返回 0 时是「找到了」是否包含str_contains8.0无需多字节版本按字节比较UTF-8 下结果通常正确前缀/后缀str_starts_with/str_ends_with8.0同上同上替换str_replace/strtr无需多字节版本按字节替换UTF-8 下通常安全大小写strtolower/strtouppermb_strtolower/mb_strtoupper前者只处理 ASCII首字母大写ucfirst/ucwordsmb_convert_caseMB_CASE_TITLE分割explode—分隔符不能为空串按长度切分str_splitmb_str_split7.4中文必须用后者补齐str_pad需自己封装按字节补齐中文会错反转strrev—会把 UTF-8 字节序打乱格式化sprintf/number_format—sprintf不认宽字符宽度HTML 转义htmlspecialchars—输出到 HTML 必用换行转换nl2br—只处理\n、\r\n关于「按字节 vs 按字符」的一个关键认识如果操作是「查找子串」或「替换子串」并且子串本身是完整的 UTF-8 序列那么单字节函数通常也能得到正确结果因为 UTF-8 的设计保证了多字节序列不会与 ASCII 字节混淆不存在某个汉字的某个字节等于a的情况。所以str_contains($title, 教程)是安全的。而「按长度截取」和「按长度切分」必然出错因为长度被按字节算。三、中文处理必须用 mb_* 的地方?php declare(strict_types1); $title PHP 字符串基础教程; echo strlen($title), PHP_EOL; // 按字节约 26 echo mb_strlen($title, UTF-8), PHP_EOL; // 按字符11 左右 // ❌ 可能切在字符中间产生乱码 echo substr($title, 0, 8), PHP_EOL; // ✅ 按字符安全截取 echo mb_substr($title, 0, 8, UTF-8), PHP_EOL;mb_*系列函数的最后一个参数是编码。不要依赖mb_internal_encoding()的默认值HTTP 请求、CLI 脚本、定时任务三种环境下它可能不一致显式传UTF-8是唯一稳妥的做法。补齐宽度也是同样的道理。str_pad()按字节补齐中文列会完全对不齐?php function mb_pad(string $s, int $len, string $pad , int $type STR_PAD_RIGHT): string { $cur mb_strlen($s, UTF-8); if ($cur $len) { return $s; } $need $len - $cur; // 注意这里假定 $pad 是单个字符多字符填充符需要另算 switch ($type) { case STR_PAD_LEFT: return str_repeat($pad, $need) . $s; case STR_PAD_BOTH: $left intdiv($need, 2); $right $need - $left; return str_repeat($pad, $left) . $s . str_repeat($pad, $right); default: return $s . str_repeat($pad, $need); } }较新版本的 PHP 提供了mb_str_pad()在 8.2 上仍需自己封装或者用上面的写法。四、可运行的综合示例下面这份脚本把常用的字符串操作都串了一遍保存为strings.php直接运行即可单文件、无依赖?php declare(strict_types1); // 需要 PHP 8.2 // ---------- 1. 字面量与插值 ---------- $name Ada; echo 单引号: $name 不会被解析, PHP_EOL; echo 双引号: 你好, {$name}, PHP_EOL; // ---------- 2. 查找类 ---------- $haystack PHP 8.2 字符串基础; var_dump(str_contains($haystack, 字符串)); // bool(true)PHP 8.0 var_dump(str_starts_with($haystack, PHP)); // bool(true) var_dump(str_ends_with($haystack, 基础)); // bool(true) // 经典陷阱strpos 返回 0 表示在开头找到不能用 if (!$pos) 判断 $pos strpos($haystack, PHP); if ($pos ! false) { echo strpos 找到位置 {$pos}, PHP_EOL; } // ---------- 3. 截取与长度中文必须用 mb_* ---------- $text PHP 字符串基础教程; printf(strlen%d mb_strlen%d\n, strlen($text), mb_strlen($text, UTF-8)); printf(mb_substr(0,9)%s\n, mb_substr($text, 0, 9, UTF-8)); // ---------- 4. 分割与合并 ---------- $csv apple,banana,cherry; $parts explode(,, $csv); echo implode( | , $parts), PHP_EOL; // 按「字符」切分中文str_split 会切坏 var_dump(mb_str_split(中文, 1, UTF-8)); // [中, 文] // ---------- 5. 替换 ---------- // strtr 做多组替换时是一次扫描完成的不会出现替换结果被二次替换 $map [甲 A, 乙 B]; echo strtr(甲和乙, $map), PHP_EOL; // A和B // ---------- 6. HTML 转义输出到页面时必做 ---------- $danger scriptalert(x)/script; echo htmlspecialchars($danger, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, UTF-8), PHP_EOL; // ---------- 7. 格式化 ---------- printf(%-12s|%s\n, name, value); echo number_format(1234567.891, 2), PHP_EOL; // 1,234,567.89 echo sprintf(%05.1f, 3.14159), PHP_EOL; // 003.1 // ---------- 8. 正则处理 UTF-8 要加 u 修饰符 ---------- $subject 订单号AB-12345数量3; if (preg_match(/数量(\d)/u, $subject, $m) 1) { echo 数量 {$m[1]}, PHP_EOL; } // ---------- 9. 大小写 ---------- echo strtoupper(php8), PHP_EOL; // PHP8只处理 ASCII echo mb_strtoupper(café, UTF-8), PHP_EOL; // CAFÉ // ---------- 10. 清理 ---------- $dirty \t 内容 \n; // 注意trim 的默认字符集包含 \t \n \r \0 \x0B会顺手去掉 \0 var_dump(trim($dirty));五、PHP 8.2 相关的字符串变化变化版本处理方式${var}插值弃用8.2改成{$var}utf8_encode()/utf8_decode()弃用8.2改用mb_convert_encoding()strtolower()/strtoupper()/ucfirst()/ucwords()/lcfirst()不再受 locale 影响8.2行为更可预测但不处理非 ASCIIstr_contains()/str_starts_with()/str_ends_with()8.0替代strpos ! false的写法mb_str_split()7.4按字符切分utf8_encode()的迁移有个细节它的作用是把 ISO-8859-1 转成 UTF-8而不是「校验 UTF-8」。如果原来的代码是在做编码转换直接换成mb_convert_encoding($s, UTF-8, ISO-8859-1)如果原本只是想「确保是 UTF-8」那么更应该用mb_check_encoding($s, UTF-8)做校验或者用mb_convert_encoding($s, UTF-8, UTF-8)做清洗——这两件事的语义完全不同换错会得到乱码。关于大小写函数的变化实践中的影响是这样的PHP 8.2 起它们只做 ASCII 范围内的转换不再随系统的 locale 变化。这修掉了「同一份代码在不同服务器上大小写转换结果不一致」的老问题但不要因此期待它能处理中文或带重音的拉丁字母——那些仍然需要mb_strtoupper()。常见坑点1. 用if (!$pos)判断strpos❌if (strpos($s, $needle)) { /* 找到了 */ }✅if (strpos($s, $needle) ! false) { /* 找到了 */ }strpos在开头找到时返回00是假值判断会被跳过。这是 PHP 里最经典的陷阱之一而且症状是「只有特定数据出问题」极难复现。2. 用str_contains判断非空❌if (str_contains($input, )) { /* 认为看到了什么 */ }✅ 先用if ($input ! )判断空值。str_contains($s, )对任何字符串都返回true空串是任何字符串的子串。用它做「有没有内容」的判断会全部通过。3. 用explode传空分隔符❌explode(, $str)✅str_split($str)或对中文用mb_str_split($str, 1, UTF-8)。PHP 8.0 起空分隔符会直接抛ValueError脚本中断。这个错误的产生往往是复制粘贴时把explode( , ...)的空格弄丢了代码看起来完全正常。4. 用substr截取中文标题❌$short substr($title, 0, 20) . ...;✅$short mb_substr($title, 0, 20, UTF-8) . ...;按字节截 20 位意味着只截到 6 个多汉字更糟的是可能切在字符中间末尾出现?或方块。如果这个字符串要写进 JSONjson_encode会因为无效 UTF-8 而失败。5. 用str_pad对齐中文表格❌str_pad($name, 20) . $value✅ 用mb_strlen自己封装或按显示宽度东亚宽字符算 2 列计算。用于终端输出的对齐表中文列会整体歪掉而且每个汉字歪的程度一致看起来像是「列宽参数写错了」。6. 用str_replace做多组替换却依赖顺序❌ 用一串str_replace依次替换结果后面的替换把前面刚替换进去的内容又改了。 ✅ 用strtr($str, $map)它是一次扫描。str_replace([a,b], [b,c], a)会先得到b再把这个b换成c如果是数组形式替换是顺序执行的。strtr不会产生这种连锁反应。7. 正则忘了加u修饰符❌preg_match(/[\w]/, 中文)—— 按字节匹配中文被拆碎。 ✅preg_match(/[\w]/u, 中文)u修饰符让 PCRE 按 UTF-8 解析模式与被匹配串。漏掉它时.匹配的是单个字节量词{2,4}算的也是字节结果要么匹配失败要么匹配到半个字符。8. 用 trim 后惊讶于数据里出现了意外变化❌ 以为trim()只去掉空格。 ✅ 记住它的默认字符集是 \t\n\r\0\x0B包含 NUL 字节和垂直制表符。从二进制协议或加密数据里读出来的字符串如果末尾恰好有\0trim()会把它悄悄删掉导致长度校验或哈希校验失败。总结场景正确做法常见错误固定字符串单引号双引号无谓的解析开销插值{$var}${var}8.2 起弃用长度mb_strlen($s, UTF-8)strlen按字节算截取中文mb_substrsubstr可能切出无效字节切分中文mb_str_split7.4str_split按字节切判断包含str_contains8.0if (strpos(...))判断前缀后缀str_starts_with/str_ends_with8.0substr($s,0,n) 分割字符串explode分隔符非空explode()抛异常多组替换strtr链式str_replace输出到 HTMLhtmlspecialchars显式 flags 与编码直接echo正则匹配 UTF-8加u修饰符漏掉uPHP 的字符串基础用法可以归纳成一句话先想清楚这一次操作是按字节还是按字符再决定用哪一套函数。查找、替换、前缀判断这类「整串比较」的操作单字节函数在 UTF-8 下通常是安全的而长度、截取、切分、补齐、反转这类「需要数位置」的操作必须用mb_*系列并显式指定编码。剩下的是细节strpos的返回值 0 要判! false、explode的分隔符不能为空、htmlspecialchars要带ENT_SUBSTITUTE、正则别忘了u。这些坑的共同特点是——不报错或者报错位置远离真实原因所以与其记结论不如记住它们背后的那条规则。