由 Excel 数据导入数据库出现中文乱码引发的对 PHP 字符编码转换的探究
前面的 PHPExcel 读取 Excel 数据并导入数据库,在组合脚本的过程中,数据库插入数据后发生了中文乱码现象。
经过一番波折 ,虽然最终确定了造成插入数据中文乱码原因,但对 PHP 字符编码转换函数的测试也有一些小发现。
为什么数据库插入数据会有中文乱码
数据库数据出现中文乱码可以从三个方面考虑:
- 数据库存储字符集与查看环境字符集不同
- 插入数据字符编码与数据库存储字符集不同
- 客户端和服务器之间传递字符的编码规则不同
这三个方面主要涉及的点也是三个,数据库、MySQL 服务器和数据。只要统一这三者的字符集,就能确保不会出现乱码。检查了一下 ,数据库字符集为 utf8;数据是从 Excel 中读取的 ,不确定;MySQL 服务器也不确定,但可以用指令查看。
MySQL 服务器编码规则查看
# 进入 MySQL 命令行
mysql> show variables like 'character%';
发现好多项都不是 utf8,最终结果是,执行 set names utf8
之后,再插入数据显示正常。
那么中间那些变量值改变了呢?
所以 set names utf8
实际修改了 character_set_client、character_set_connection、character_set_results 这三项的值。这三项定义了客户端、连接和返回结果的字符集。
如何确定读取数据的字符编码
PHP 主要有两个修改字符串编码的函数 iconv 和 mb_convert_encoding,一个判断字符串编码的函数 mb_detect_encoding。
iconv($encoding, "UTF-8//IGNORE", $str)
mb_convert_encoding($text, 'UTF-8', $encoding);
两者区别:
一般情况下用 iconv,只有当遇到无法确定原编码是何种编码,或者iconv转化后无法正常显示时才用mb_convert_encoding 函数。
注意:要安装相应的 mbstring 扩展,并在配置文件中启动之后,mb_*
方法才能生效。
所以在不确定字符编码的情况下,必须有 mb_detect_encoding 协助才行。
mb_detect_encoding($str ,array('ASCII','GB2312','GBK','UTF-8'), true);
mb_detect_encoding($str, mb_detect_order(), false)
第二个参数给出优先判断的字符编码列表,因为字符编码太多,这样做可以一定程度的提高函数运行效率,默认为mb_detect_order()
。测试了一下,mb_detect_order()
输出一数组,包含 ‘ASCII’ 和 ‘UTF-8’ 两个元素。最后一个参数表示是否使用严格的编码检测,默认为 false。
在网上找到一个万金油版的组合使用方法:
function ConvertToUTF8($text){
$encoding = mb_detect_encoding($text, mb_detect_order(), false);
if($encoding == "UTF-8")
{
$text = mb_convert_encoding($text, 'UTF-8', 'UTF-8');
}
$out = iconv(mb_detect_encoding($text, mb_detect_order(), false), "UTF-8//IGNORE", $text);
return $out;
}
如果执行 iconv 报错 Detected an incomplete multibyte character
,可以尝试使用这个方法来转换。
我之前适配过的一个处理抓取 html 文档的标题的写法:
$encode = mb_detect_encoding($title, array("ASCII","GB2312","GBK","UTF-8"));
if ($encode == "EUC-CN") {
// $title = mb_convert_encoding($title, "GBK", $encode);
$title = iconv($encode, "UTF-8", $title);
} elseif (!in_array($encode, array("ASCII", "CP936", "GB2312", "UTF-8", "GBK"))) {
$title = iconv($encode, "GBK", $title);
}
诡异的 CP936 编码无法转换成 UTF-8
CP936 转化为 UTF-8 失败?这是一个错误的命题,因为 mb_detect_encoding 并不是完全准确的判断字符编码,而是根据第二个参数,按照顺序去匹配字符编码。如果满足了某个字符编码,则会立即返回该编码。
$ php -r 'echo mb_detect_encoding("ABC", mb_detect_order(), true);'
ASCII
$ php -r 'echo mb_detect_encoding("ABC", mb_detect_order(), false);'
ASCII
$ php -r 'echo mb_detect_encoding("ABC-DEF", mb_detect_order(), false);'
ASCII
$ php -r 'echo mb_detect_encoding("ABC-DEF?", mb_detect_order(), true);'
UTF-8
$ php -r 'echo mb_detect_encoding("ABC-DEF?", mb_detect_order(), false);'
UTF-8
$ php -r "echo mb_detect_encoding('我是白羊座', mb_detect_order(), false);"
UTF-8
...
$ php -r "echo mb_detect_encoding('我是金牛座', array('ASCII','GB2312','GBK','UTF-8'), false);"
CP936
得到的结果很尴尬,为了执行效率而特意适配使用的 array('ASCII','GB2312','GBK','UTF-8')
竟然成了导致判断错误的罪魁祸首。也就是说,字符串本来就是‘UTF-8’,转化过程中字符编码没有有任何变化,转化后又通过这个 mb_detect_encoding('我是金牛座', array('ASCII','GB2312','GBK','UTF-8'), false)
方法,判断的结果当然还是错误的结果。
并不是 CP936 编码无法转换成 UTF-8,而是判断出的编码有误。
所以在是用 mb_detect_encoding 方法时,不加任何多余参数判断是最准确的:
mb_detect_encoding('我是白羊座')