在一个文档预览服务里面,我需要探测字符串(文件)的编码集,并根据编码集输出正确的header信息。
一般的做法是用mb_detect_encoding
来进行探测,但是这个探测的误报率比较高,经常容易探测不出来准确的字符集。因为时间关系没有细研究mb_detect_encoding
异常的原因,我自己实现了一个detect_charset
的方法(见后)。
但是,在开始之前,我们需要有一些知识准备:
内码
我们知道任何字符串在某个计算机系统中都是以01形式存储,这个01二进制流就是内码。
比如 $a = 'ab'
,这里字符串ab
在内存里面存储的大概为0x61 0x62
这种形式的二进制数据(根据不同的编码集,具体占用的字节数可能不一样,中间会有一些补零)。
在计算机内部进行数据流转的时候,其实是内码的传输拷贝。比如file_put_content($a)
就是把ab
字符串对应的二进制内码写入到文件中,而非其字符图片的二进制数据。
编码集
编码集就是计算机内码所使用的编码规则,为什么要有这个规则呢?
如果没有编码集,我们如何表示一个字符串,比如字符a
? 我们知道计算机只能识别一个二进制流,如果不用编码集,我们如何用图形化的方式表示字符a
?
比如,我们用一个8x8的格子用点状来描绘a
:
. . . . . . . .
. * * * * . . .
. . * * * * . .
. * . . . * . .
. * . . . * . .
. * . . . * * *
. . * * * . . .
. . . . . . . .
这里,就是一个图形化的a
,虽然不是很像,但是已经有a
的雏形了。那么,如果把这个转换为计算机可以识别的形式呢?
我们用0代表.
,1代表*
,上面的格子可以转换为:
0 0 0 0 0 0 0 0
0 1 1 1 1 0 0 0
0 0 1 1 1 1 0 0
0 1 0 0 0 1 0 0
0 1 0 0 0 1 0 0
0 1 0 0 0 1 1 1
0 0 1 1 1 0 0 0
0 0 0 0 0 0 0 0
这样,我们就可以用8x8=64个二进制位来存储,即8字节来存储这个点图。
bcd...
同理。
这种编码方式其实也是一种编码集,但是,我们的计算机其实不是这样的。为什么不用这种编码方式呢?后面我们可以看到这种编码方式会有很多的冗余,而且不利于扩展,比如中文的字符’的’用这种方式如何编码?可能8x8的格子就已经不够用了。
最早期的ASCII编码是这样做的:
因为拉丁文常用字符a-zA-Z
只有52个不同的字符,那么我们用数字1-52
就可以分别代表这52个字符,而数字1-52
只需要一个字节就可以存储。因为一个字节的存储范围为0~255。
所以,实际上ASCII用0x61代表字符a
,其他的以此类推。这就是ASCII编码集,它的内码范围为0x00-0x7f
。
但是,随着计算机的传播跟发展,我们发现其他国家的语言没法用这个编码集来表示。这个时候,不同国家的人就不得不制定新的扩展集,来表示自己国家的文字字符。
比如,我们中文,就有gbk,big5,gb2310之类的编码集。而这些编码集其实质是建立一个中文字符和二进制值的映射关系。
/**
* 检测字符串编码(注意:存在误判的可能性,降低误判的几率的唯一方式是给出尽可能多的样本$line)
* 检测原理:对给定的字符串的每一个字节进行判断,如果误差与gb18030在指定误差内,则判定为gb18030;与utf-8在指定误差范围内,则判定为utf-8;否则判定为utf-16
* @param string $line
* @return string 中文字符集,返回gb18030(兼容gbk,gb2312,ascii);西文字符集,返回utf-8(兼容ascii);其他,返回utf-16(双字节unicode)
* @author fangl
*/
function detect_charset($line) {
if(self::detect_gb18030($line)) {
return 'gb18030';
}
else if(self::detect_utf8($line)) {
return 'utf-8';
}
else return 'utf-16';
}
/**
* 兼容ascii,gbk gb2312,识别字符串是否是gb18030标准的中文编码
* @param string $line
* @return boolean
* @author fangl
*/
function detect_gb18030($line) {
$gbbyte = 0; //识别出gb字节数
for($i=0;$i+3<strlen($line);) {
if(ord($line{$i}) >= 0 && ord($line{$i}) <= 0x7f) {
$gbbyte ++; //识别一个单字节 ascii
$i++;
}
else if( ord($line{$i}) >= 0x81 && ord($line{$i}) <= 0xfe &&
(ord($line{$i+1}) >= 0x40 && ord($line{$i+1}) <= 0x7e ||
ord($line{$i+1}) >= 0x80 && ord($line{$i+1}) <= 0xfe) ) {
$gbbyte += 2; //识别一个双字节gb18030(gbk)
$i += 2;
}
else if( ord($line{$i}) >= 0x81 && ord($line{$i}) <= 0xfe &&
ord($line{$i+2}) >= 0x81 && ord($line{$i+2}) <= 0xfe &&
ord($line{$i+1}) >= 0x30 && ord($line{$i+1}) <= 0x39 &&
ord($line{$i+3}) >= 0x30 && ord($line{$i+3}) <= 0x39) {
$gbbyte += 4; //识别一个4字节gb18030(扩展)
$i += 4;
}
else $i++; //未识别gb18030字节
}
return abs($gbbyte - strlen($line)) <= 4; //误差在4字节之内
}
/**
* 识别字符串是否是utf-8编码,同样兼容ascii
* @param string $line
* @return boolean
* @author fangl
*/
function detect_utf8($line) {
$utfbyte = 0; //识别出utf-8字节数
for($i=0;$i+2<strlen($line);) {
//单字节时,编码范围为:0x00 - 0x7f
if(ord($line{$i}) >= 0 && ord($line{$i}) <= 0x7f) {
$utfbyte ++; //识别一个单字节utf-8(ascii)
$i++;
}
//双字节时,编码范围为:高字节 0xc0 - 0xcf 低字节 0x80 - 0xbf
else if(ord($line{$i}) >= 0xc0 && ord($line{$i}) <= 0xcf
&& ord($line{$i+1}) >= 0x80 && ord($line{$i+1}) <= 0xbf) {
$utfbyte += 2; //识别一个双字节utf-8
$i += 2;
}
//三字节时,编码范围为:高字节 0xe0 - 0xef 中低字节 0x80 - 0xbf
else if(ord($line{$i}) >= 0xe0 && ord($line{$i}) <= 0xef
&& ord($line{$i+1}) >= 0x80 && ord($line{$i+1}) <= 0xbf
&& ord($line{$i+2}) >= 0x80 && ord($line{$i+2}) <= 0xbf) {
$utfbyte += 3; //识别一个三字节utf-8
$i += 3;
}
else $i++; //未识别utf-8字节
}
return abs($utfbyte - strlen($line)) <= 3; //误差在3字节之内的,则识别为utf-8编码
}