要从“𥊍”这个字说起,因为这个字提了一个Issue(https://github.com/zblogcn/zblogphp/issues/42) 。然后翻找了半天资料才算勉强解决这个问题。
首先,其UTF-8编码是U+2528D,属于CJK Unified Ideographs Extension B(中日韩统一表意文字扩充B)字符集的字符,处于第二辅助平面(SIP,表意文字补充平面),不属于我们通常所见的基本多文种平面(BMP,即Unicode编码范围在0000-FFFF之内)的字符。保存一个字,需要占4 Bytes的字节;相比之下,在BMP范围之内的字符只需要占用3 Bytes。仅仅就因为字符保存位数不同,就让程序开发出现了难题。
先看MySQL。根据MySQL 5.0版本的官方文档所述(https://dev.mysql.com/doc/refman/5.0/en/charset-unicode-utf8.html):
Korean, Chinese, and Japanese ideographs use 3-byte sequences.
也就是说,我们常用的utf8_general_ci
这种保存表的方式,其只能以三个字符保存这些数据。出现了像上面这个字一样的字符,其自然便出错了。
那怎么解决呢?用utf8mb4_general_ci
来代替utf8_general_ci
,就可以让MySQL以四个字符来保存UTF-8数据,其自然就能解决。
可是……
别忙着!utf8mb4_general_ci
是在MySQL 5.5.3以上版本才支持的!我们可以看看WordPress的以下代码:
if ( version_compare( $version, '5.5.3', '<' ) ) {
return false;
}
// .......
/*
* libmysql has supported utf8mb4 since 5.5.3, same as the MySQL server.
* mysqlnd has supported utf8mb4 since 5.0.9.
*/
if ( false !== strpos( $client_version, 'mysqlnd' ) ) {
$client_version = preg_replace( '/^\D+([\d.]+).*/', '$1', $client_version );
return version_compare( $client_version, '5.0.9', '>=' );
} else {
return version_compare( $client_version, '5.5.3', '>=' );
}
所以说,如果没有自己配置主机的权限,主机MySQL版本低于5.5.3的话,还是不能使用。
那怎么办呢?
那,当然是强制转换了。将其转换为连数据库都读得懂的编码后,再存入或读出,自然也便可以了。
当然,我们在PHP中可以直接使用bin2hex
函数将其直接转换为16进制,通过加装一些壳来界定其边界;读出时直接再一个正则读回来转回二进制输出到页面就是。可是,这未免也太麻烦了。如果我们只是在浏览器中显示这个字符的话,完全可以使用其他方式。
在HTML中,可以使用&#xhhhh;
来显示一个字符,hhhh为十六进制。这类字符似乎被称之为HTML Entity
,或是HTML实体
。我目前只能确认&
之类字符算是HTML Entity,&#xhhhh;
只看到如Code Hex
这样的称呼,无论如何Google都搜索不到比较靠谱的名字,这就不管他了。
这里的这个十六进制,实际上是要用UTF-32
或UCS-4
编码来获取的;用UTF-16
得到的编码只适用于BMP内字符,用UTF-8
得到的十六进制只适合ASCII字符。这一点不知道坑了我半天。既然知道了,那就好办了。在SQL查询前挂个正则替换,一切就安宁了。
$sql = preg_replace_callback("/[\x{10000}-\x{10FFFF}]/u", function ($matches) {
return sprintf("&#x%s;", ltrim(strtoupper(bin2hex(iconv('UTF-8', 'UCS-4', $matches[0]))), "0"));
}, $sql);
反之,若这个字符不仅仅用于浏览器显示用途,而要在程序内操作的话,也可以反转回来:
$content = preg_replace_callback("/\&\#x([0-9A-Z]{2,6})\;/u", function($matches) {
iconv('UCS-4', 'UTF-8', hex2bin(str_pad($matches[1], 8, "0", STR_PAD_LEFT)));
}, $content);
比较麻烦的是,PHP < 5.4的版本不支持回调函数和hex2bin
(http://php.net/manual/zh/function.hex2bin.php) ,所以可能还要自己去实现一个。不过人PHP官方的评论区里就有,拿来抄就是了。
不过还有更悲惨的事情,那就是在JavaScript中使用。
由于历史原因,JavaScript采用的编码是UCS-2
或是UTF-16
,直到最近的ECMAScript 6标准才有了对Unicode原生的支持。如果我们要在JavaScript中使用这些非ASCII字符,也就必须将其转换为UCS-2
编码。有以下方式可用:
$sql = preg_replace_callback("/[\x{10000}-\x{10FFFF}]/u", function ($matches) {
return sprintf("\u%s", substr(implode("\u", str_split(strtoupper(bin2hex(iconv('UTF-8', 'UTF-16', $matches[0]))), 4)), 6));
}, $sql);
将UTF-8
转为UTF-16
后会多出\uFFEF
这个字(非法字符),所以要将其用substr
砍掉。
另外更机智的做法是:
$sql = preg_replace_callback("/[\x{10000}-\x{10FFFF}]/u", function ($matches) {
return trim(json_encode($matches[0]), '"');
}, $sql);
总的来说,又是被资料不全 + 坑爹问题坑了一个下午的时间吧,嗯。