PHP的文件编码一般都是默认的UTF-8。而面对GBK的数据库的话,接入数据库不仅要指定字符集是gbk,得到的结果中,汉字部分也还是需要从GBK转编码到UTF-8的。

网上呢,关于这个一般介绍也就给个mb_detect_encoding,最多再给个iconv就到头了。

但我打算梳理下自己负责的一个系统相关方法,以码会友,于是,就有了这篇进化史。

注:PHP默认文件编码为UTF-8。故测试用例的汉字都预先强制从UTF-8转码为GBK。


第一版

function stringToUtf8($str)
{
$encode = mb_detect_encoding($str, [ "ASCII","UTF-8","GB2312","GBK","BIG5", "EUC-CN" ]);
if ($encode!=="UTF-8") {
return mb_convert_encoding($str, "UTF-8", $encode);
} else {
return $str;
}
}

看起来是很中规中矩是吧,网上找的大部分编码检测也差不多类似。理解起来就是,如果检测结果不是UTF-8,那就转码。

乍一看挺好啊,这不是实现了么,那试试下面的测试用例:

<?php
$arr = ["影", "小鸠", "露营", "鸭寮街", "欧洲", "甄嬛ABC"];
foreach ($arr as $ret) {
test($ret);
}

function test($ret)
{
$ret = mb_convert_encoding($ret, "GBK", "UTF-8");
$ret = stringToUtf8($ret);
echo "{$ret}\n";
$ret = stringToUtf8($ret);
echo "{$ret}\n";
}

输出结果:

Ӱ
Ӱ
С
С
¶Ӫ
¶Ӫ
Ѽ弽
Ѽ弽
欧洲
欧洲
甄嬛ABC
甄嬛ABC

呐,你看,很多结果都不对。当然,如果你给每个测试用例加个全角的句号“。”,那结果就不一样了。但这不重要,有兴趣自己试试?

剑走偏锋(精神病人思路广)的进化思路
最初发现问题是因为写入Elasticsearch部分条目报错json异常。灵感来了,json_encode只能转化UTF-8编码的文字,遇到中文就会返回false。这特性看起来可以利用。


第二版

function stringToUtf8($str)
{
$encode = mb_detect_encoding($str, [ "ASCII", "UTF-8", "GB2312", "GBK", "BIG5", "EUC-CN" ]);
if ($encode!=="UTF-8") {
$ret = mb_convert_encoding($str, "UTF-8", $encode);
} else {
$ret = $str;
}
#遇到特殊字会检测为UTF-8,但其实是GBK的,需要用json_encode判定,如果转码失败就强制修正
if (json_encode([$ret])===false && $encode=="UTF-8") {
$ret = mb_convert_encoding($str, "UTF-8", "EUC-CN");
}
return $ret;
}

已经结合上一版的改进思路,加入了json_encode方法的利用。测试用例再走一波:

Ӱ
Ӱ
小鸠
小鸠
¶Ӫ
¶Ӫ
鸭寮街
鸭寮街
欧洲
欧洲
甄嬛ABC
甄嬛ABC

= =是不是很6?影和露营依然有问题。

精神病人思路广的进化思路
恩- -检测目标其实就是区分下是不是UTF-8。而mb_detect_encoding备选编码顺序是会影响结果的,那我就只保留两个备选编码:”EUC-CN”, “UTF-8″。
如果检测失败,或者是EUC-CN,或者json_encode失败,就强制从EUC-CN转UTF-8。


第三版

function stringToUtf8($str)
{
$ret = $str;
if ($str!="") {
$encode = mb_detect_encoding($str, [ "EUC-CN", "UTF-8" ]);
#如果未检测出编码类型,或者是中文或者Json转失败都强制转换
if (!$encode || $encode=="EUC-CN" || json_encode([$str])===false) {
$ret = mb_convert_encoding($str, "UTF-8", "EUC-CN");
}
}
return $ret;
}

输出结果:

影
褰
小鸠
小鸠
露营
露营
鸭寮街
鸭寮街
欧洲
娆ф床
甄??BC
甄??BC

额?二次转码会重复转码造成部分结果出现异常,预期上来看,重复转码应当确保结果稳定才对,怎么整呢?

精神病人思路广的进化思路
这时许赢提出,汉字的编码正则判定是按范围来的,影这个字,json_encode可以解析为\u04f0。那~~~我反复一下,决定试试调优下第一版的旧方法如何?


第四版(调优第一版方法,没啥参考价值,可以跳过)

function stringToUtf8($str)
{
$encode = mb_detect_encoding($str, [ "ASCII", "UTF-8", "GB2312", "GBK", "BIG5", "EUC-CN" ]);
if ($encode!=="UTF-8") {
$ret = mb_convert_encoding($str, "UTF-8", $encode);
} else {
$ret = $str;
}
#遇到特殊字会检测为UTF-8,但其实是GBK的,需要用json_encode判定,如果转码失败就强制修正
#思路来自许赢
$content = json_encode([$ret]);
$findpos = -1;
do {
$findpos = strpos($content, '\u', $findpos+1);
if ($findpos!=false) {
$str1 = hexdec(substr($content, $findpos+2, 4));
#\u4e00-\u9fa5是汉字范围,超出了,说明检测不对,需要强转码
if ($str1 < 19968 || $str1 > 40869) {
$ret = mb_convert_encoding($str, "UTF-8", "EUC-CN");
}
break;
}
} while($findpos!=false);
return $ret;
}

输出结果:

影
影
С
С
露营
露营
Ѽ弽
Ѽ弽
欧洲
欧洲
甄嬛ABC
甄嬛ABC

= =这结果,比较扯淡,还是放弃这种骚方法吧。继承第三版方法改进。

精神病人思路广的进化思路
其实之前从第二版到第三版的尝试过程中,灵光乍现想过转码前后长度是不一样的,但是实测没卵用,就放弃了。但在第三版结果趋于稳定的情况下,许赢再次提到了这个思路,结合思路,加入长度判定,看看效果如何。

欲知后事如何,且见明日博客。

Related Posts: PHP UTF-8下的GBK编码检测与转换 进化史 开篇 :

avatar