PHP中编码检测

背景:

  • 编码问题在Python同学眼中应该是老生常谈的,本文谈下PHP中常见的编码相关检测方法及局限
  • 数据在写入时决定了编码形式,而由于历史变更可能存在历史数据中写入编码不同
  • 编码检测的目的:检测数据的编码形式,正确解码及界面展示

PHP中不同编码检测方法

mb_detect_encoding检测

mb_detect_encoding函数中$encoding_list参数中编码顺序不同,会影响最终检测的结果。

1
2
3
4
5
6
7
8
// 用法:
$coding = mb_detect_encoding($unameGbk, array('UTF-8', 'GBK'), true);

$coding = mb_detect_encoding($unameGbk, array('GBK', 'UTF-8'), true);

// 实例
var_dump(mb_detect_encoding('hello', ['utf8', 'gbk'])); // utf8
var_dump(mb_detect_encoding('hello', ['gbk', 'utf8'])); // CP936 == gbk

自定义实现的is_utf8()和is_gbk()方法

这个来源是流传较广的一种写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static function is_utf8($str)
{
return 1 == preg_match('%^(?:[\x09\x0A\x0D\x20-\x7E]' . // ASCII
'| [\xC2-\xDF][\x80-\xBF]' . //non-overlong 2-byte
'| \xE0[\xA0-\xBF][\x80-\xBF]' . //excluding overlongs
'| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}' . //straight 3-byte
'| \xED[\x80-\x9F][\x80-\xBF]' . //excluding surrogates
'| \xF0[\x90-\xBF][\x80-\xBF]{2}' . //planes 1-3
'| [\xF1-\xF3][\x80-\xBF]{3}' . //planes 4-15
'| \xF4[\x80-\x8F][\x80-\xBF]{2}' . //plane 16
')*$%xs', $str);
}

public static function is_gbk($str)
{
//return preg_match('%^(?:[\x81-\xFE]([\x40-\x7E]|[\x80-\xFE]))*$%xs', $str);
return 1 == preg_match('%^(?:[\x81-\xFE][\x40-\x7E]' .
'| [\x81-\xFE][\x80-\xFE]' .
'| [\x00-\x80])*$%xs', $str);
}

iconv同编码转换后判等方式

  • 通过iconv判断转换前后的内容是否一致,作为编码检测的依据
  • 优劣
    • 必须指定检测范围
    • 在明确范围的情况下,检测准确性最佳
1
2
3
4
5
6
7
function detectEncoding ($str) {
foreach (array('GBK', 'UTF-8') as $v) {
if ($str === @iconv($v, $v . '//IGNORE', $str)) {
return $v;
}
}
}

实例验证

先看个正常的例子:

  • 小知识:GBK 和 CP936可以认为是同一种编码的不同叫法,CP936是GBK的实时实现。
1
2
3
4
5
6
7
$str = '天下相思';
$strNew = mb_convert_encoding($str, 'gbk', 'utf8'); // 从utf8转码至gbk,讲道理strNew只能是gbk吧


$coding = mb_detect_encoding($strNew, array('UTF-8', 'GBK'), true); // CP936
$coding = mb_detect_encoding($strNew, array('GBK', 'UTF-8'), true); // CP936
// 无论哪种方式检测,结果都是CP936,这就比较统一
  • 再一个极端的例子,不论哪种方式检测,都无法做到100%正确?
  • 此时严重依赖detectList中指定的待检测编码的顺序,也就是说可能误判为utf8,而此时如果输出则会乱码(实际并非utf8编码)
1
2
3
4
5
6
7
8
9
10
$str = '鸶姬'; // 本地utf8编码
$strNew = mb_convert_encoding($str, 'gbk', 'utf8'); // 从utf8转码至gbk,讲道理strNew只能是gbk吧

// 请问此时$strNew的编码是什么?编码检测工具检测出的结果如何?

$coding = mb_detect_encoding($strNew, array('UTF-8', 'GBK'), true); // UTF-8
$coding = mb_detect_encoding($strNew, array('GBK', 'UTF-8'), true); // CP936

var_dump(is_utf8($strNew)); // true
var_dump(is_gbk($strNew)); // true

结论&注意事项

  • 编码检测的准确性,验证依赖detectList待检测编码的顺序,应将范围小的在前,减少误判的概率(但无法避免)
  • 同时存在不同编码下字符,编码值保持相同,所以编码检测结果是首个包含此字符的编码,如英文词汇的gbk/utf8编码结果相同
  • 一种编码检测及转换为utf8的可行方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 注意,这里仅支持gbk/utf8的编码检测,且数据源多数为gbk编码
// 函数副作用:可能存在字符串检测结果既是gbk又是utf8,此方法会优先返回gbk
// 根据有限的几条测试情况,这种情况下,gbk->utf8的转码并不会改变字节码,结果一致
// 所以还是有局限性的,请明确使用范围和影响
function detectEncoding ($str) {
foreach (array('GBK', 'UTF-8') as $v) {
if ($str === @iconv($v, $v . '//IGNORE', $str)) {
return $v;
}
}
}

// 输入字符串,编码待确定,下面统一转换为utf8编码
$str = 'xxxx';
if(detectEncoding($str) == 'GBK') { // 源字符串为gbk编码
$strUtf8 = mb_convert_encoding($str, 'utf8', 'gbk'); // 从gbk转码为utf8
} else { // 源字符串为utf8编码
$strUtf8 = $str;
}
// 最终utf8编码的字符串保存在变量$strUtf8中