news 2026/10/8 13:00:17

php smarty截取中文字符乱码问题?gb2312/utf-8

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
php smarty截取中文字符乱码问题?gb2312/utf-8

前言


「Smarty 截取中文乱码」这个说法其实不准确。乱码不是 Smarty 造成的,Smarty 只是把 PHP 的字符串处理函数包了一层。真正的成因只有两个:按字节截断,以及字符集声明前后不一致。搞清楚这两件事,「乱码」就变成了一个必然结果,而不是玄学问题。


另一个要提前纠正的前提:这个标题把 gb2312 和 utf-8 并列当作两种可选方案。实际情况是,GB2312 是上世纪的标准,收录汉字有限,遇到生僻字会出现无法表示的字符;UTF-8 才是当前所有新项目的唯一合理选择。如果你是接手一个老 GB2312 项目,正确目标是迁移到 UTF-8,而不是继续在 GB2312 里缝缝补补。


本文先讲清字符与字节的关系,再拆开 Smarty 的truncate修饰器看它到底按什么单位截断,然后给出几个可以直接用的安全写法。示例基于 PHP 8.1 与 Smarty 4.x;Smarty 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.0+
var_dump(extension_loaded('mbstring'));
var_dump(mb_internal_encoding());

mbstring 没装就装上,排查就到此为止。装好之后仍然乱码,就往下一步走。


三、字符集链条:四者必须一致


即使 mbstring 装好了,只要链路上任何一个环节的字符集声明和实际的字节不一致,照样乱码。这条链上有四个位置:



  1. PHP 源文件本身的编码(含模板文件.tpl)。必须是无 BOM 的 UTF-8。

  2. mbstring 的 internal encoding,以及你调用mb_substr时传的字符集参数。

  3. 数据来源的编码。数据库连接字符集(MySQL 用utf8mb4)、导入的 CSV 或接口返回的数据。

  4. 输出端的声明。HTTP 响应头Content-Type: text/html; charset=utf-8,以及 HTML 里的<meta charset="utf-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_types=1);

/**
* 按字符截断,超出部分用省略号代替。
* $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.0+
echo htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

Smarty 侧的等价写法是给修饰器补上类型参数:


{$text|escape:'html'}

如果希望模板里所有{$var}默认就转义,可以在 PHP 侧打开开关:


<?php // 适用于 PHP 8.0+
$smarty->escape_html = true;

常见坑点



  1. ❌ 在模板里写{$title|truncate:20},服务器上没装 mbstring


✅ 要么装 mbstring 并用{$title|truncate:20:"...":true},要么把截断挪到 PHP 侧用mb_substr()。前者依赖环境,后者最稳。



  1. ❌ 用substr()截中文


✅ 用mb_substr($s, 0, $n, 'UTF-8')。substr()按字节,切在汉字中间必然坏掉。



  1. ❌ 数据库连上了、字段也存了 UTF-8,却漏了连接的字符集


✅ MySQL 要显式设定连接字符集(PDO 的 DSN 里加charset=utf8mb4,或在连接后执行SET NAMES utf8mb4),否则数据在传输层被再编码一次。



  1. ❌ HTML 里写了<meta charset="utf-8">就以为万事大吉


✅ 响应头Content-Type里的 charset 优先级更高,两者不一致时以响应头为准。两边都写对。



  1. ❌ 模板文件和 PHP 文件编码不一致


✅ 统一 UTF-8 无 BOM。混用 GB2312 与 UTF-8 的include,等于在拼接不同编码的字节流。



  1. ❌ 用iconv()转换时没处理非法字符


✅iconv()遇到无法转换的字符会返回false(或触发 notice),可以用//IGNORE、//TRANSLIT后缀处理,或直接改用mb_convert_encoding()。



  1. ❌ 靠mb_internal_encoding()的全局值隐式生效


✅ 每个mb_*调用都显式传'UTF-8'。全局值可能被框架、扩展或另一个库改掉,排查这类问题时最容易忽略。



  1. ❌ 在模板里对已经截断过的字符串再截一次


✅ 截断只做一次,且在数据进入模板之前完成。多层截断会让省略符出现两次,还会因为中间的 escape 把实体当成普通字符再切一次。


总结




现象真正的原因处理办法



中文尾部出现问号或方块按字节截断,切在汉字中间用mb_substr()/mb_strimwidth()

开发机正常,服务器乱码服务器没装 mbstring,truncate退化成按字节安装 mbstring,或把截断移到 PHP 侧

一整段文字变成ä¸ÂUTF-8 字节被按 Latin-1 解码了一次检查数据库连接字符集与响应头

页面某段文字凭空消失htmlspecialchars()遇到非法字节序列PHP 8.1 前补ENT_SUBSTITUTE,并显式传'UTF-8'

GB2312 项目生僻字缺字编码字符集本身不含该字迁移到 UTF-8,转换时按 GB18030 读旧数据



一句话结论:中文截断的关键不在 Smarty,而在「用按字符的函数、显式指定 UTF-8、让整条链路的字符集声明一致」。把截断放到 PHP 侧做,既绕开了truncate依赖 mbstring 的不确定性,也让这段逻辑变得可测试。至于 GB2312,它的正确归宿是尽早迁移,而不是继续在新代码里使用。





版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 12:59:47

电子保险丝+MCU:构建智能电源路径保护系统

干嵌入式和工业电子这行&#xff0c;迟早会在某个深夜&#xff0c;因为一块板子的电源出问题而加班。我自己的一个教训来自一台室外网关&#xff1a;现场接线松脱&#xff0c;12V 电源轨上被灌入了一股持续过流&#xff0c;保险丝没有在第一时间熔断&#xff0c;反而把 PCB 上的…

作者头像 李华
网站建设 2026/10/8 12:53:59

伺服压机控制系统架构:上下位机分离与Modbus通讯实战

1. 伺服压机控制系统到底在控什么 先把场景说清楚。伺服压机不是普通的液压机或者气动压机&#xff0c;它的核心执行机构是一台伺服电机&#xff0c;通过丝杠、同步带或者减速机把旋转运动转成直线运动&#xff0c;压头的位置、速度、压力三个量都能做到闭环控制。这就意味着它…

作者头像 李华