中文内容转码UTF8解决方案

              在最近的工作中遇到一些中文编码方式的问题,具体描述如下。

              工作内容:审计邮件的内容,并对收发件人进行匹配,若命中则记录邮件内容(数据库中字符串最长1024个字节)到数据库,整个过程编码为UTF8,即需要将邮件内容转码成utf8存储,用到库函数iconv函数转化。

              遇到的问题:在实际网络流量中,用iconv(这个函数的各个参数及返回值详见附录1)转化出现转换失败的问题;即使转换成功,切转化之后的长度大于1024,需要截取1024个字符入库有非法字符的情况,入库失败。

问题分析:

              问题一分析:若邮件长度比较长,在数据包中会进行分片,如图1,2所示,图1是内容分片的第一部分,图2是内容分片的第二部分,我们的系统按照内容分片进行转码,如果一段比较长的邮件内容被分成n(>=2)段,则可能一个中文在数据流中被截断了,也就是说在这个分片的最后和下一份分片的开头,把这两部分组合起来才是一个汉字,故iconv转化失败了,报错。

中文内容转码UTF8解决方案

图1

中文内容转码UTF8解决方案

图2

              问题二分析:因为数据库存储邮件内容的字段有长度限制,所以需要在邮件内容过长的情况下对其进行截取,如果截取存在把一个汉字截成两片的情况就会出现入库报错。如图3所示。

              这说明入库的内容中存在非法字符“\xE6\x95”,数据库报错。根据utf8编码规则(详见附录2),\xE6转化为二进制为11100110,\x95转化为二进制为1001 0101,这说明这个汉字包含三个字节,而因为此汉字的三个字节分别为字符串的第1023和1024和1025个字节,故其被从1024处截断了。

解决方案:

              对于问题1,我们假设一个邮件内容因为长度较长被在数据流中分成了三个流,一个中文在第一个和第二个流之间被截断了,第二个和第三个流被正常分割。因为我们的系统是一个流一个流的处理,这时我们转化第一个流肯定失败,我们的解决方案是第一个流转化失败就先把它存起来,等第二个流来了之后把第一个流和第二个流合并起来进行统一转码,这个时候由于把分割的汉字拼在一起转码成功了。

              当然,存在每一个流里都分割了中文的情况,这个可以按照上述方法,先进行存储然后转化,如果可以都存起来就可以避免中文被分割的情况,从而实现多次存储一次转码的方案。

              对于问题二,我们的解决方案是判断最后一个字节的值,即用它&\0xFF,如果小于128说明此为只有一个字符,应该是一个英文或者一个数字,可以直接截断到此处。如果大于128 小于192则说明次字符不止一个字节,所以一直向前找,找到大于192的为止,这说明这是这个汉字的头一位,直接截取到这个位置就可以了。

如上方法可能存在最后一个字节是本汉字的最后一个字节,应该可以截断,但我们没有而是继续向上截取上一个的情况,因为对于多个字节的情况,最后一个字节和中间的字节都是大于128而小于192的,不好做判断。

 

附件1:

inconv_t cd:函数iconv_open()分配的编码转换句柄。

 

char **restrictinbuf:指向需要编码转换的缓冲区。(其中关键字restrict只用于限定指针;该关键字用于告知编译器,所有修改该指针所指向内容的操作全部都是基于(base on)该指针的,即不存在其它进行修改操作的途径;这样的后果是帮助编译器进行更好的代码优化,生成更有效率的汇编代码。)

 

size_t *restrictinbytesleft:inbuf中还需要编码转换的字节数。

 

char **restrictoutbuf:指向存放转码的缓冲区。

 

size_t *restrictoutbytesleft:outbuf中还可以存放转码的字节数,也就是outbuf中的剩余空间。

 

返回值:是size_t类型的值,若返回值为ret,可以用(ret==-1)判断转码是否成功,-1为不成功

 

附件2:

UTF-8是一种变长字节编码方式。对于某一个字符的UTF-8编码,如果只有一个字节则其最高二进制位为0;如果是多字节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的位数,其余各字节均以10开头。UTF-8最多可用到6个字节。 
如表: 
1
字节 0xxxxxxx 
2
字节 110xxxxx 10xxxxxx 
3
字节 1110xxxx 10xxxxxx 10xxxxxx 
4
字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 
5
字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 
6
字节 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 
因此UTF-8中可以用来表示字符编码的实际位数最多有31位,即上表中x所表示的位。除去那些控制位(每字节开头的10等),这些x表示的位与UNICODE编码是一一对应的,位高低顺序也相同。 
实际将UNICODE转换为UTF-8编码时应先去除高位0,然后根据所剩编码的位数决定所需最小的UTF-8编码位数。 
因此那些基本ASCII字符集中的字符(UNICODE兼容ASCII)只需要一个字节的UTF-8编码(7个二进制位)便可以表示。 

对于上面的问题,代码中给出的两个字节是 
十六进制:C0 B1 
二进制:11000000 10110001 
对比两个字节编码的表示方式: 
110xxxxx 10xxxxxx 

提取出对应的UNICODE编码: 
00000 110001 

可以看出此编码并非“标准”的UTF-8编码,因为其第一个字节的“有效编码”全为0,去除高位0后的编码仅有6位。由前面所述,此字符仅用一个字节的UTF-8编码表示就够了。 
JAVA
在把字符还原为UTF-8编码时,是按照“标准”的方式处理的,因此我们得到的是仅有1个字节的编码。