mb_substr 不会修复不完整 UTF-8 字符,需先用 mb_convert_encoding('UTF-8//IGNORE') 清洗非法字节,再显式指定编码调用;流式场景应累积 buffer 后统一清洗,避免破坏跨 chunk 的合法 UTF-8 组合。

mb_substr 会截断不完整 UTF-8 字符,但不报错也不修复
AI 生成文本(尤其流式输出或截断响应)常以不完整 UTF-8 字节序列结尾,比如 "\xf0\x9f"(只写了 emoji 前两个字节)。mb_substr 默认按字符计数,遇到这种非法开头时,PHP 的 mbstring 扩展会将其视为空字符或跳过——具体行为取决于 mb_substitute_character 设置,但**不会自动补全或修正字节**。
常见现象:截出的字符串末尾显示 ,或长度异常变短(如预期 10 个字符,实际返回 9 个),甚至 mb_strlen 返回值与原始字节流长度严重不符。
- 默认 substitute 是
none,非法字节被静默丢弃 - 设为
long或entity会插入替换符,但仍是“掩盖问题”,不是修复 - 不依赖
mb_internal_encoding()是否设为UTF-8—— 只要输入含非法字节,结果就不可靠
必须先清理再用 mb_substr,推荐 mb_convert_encoding + 'UTF-8//IGNORE'
不能指望 mb_substr 自己处理脏数据。正确做法是:在调用前,用 mb_convert_encoding 过滤掉不完整/非法 UTF-8 字节。
示例:
立即学习“PHP免费学习笔记(深入)”;
$dirty = "Hello\xF0\x9F"; // 不完整 emoji $clean = mb_convert_encoding($dirty, 'UTF-8', 'UTF-8//IGNORE'); // $clean === "Hello",非法 \xF0\x9F 被丢弃 $result = mb_substr($clean, 0, 5); // 安全截取
-
'UTF-8//IGNORE'是关键:它让转换器跳过无法解码的字节序列 - 不要用
//TRANSLIT,它会尝试替换,可能引入意外字符 - 注意:此操作是无损清洗,不是修复——丢失的字节不会恢复,但至少保证后续操作稳定
流式场景下,需在每次 chunk 合并后重新清洗
AI 接口分块返回时,每个 chunk 单独调用 mb_substr 很危险:前一个 chunk 末尾的不完整字节,可能和后一个 chunk 开头拼成合法字符(如 \xF0\x9F + \x98\x80 → ?)。直接清洗单个 chunk 会破坏这种跨 chunk 的 UTF-8 组合。
- 正确做法:累积 buffer,只在确认本轮数据收完(如收到
done标志)后再统一清洗 + 截取 - 若必须边收边显示,可用
iconv('UTF-8//IGNORE', 'UTF-8', $chunk)临时清洗,但需记录是否截断在多字节中间(如最后 1–3 字节是 UTF-8 多字节开头),留待下一次合并 - 检测方法:
strlen($chunk) - strlen(utf8_decode($chunk))非零,说明存在非法字节;更准的是用preg_match('//u', $chunk) === false
mb_substr 第三个参数 $encoding 必须显式传 'UTF-8'
即使已用 mb_internal_encoding('UTF-8'),mb_substr 仍可能因内部编码缓存或 SAPI 差异误判编码。不显式指定,某些 PHP 版本(尤其 7.4 之前)会回退到 ISO-8859-1,导致中文全乱码或截断错位。
- 永远写成
mb_substr($str, 0, 10, 'UTF-8'),别省略第三个参数 - 避免用
mb_regex_encoding()干扰——它只影响正则,不影响mb_substr - 检查当前设置:
var_dump(mb_internal_encoding(), mb_regex_encoding()),二者不一致时尤其要显式传参
真正麻烦的不是函数本身,而是你拿到的字符串是否真的由连续、完整的 UTF-8 字节构成——AI 输出几乎从不保证这点,所以清洗永远比截取优先。



















