aardio 文档

理解字符串与 UTF-8 编码

  1. 字节、字符串

    计算机中以八个二进制位表示一个八位字节 - 这称为一个单字节字符。

    一组连续的字节就构成一个字符串,在aardio中字符串是基于二进制的,可以包含任何数据(例如图像、文本、或者'\0'等不可打印字符)。

    //下面的代码定义了一个字符串变量str  
    var str = 'abcd中文\0\0\t'
    

    上面的是一个最基础的字符串,注意上面的字符串放在单引号号,单引号中可以使用转义符,例如使用'\t'表示制表符,放在双引号中则不能使用转义符(所以双引号中只能包含可见的文本字符),关于怎么表示一个字符串的更多语法请参考aardio语法手册,这里不再详述。

  2. 字节码

    每个八位字节实际上存储在内存里就是一个数值 - 我们把这个值称之为字节码。

    在 aardio 中你可以把字符串理解为一个字节数组,例如 str[1] 读取字符串的第一个字节码(数值类型),而 str[[1]] 则返回一个字符(字符串类型)

    在aardio 是可以使用 #str 获取一个字符串包含的字节数(也就是字符串长度)。

    请运行下面的 aardio 代码:

    import console;  
    console.log("ab的长度为2" ,#"ab" ); //每个英文字符占一个字节  
    console.log("'中文'的长度为6" ,#"中文" ); //UTF8是多字节变长编码,这里的两个汉字占用6字节  
    console.pause(true);
    

    可以看到一个英文字符占1个字节,而一个汉字占用多个字节。

  3. 文本字符串、二进制字符串

    1. 文本字符串
      文本字符串只包含可正常打印、直接显示的字符,一般的单字节、多字节编码的字符串以'\0'表示字符串终止,而 UTF-16 编码的字符串以'\u0000'为终止符。很明显的,文本字符串不能在内容中包含终止符。
    2. 二进制字符串
      二进制字符串指的是可以包含任何字节码,当然也可以包含文本,文本也是二进制,但我们一般说的二进制字符串指的是他不仅仅可以包含文本,目的也不仅仅是用于直接打印显示。显然,二进制字符串可以在内容中包含'\0','\u0000'这些,不再是终止符。二进制字符串会自己记录自己的长度,而不是依赖终止符去获取长度。

      aardio中的字符串是二进制字符串,记录自己的长度而不是依赖终止符取长度,例如在C语言中用 strlen(str) 取一个字符串的长度,就需要一个字节一个字节的往后找 - 直到遇到终止符'\0',Unicdoe字符串也类似,所以把 aardio 的字符串传入C语言等实现的接口参数时,aardio还是会保证字符串尾部有终止符'\u0000' 以避免越界操作,这个保护性的终止符是隐藏的( 在aardio字符串中看不到这个终止符,取字符串长度也不会包含终止符 )。在 aardio 中用 raw.buffer() 分配内存缓冲区时,也会有类似的机制在尾部隐藏的放置一个保护性的终止符 '\u0000'(你在aardio代码中访问不到他,取长度也不会包含终止符)。

  4. 字符编码、字符集、ASCII编码

    请在 aardio 中运行下面的代码,查看控制台输出:

    import console;
    var str = "ab";  
    console.log("输出'a'的字节码97",str[1]);  
    console.log("输出字符'b'",str[[2]]);  
    
    console.pause(true);
    

    实际上"a" 在内存中存储的值就是一个数值 97 ,而97转换的2进制为 1100001

    可以看到一个字节码使用了8个二进制位来存储。实际上理论上来说你可以自己设计一套编码规则,例如你可以规定:

    ANSI已经制定了ASCII编码用于表示单字节字符集,常用的英文字符即是使用ASCII编码表示,例如上面的97( 2#1100001 ) 表示字母"a"就是ASCII规定的。大家可以自行网上搜索ASCII码表看一看了解一下,这里不再详述。

  5. 多字节字符集

    用单个字节(8个二进制位)能表示的字符数量非常有限,这对英文世界不是问题,但对中、日、韩这样的文字就是问题了,所以各个有了各个国家不同的多字节编码方案,用多个字节来表示一个字符,例如中文里的GBK(GB2312)编码,繁体中文的BIG5编码,日文的Shift-JIS编码,多字节编码即 MBCS(Multi-Byte Chactacter System),MBCS的常见实现是DBCS(Double-Byte Character Set) - 也就是双字节字符集,双字节字符集的基本编码规则如下:

    1. 小于等于0x80的字节码表示ASCII单字节字符,英文字母数字符号等,注意'\x80'是欧元符号(也就是最大的一个单字节码),ASCII中小于32的是控制字符,至于这个32就是空格符了。
    2. 大于0x80的字节表示他是一个双字节字符,他后面还跟了一个尾字节共同组成一个字符,常见的几种中日韩编码(代码页 936,950,932,949)尾字节不小于0x40,这个0x40就是"@"这个字符,所以理论上字节码小于"@"的一般做二进制的搜索拆分就比较安全。

    多字节编码的好处是比较节省存储空间,兼容一些二进制的字符串操作,但是有一个比较麻烦的是,例如你用一个单字节字符来分隔字符串,但是他找啊找找到某个双字节字符的尾字节,然后拆分什么的,出来的结果就乱套、乱码了(这个问题我们称之为“串码”),所以这种多字节处理起来就比较麻烦(你不能直接用处理二进制字符串的代码去处理文本,因为你得不停的分析某个字节是不是一个多字节字符的尾字节),另外各国使用的编码不一致,这就导致经常出现乱码等问题。

  6. ANSI 代码页( Code Page ),Unicode编码。

    各种语言使用的多字节字符集并不统一,ANSI 代码页就是用来告诉操作系统当前使用的是哪种语言的字符集,例如把系统代码页设为 936 就支持简体中文,而繁体 BIG5 编码的代码页就是 950。

    因为不同的字符集并不统一,所以就有了 Unicode 统一编码这种东西,各种语言在这个 Unicode 里的编码都是统一的,这个 Unicode 的知识这里就不细讲了,网上太多。Unicode有多种不同的编码方案,常用的是 UTF-16 LE, UTF-8 , UTF-16 每个字符用两个字节表示,aardio 中一般没有特定说明,函数或文档中提到的 UTF-16 都是指 UTF-16 LE,另外还有一个UTF-16 BE,LE 是小端序,BE 是大端序,这两种编码的区别是字符串中两个字节的前后位置相反。

    UTF-8 他的编码规则很像多字节字符集,是变长编码,而且他跟其他多字节字符集一样,可以方便地转换到其他代码页,例如在 aardio 中可以这样转换 UTF-8 编码:

    //把 UTF-8 编码转换为 GBK 编码  
    str = string.fromto('UTF-8 字符串',65001,936)
    

    65001 就是 UTF-8 的代码页,936 是简体中文的代码页。因为各种代码页中的字符基本都能在Unicode 中找到位置,所以把一种编码转换到 Unicode( UTF-16 ) ,再从 Unicode( UTF-16 ) 转换到其他编码,就实现了编码的相互转换。

    aardio 里实现这个功能的就是 string.fromto 函数, string.fromto() 函数就先调用 string.toUtf16() 将字符串的编码转换到 Unicode( UTF-16 ),然后再调用 string.fromUtf16() 将字符串从 Unicode( UTF-16 ) 转换到指定编码。

    UTF-16 LE 的代码页是 1200,UTF-16 BE 的代码页是1201。string.fromto() 函数也可以支持这两个代码页,也就是说 string.fromto 的参数可以指定任意编码方案,包括 UTF-16 它自己。

  7. UTF-8 编码

    UTF-8编码是变长编码,但他有一个好处是很像多字节编码,兼容单字节编码,类似英文ASCII字符这些仍然只要一个字节存储,而且编码与ASCII兼容。UTF-8的代码是变长的,其编码规则如下:

    /*  
    * 0000 0000-0000 007F - 0xxxxxxx 单字节  
    * 0000 0080-0000 07FF - 110xxxxx 10xxxxxx 双字节  
    * 0000 0800-0000 FFFF - 1110xxxx 10xxxxxx 10xxxxxx 3字节  
    */
    

    Unicode 0080 ~07FF 转换为UTF8需要2字节,例如 \u00CA (11001010)转换为UTF8过程如下:

    Unicode 0800 ~FFFF 转换为UTF8需要2字节,例如\uF03F (11110000 0011111) 转换为UTF8过程如下:

    UTF8的编码比较特别,小于0x80(最大0x7F)一定是单字节,多字节的前导字节用前导二进制位表示自己有几个字节,附加字节总是10xxxxxx,所以比较容易用代码分析一段字符串是否使用UTF8编码,例如Windows上的记事本就是通过这种机制自动识别一个文本文件的编码,aardio中也有一个类似的函数,即 string.isUtf8(字符串) , 这个函数如果检测到一个合法的UTF8编码的字符串会返回true。

    UTF8中一个字符有多个字节时 - 每个字节一定不会小于 0x80,这带来一个好处:以处理二进制字符串的函数(例如模式匹配)操作ASCII字符不会出现MBCS编码那样的串码问题。但如果用支持MBCS编码的代码去处理UTF-8字符串会出问题,因为在UTF8中一个三字节的中文字符、加上一个单字节的英文字符,用支持MBCS编码的函数去处理,会被理解还两个双字节字符,结果就乱了, 下面我们用一段 aardo 代码演示一下这种乱码出现的过程:

    import console;/*  
    有一些用Unicode是没法表示的编码(无效码点),  
    Unicode规定用Replacement Character( \uFFFD ) 表示(显示为:� )。  
    */  
    var str = '\uFFFD\uFFFD\uFFFD'  
    
    //\uFFFD 在 UTF8 中会编码为 \xEF\xBF\xBD  
    console.log('\xEF\xBF\xBD'=='\uFFFD') //输出:true  
    
    /*  
    '\xEF\xBF\xBD\xEF\xBF\xBD' 按 GBK编码 2个字节一个字符就是 "锟斤拷"  
    因为 aardio 中的控制台兼容 ANSI 编码,所以可以这样写  
    */  
    console.log('\xEF\xBF','\xBD\xEF','\xBF\xBD');//输出:锟  斤 拷  
    
    /*  
    aardio 字符串自带 UTF标记,具有 Unicode 编码自我识别能力,  
    而 '\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD' 是合法的 UTF-8 编码,  
    所以下面的字符串会显示正确的字符:���  
    */  
    console.log('\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD' ) //输出:���  
    
    /*  
    下面这样也改变不了编码,  
    因为 aardio 字符串带 UTF 标记,能自动纠正错误的编码转换。  
    */  
    var str  = string.fromto('\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD',936,65001)  
    console.log(str=='\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD') //输出:true  
    
    /*  
    如果用 buffer 创建二进制字节数组,逃脱 aardio 的 UTF 标记检测,就可以重现这个 BUG 。  
    */  
    var str  = string.fromto(raw.buffer('\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD'),936,65001)  
    console.log(str) //输出:锟斤拷锟?  
    
    console.log("ANSI 编码仅仅是历史包袱,已经没有存在的意义和使用价值。 ")  
    console.pause(true);
    

Markdown 格式