2

PHP检测字符编码

 2 years ago
source link: http://hustnaive.github.io/php/2015/08/21/php-detect-encoding.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

PHP检测字符编码


在一个文档预览服务里面,我需要探测字符串(文件)的编码集,并根据编码集输出正确的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编码
    }


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK