
前言「Smarty 截取中文乱码」这个说法其实不准确。乱码不是 Smarty 造成的Smarty 只是把 PHP 的字符串处理函数包了一层。真正的成因只有两个按字节截断以及字符集声明前后不一致。搞清楚这两件事「乱码」就变成了一个必然结果而不是玄学问题。另一个要提前纠正的前提这个标题把 gb2312 和 utf-8 并列当作两种可选方案。实际情况是GB2312 是上世纪的标准收录汉字有限遇到生僻字会出现无法表示的字符UTF-8 才是当前所有新项目的唯一合理选择。如果你是接手一个老 GB2312 项目正确目标是迁移到 UTF-8而不是继续在 GB2312 里缝缝补补。本文先讲清字符与字节的关系再拆开 Smarty 的truncate修饰器看它到底按什么单位截断然后给出几个可以直接用的安全写法。示例基于 PHP 8.1 与 Smarty 4.xSmarty 3 也保留同样的truncate修饰器。文中代码无法在本机执行验证请结合本地实测阅读。一、字符、字节、编码乱码的根一个汉字在计算机里存成几个字节取决于编码编码一个汉字占多少字节说明UTF-8通常 3 字节扩展区 4 字节变长编码ASCII 仍是 1 字节GB23122 字节只覆盖常用汉字生僻字缺字GBK / GB180302 字节GB18030 有 4 字节区GB2312 的超集PHP 的substr()是按字节切的它不认识「一个汉字」这个概念。对 UTF-8 字符串中文字符调用substr($s, 0, 4)切出来的是「中」加上「文」的第一个字节——半个字符。这半个字符再输出到浏览器解析不了于是显示成问号、方块或者一串鬼画符。正确的工具是 mbstring 扩展提供的mb_substr()它按字符切?php // 适用于 PHP 8.0$s 中文字符串;echo substr($s, 0, 4); // 按字节切切碎了echo \n;echo mb_substr($s, 0, 4, UTF-8); // 按字符切中文所以第一条结论任何针对中文的截断都必须用mb_*系列函数并且显式指定字符集。不指定字符集时mbstring 会退回到mb_internal_encoding()的值那个值可能被框架或 php.ini 改过显式写死才稳。二、Smarty 的 truncate 到底按什么单位截Smarty 的截断修饰器用法是{$content|truncate:30:...:true}位置参数依次是截断长度、省略符、是否允许在单词中间断开break_words还有一个middle参数用于从中间截断。这里要注意的是长度参数的单位并不固定当环境启用了 mbstring 扩展时Smarty 用mb_substr()按字符截断中文不会被切碎当 mbstring 不可用时Smarty 回退到substr()也就是按字节截断中文必然乱码。这就是为什么同样一份模板在开发机装了 mbstring正常、上了某台服务器没装 mbstring就乱码。所以排查顺序应该是php -m | grep mbstring或者用代码判断?php // 适用于 PHP 8.0var_dump(extension_loaded(mbstring));var_dump(mb_internal_encoding());mbstring 没装就装上排查就到此为止。装好之后仍然乱码就往下一步走。三、字符集链条四者必须一致即使 mbstring 装好了只要链路上任何一个环节的字符集声明和实际的字节不一致照样乱码。这条链上有四个位置PHP 源文件本身的编码含模板文件.tpl。必须是无 BOM 的 UTF-8。mbstring 的 internal encoding以及你调用mb_substr时传的字符集参数。数据来源的编码。数据库连接字符集MySQL 用utf8mb4、导入的 CSV 或接口返回的数据。输出端的声明。HTTP 响应头Content-Type: text/html; charsetutf-8以及 HTML 里的meta charsetutf-8。四个位置里只要有一个是 GB2312、其他三个是 UTF-8数据在这一环就会被二次编码或错误解码。典型症状是「数据库里看着正常页面上是一串『ä¸Â』这样的怪字符」——那是 UTF-8 的字节被当成 Latin-1 又解码了一次。如果确实需要和历史数据互转用mb_convert_encoding()或iconv()?php // 适用于 PHP 8.0// 老数据是 GB2312转成 UTF-8 再进模板$utf8 mb_convert_encoding($legacy, UTF-8, GB2312);// 生僻字建议按 GB18030 处理它兼容 GB2312 且字符集更大$utf8 mb_convert_encoding($legacy, UTF-8, GB18030);注意转换方向mb_convert_encoding($str, $to_encoding, $from_encoding)第三个参数是来源编码写反了会更乱。四、推荐做法与输出安全最稳的方案是根本不在模板里截断。模板只做展示字符串加工在 PHP 里完成这样用到的函数、字符集、边界处理都是你可以控制的?php // 适用于 PHP 8.0需要 mbstring 扩展declare(strict_types1);/*** 按字符截断超出部分用省略号代替。* $len 指保留的字符数不是字节数。*/function cutText(string $text, int $len, string $etc …): string{if ($len 0) {return ;}if (mb_strlen($text, UTF-8) $len) {return $text;}return mb_substr($text, 0, $len, UTF-8) . $etc;}$smarty-assign(intro, cutText($row[intro], 40));模板里就只剩一句p{$intro}/p如果业务要求按「显示宽度」截断半角算 1、全角算 2用mb_strimwidth()?php // 适用于 PHP 8.0// 宽度 40 意味着大约 20 个汉字超出部分用 ... 补齐$short mb_strimwidth($text, 0, 40, ..., UTF-8);它的第四个参数是省略符第五个是字符集返回的字符串总宽度包含省略符在内。这里要注意它和mb_substr的区别mb_strimwidth数的是「显示宽度」不是「字符个数」。截断处理完之后还有一个容易和「乱码」混为一谈的环节输出转义。经常有人把|escape和乱码混在一起讲。要区分清楚乱码是字节和编码不匹配属于编码问题|escape是把小于号、大于号、与号、引号转成 HTML 实体属于输出安全和编码无关。不过在 PHP 8.1 上它们确实会撞车值得单独说一句htmlspecialchars()的默认参数在 PHP 8.1 发生了变化默认旗标从ENT_COMPAT变成了ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401。其中ENT_SUBSTITUTE的作用是遇到不合法的字节序列时用替换字符代替、而不是返回空字符串。在 PHP 8.1 之前一段含有非法 UTF-8 字节的中文文本传进htmlspecialchars()会得到页面那一整段直接消失看起来比乱码还吓人。所以显式写全参数仍然是好习惯?php // 适用于 PHP 8.0echo htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, UTF-8);Smarty 侧的等价写法是给修饰器补上类型参数{$text|escape:html}如果希望模板里所有{$var}默认就转义可以在 PHP 侧打开开关?php // 适用于 PHP 8.0$smarty-escape_html true;常见坑点❌ 在模板里写{$title|truncate:20}服务器上没装 mbstring✅ 要么装 mbstring 并用{$title|truncate:20:...:true}要么把截断挪到 PHP 侧用mb_substr()。前者依赖环境后者最稳。❌ 用substr()截中文✅ 用mb_substr($s, 0, $n, UTF-8)。substr()按字节切在汉字中间必然坏掉。❌ 数据库连上了、字段也存了 UTF-8却漏了连接的字符集✅ MySQL 要显式设定连接字符集PDO 的 DSN 里加charsetutf8mb4或在连接后执行SET NAMES utf8mb4否则数据在传输层被再编码一次。❌ HTML 里写了meta charsetutf-8就以为万事大吉✅ 响应头Content-Type里的 charset 优先级更高两者不一致时以响应头为准。两边都写对。❌ 模板文件和 PHP 文件编码不一致✅ 统一 UTF-8 无 BOM。混用 GB2312 与 UTF-8 的include等于在拼接不同编码的字节流。❌ 用iconv()转换时没处理非法字符✅iconv()遇到无法转换的字符会返回false或触发 notice可以用//IGNORE、//TRANSLIT后缀处理或直接改用mb_convert_encoding()。❌ 靠mb_internal_encoding()的全局值隐式生效✅ 每个mb_*调用都显式传UTF-8。全局值可能被框架、扩展或另一个库改掉排查这类问题时最容易忽略。❌ 在模板里对已经截断过的字符串再截一次✅ 截断只做一次且在数据进入模板之前完成。多层截断会让省略符出现两次还会因为中间的 escape 把实体当成普通字符再切一次。总结现象真正的原因处理办法中文尾部出现问号或方块按字节截断切在汉字中间用mb_substr()/mb_strimwidth()开发机正常服务器乱码服务器没装 mbstringtruncate退化成按字节安装 mbstring或把截断移到 PHP 侧一整段文字变成ä¸ÂUTF-8 字节被按 Latin-1 解码了一次检查数据库连接字符集与响应头页面某段文字凭空消失htmlspecialchars()遇到非法字节序列PHP 8.1 前补ENT_SUBSTITUTE并显式传UTF-8GB2312 项目生僻字缺字编码字符集本身不含该字迁移到 UTF-8转换时按 GB18030 读旧数据一句话结论中文截断的关键不在 Smarty而在「用按字符的函数、显式指定 UTF-8、让整条链路的字符集声明一致」。把截断放到 PHP 侧做既绕开了truncate依赖 mbstring 的不确定性也让这段逻辑变得可测试。至于 GB2312它的正确归宿是尽早迁移而不是继续在新代码里使用。