最近在做一个百度词典的采集,http://dict.baidu.com,发现了一个可以值得探讨的一个问题,汉字的编码问题。首先,我们输入一个汉字词语进行搜索,比如,我们,URL上的地址变成http://dict.baidu.com/s?wd=%CE%D2%C3%C7,前面的http://dict.baidu.com/s?wd=就不用管了,都知道是什么意思,我们关注一下后面“%CE%D2%C3%C7”这几个东西。显然是词语“我们”这个的汉字编码。我们知道PHP有个函数urlencode,可以把汉字转换为类似那样行事的。,得到结果为:%E6%88%91%E4%BB%AC。
显然,不是%CE%D2%C3%C7这种格式的。是不是进制的问题呢?还是汉字编码的问题(UTF-8或者GB2312)。我们做一下实验。
我们先进行转码:
1
2 3 4 5 6 7 |
<?php
$str = '我们' ; //iconv('utf-8','cp936',$str); //echo urlencode($str); iconv ( 'cp936' , 'utf-8' , $str ) ; echo urlencode ( $str ) ; ?> |
发现输出结果都为:%E6%88%91%E4%BB%AC,因为我测试的页面为utf-8的页面。所以结果一样。当页面为GBK或者GB2312的时候结果为:%CE%D2%C3%C7,这样,就和百度上的那个编码一致了。现在我们主要讨论页面编码为UTF8的时候,怎么得到正确的编码。
汉字的编码究竟是如何做的。我们知道,国内大部分都是GBK编码的,我们知道,GBK编码中一个汉字由二个字符组成,获取汉字字符串的方法如下:
1
2 3 4 5 6 7 8 9 10 11 |
<?php
$string = "我们" ; $length = strlen ( $string ) ; for ( $i = 0 ; $i < $length ; $i ++ ) { if ( ord ( $string [ $i ] ) > 127 ) { $result [ ] = ord ( $string [ $i ] ) . ' ' . ord ( $string [ ++ $i ] ) ; } } var_dump ( $result ) ; ?> |
由于一个汉字为两个字符组成,通过ord()函数获取字符的ASCII值如果大于127时,就可以确定当前字符为一个汉字的前半部分,还需要获取汉字的后半部分。当然,这种判断的方法要结合具体的开发环境,如果存在ASCII值大于127的单个字符,这种方法判断显然就不正确。得到结果为一个数组:
GBK编码的页面结果:
1
2 3 4 5 6 |
array
(
2
)
{
[ 0 ] => string ( 7 ) "206 210" [ 1 ] => string ( 7 ) "195 199" } |
UTF-8的页面结果:
1
2 3 4 5 6 7 8 |
array
(
3
)
{
[ 0 ] => string ( 7 ) "230 136" [ 1 ] => string ( 7 ) "145 228" [ 2 ] => string ( 7 ) "187 172" } |
GB2312的页面结果:
1
2 3 4 5 6 |
array
(
2
)
{
[ 0 ] => string ( 7 ) "206 210" [ 1 ] => string ( 7 ) "195 199" } |
从以上结果可以看出,如果页面编码为国标编码的时候,一个汉字是由两个字节组成。而页面编码为UTF8的时候,汉字是由三个字节组成的。但是进制都是十进制的,而我们需要的是十六进制的。那么怎么把十进制的汉字编码转换为十六进制呢?
可以采用以下办法,php由几个内建函数可以直接转换进制,decbin(),十进制转换为二进制;dechex(),十进制转换为十六进制;decoct(), 十进制转换为八进制。
1
2 3 4 5 6 7 |
<?php
foreach ( $result as $v ) { $dec = explode ( " " , $v ) ; $strings [ ] = dechex ( $dec [ 0 ] ) . " " . dechex ( $dec [ 1 ] ) ; } var_dump ( $strings ) ; ?> |
UTF8下得到结果为:
1
2 3 4 5 6 7 8 |
array
(
3
)
{
[ 0 ] => string ( 5 ) "e6 88" [ 1 ] => string ( 5 ) "91 e4" [ 2 ] => string ( 5 ) "bb ac" } |
看到没,成了十六进制了,同理,转换二进制或者八进制只需将dechex函数换成decbin或者decoct就可以了。
现在我们明白汉字编码和转换进制问题了。
接着我们的问题继续,在UTF8页面上实现转换UTF8汉字为十六进制的GBK汉字编码。
首先学习两个函数:strtoupper() 函数把字符串转换为大写;base_convert() 函数在任意进制之间转换数字;iconv() 函数,实现各种字符集间的转换。mb_detect_encoding() 函数,判断汉字编码。
以上是分步进行转换,先得到汉字编码的十进制编码,然后我们用进制转换函数得到我们想要的汉字编码。下面我们依然这样做。
因为我们的页面是UTF8的页面。所以,我们先得到汉字的十进制编码,当然是UTF8下的编码,然后转成UTF8的十六进制编码。然后使用iconv函数进行字符集转换就了。废话少说,看代码:
1
2 3 4 5 6 7 8 9 10 11 12 |
<?php
function convertStr ( $str ) { $strlength = strlen ( $str ) ; $cstr = '' ; for ( $i = 0 ; $i < $strlength ; $i ++ ) { $cstr .= "%" . strtoupper ( base_convert ( ord ( $str { $i } ) , 10 , 16 ) ) ; } return $cstr ; } $contents = ( $contentscharset = mb_detect_encoding ( $s , "ASCII, UTF-8, GB2312, GBK" ) ) == "GB2312" ? $s : iconv ( $contentscharset , "CP936" , $s ) ; $w = convertStr ( $contents ) ; ?> |
现在$w就是我们想要的十进制GBK下的汉字编码了了。
convertStr()函数,把汉字以16进制输出。首先判断汉字编码如果是UTF8则由UTF8转换成GBK的。然后执行convertStr()函数,完成进制转换。得到结果为:%CE%D2%C3%C7,这样,我们就得到了这个UTF8下的GBK汉字编码了。
<?php
//测试时文件的编码方式要是UTF8
$str='中文a字1符';
echo strlen($str).'<br>';//14
echo mb_strlen($str,'utf8').'<br>';//6
echo mb_strlen($str,'gbk').'<br>';//8
echo mb_strlen($str,'gb2312').'<br>';//10
/*
结果分析:在strlen计算时,对待一个UTF8的中文字符是3个长度,所以“中文a字1符”长度是3*4+2=14
在mb_strlen计算时,选定内码为UTF8,则会将一个中文字符当作长度1来计算,所以“中文a字1符”长度是6
*/
//利用这两个函数则可以联合计算出一个中英文混排的串的占位是多少(一个中文字符的占位是2,英文字符是1)
echo (strlen($str) + mb_strlen($str,'UTF8')) / 2;
//例如 “中文a字1符” 的strlen($str)值是14,mb_strlen($str)值是6,则可以计算出“中文a字1符”的占位是10.
echo mb_internal_encoding();
PHP内置的字符串长度函数strlen无法正确处理中文字符串,它得 到的只是字符串所占的字节数。对于GB2312的中文编码,strlen得到的值是汉字个数的2倍,而对于UTF-8编码的中文,就是3倍的差异了(在 UTF-8编码下,一个汉字占3个字节)。
采用mb_strlen函数可以较好地解决这个问题。mb_strlen的用法和 strlen类似,只不过它有第二个可选参数用于指定字符编码。例如得到UTF-8的字符串$str长度,可以用 mb_strlen($str,'UTF-8')。如果省略第二个参数,则会使用PHP的内部编码。内部编码可以通过 mb_internal_encoding()函数得到。需要注意的是,mb_strlen并不是PHP核心函数,使用前需要确保在php.ini中加载 了php_mbstring.dll,即确保“extension=php_mbstring.dll”这一行存在并且没有被注释掉,否则会出现未定义函 数的问题。