字符编码

json decode

最近项目要开始和服务器通讯了,客户端使用的是quick cocos,服务器的一些服务使用php开发。发送请求后服务器返回的是json格式的字符串,一般字符都没有问题,遇到中文后返回的是“\u90ae\u7bb1\u4e0d\u5b58\u5728”这样的字符串,客户端解析后没有把对应编码的中文显示出来,而是直接得到“u90aeu7bb1u4e0d”,显然这是不对的。

google一下发现是解码问题,“\u90ae\u7bb1\u4e0d\u5b58\u5728”使用的是unicode符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。

网上建议的处理方式是更换一个json解析库,让它可以处理\u前缀的unicode编码。这里有给出所有lua可以使用的json模块,我挑选了dkjson一个纯lua实现的json解析库,支持0~0x10ffff范围内的unicode编码。

但是在所有的json解析库里面quick cocos自带的cjson是速度最快的,具体对比在这里

所以这个修改还是有待商榷的,程序的世界里总是在寻找一个最优的解决方案,如果你认为当前的方法是现在及以后的最优解,这只能说明你不是很在乎自己在做什么。

ascii unicode utf-8

ascii编码

计算机键盘上的符号数数也不是很多,ascii编码使用一个字节来表示绰绰有余,ascii编码一共规定了128个字符的编码,比如空格是32,大写字母A是65。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面1位统一规定为0。

unicode字符集

计算机现在不是单单给以英语为母语的人使用的,越来越多的语言被加入其中。每种语言都有自己的编码方式,比如中文就有GB2312,GBK这种。如果网上一篇文章既使用了中文又使用了日文来编写,那处理这篇文章的程序写起来岂不是非常蛋疼。

我们需要统一编码,将这个世界上的所有符号都纳入其中。每一个符号都给予一个独一无二的编码,这样所有人都开心了。这就是unicode,正如同它的名字一样,这是一种所有符号的编码。这里的编码和后面的utf-8的编码要严格区分开来,unicode编码是一种A到B的影射机制,比如你说苹果,我就知道是apple一样,具体这个apple应该如何写到电脑里那就是另外一个意思的编码。具体的符号对应表,可以查询unicode.org或者专门的汉字对应表

unicode字符集将来会使用4个字节进行编码,目前只用到了3个字节。这个新的4字节字符集叫做UCS-4(Unicode Character Set)。目前的3个字节中,第一个字节被称作平面,也就是大范围00x10ffff里面的00x10一共16个平面,后面两个字节,一个行,一个列。具体看下图:

在第0个平面里x轴的范围是0~0xff一共256个刻度,y轴和x轴一样。在这个坐标系下面一共可以表示65536个整数点。这些整数点可以表示将近3万个汉字以及世界各地的文字和字符了。事实上这个平面都还没有被填满。这个平面被称为BMP(Basic Multilingual Plane)也就是UCS-2。

utf-8编码

unicode字符集听起来好像是非常厉害没啥问题了,但是仔细一想存在两个严重的问题。

  1. 如何区分unicode和ascii?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号?

  2. 原来有的ascii字符如果使用3到4字节的unicode编码会白白浪费2到3个字节,这显然是不合理的。

互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种Unicode的实现方式。其他实现方式还包括UTF-16(字符用两个字节或四个字节表示)和UTF-32(字符用四个字节表示),不过在互联网上基本不用。UTF-8编码是Unicode字符集的实现方式之一

UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。UTF-8的编码规则很简单,只有二条:

  1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。

  2. 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
    下表总结了编码规则,字母x表示可用编码的位。

Unicode符号范围 | UTF-8编码方式

——————–+———————————————

0000 0000-0000 007F | 0xxxxxxx

0000 0080-0000 07FF | 110xxxxx 10xxxxxx

0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx

0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

我们拿文章开头那段字符串“\u90ae\u7bb1\u4e0d\u5b58\u5728”举例:

5个符号全部在第三排的范围内

90ae 二进制串:1001 000010 101110 UTF-8串:11101001 10000010 10101110 0xe982ae

7bb1 二进制串:0111 101110 110001 UTF-8串:11100111 10101110 10110001 0xe7aeb1

4e0d 二进制串:0100 111000 001101 UTF-8串:11100100 10111000 10001101 0xe4b88d

5b58 二进制串:0101 101101 011000 UTF-8串:11100101 10101101 10011000 0xe5ad98

5728 二进制串:0101 011100 101000 UTF-8串:11100101 10011100 10101000 0xe59ca8

这5个字符的utf-8编码为:e982ae e7aeb1 e4b88d e5ad98 e59ca8

lua 字符串处理

一个lua字符串是任意字节序列的组合(字节就是C语言编译器里面char的大小,可能是8bit或者更多)。lua没有保留任何值包括NUL,所以任何二进制数据,包括unicode编码数据都可以存放在lua字符串中。

最好情况下,使用unicode字符集的编码单元不大于1个字节,通常这要求你使用utf-8编码。但这不是必须的,任何UTF-16,UTF-32和它们大端小端格式的变体都可以被正确的储存。

在lua中字符串的输入输出符合c语言规范。在二进制模式下标准c只需要stdio库来处理任何数据。在文本模式下运行环境可以转换输入输出字符串里面的行结束符或者是非ascii编码的字符集合。如果你在使用一个库,但是没有按照这个库所要求的编码格式对字符串编码,那么你会得到一个被转换的行结束符或者是乱码了。

使用上面得到的utf-8编码后的乱码字符串,每3个字节组成一个字符,我们就可以解码得到正确的值了,是不是很简单。

local aa = string.format("%c%c%c %c%c%c %c%c%c %c%c%c %c%c%c", 
    0xe9, 0x82, 0xae, 
    0xe7, 0xae, 0xb1,
    0xe4, 0xb8, 0x8d,
    0xe5, 0xad, 0x98,
    0xe5, 0x9c, 0xa8
)
print(aa)
-- output
-- 邮 箱 不 存 在

参考

字符编码笔记:ASCII,Unicode和UTF-8

速解UTF-8中文字符,方法和原理

Lua Unicode

Share