aardio 官方社区

 找回密码
 注册会员

QQ登录

只需一步,快速开始

搜索
查看: 17000|回复: 0

理解字符串与 UTF-8 编码

[复制链接]

166

主题

2154

回帖

1万

积分

管理员

积分
13056
发表于 2016-2-1 02:51:18 | 显示全部楼层 |阅读模式
1、字节、字符串
计算机中以八个二进制位表示一个八位字节 - 这称为一个单字节字符。
一组连续的字节就构成一个字符串,在aardio中字符串是基于二进制的,可以包含任何数据(例如图像、文本、或者'\0'等不可打印字符)。
//下面的代码定义了一个字符串变量str
var str = 'abcd中文\0\0\t'

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

2、字节码

每个八位字节实际上存储在内存里就是一个数值 - 我们把这个值称之为字节码。
在 aardio 中你可以把字符串理解为一个字节数组,例如 str[1] 读取字符串的第一个字节码(数值类型),而 str[[1]] 则返回一个字符(字符串类型)

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

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

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

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

1) 文本字符串
文本字符串只包含可正常打印、直接显示的字符,一般的单字节、多字节编码的字符串以'\0'表示字符串终止,而Unicode(aadrio里都指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个二进制位来存储。实际上理论上来说你可以自己设计一套编码规则,例如你可以规定:
1000001 表示"a"
1000011 表示"b"

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中一般没有特定说明,函数或文档中提到的Unicode都是指UTF-16(LE),另外还有一个UTF-16 BE,LE是小端序,BE是大端序,这两种编码的区别是字符串中两个字节的前后位置相反。

UTF-8 他的编码规则很像多字节字符集,是变长编码,而且他跟其他多字节字符集一样,可以方便的转换到其他代码页,例如在aardio中可以这样转换UTF8编码:
//把UTF-8编码转换为GBK编码
str = string.fromto('UTF-8字符串',65001,936)

65001就是UTF-8的代码页,936是简体中文的代码页,因为各种代码页中的字符基本都能在Unicode中找到位置,所以把一种编码转换到Unicode,再从Unicode转换到其他编码,就实现了编码的相互转换, string.fromto() 函数就相当于 string.toUtf16() 加 string.fromUtf16() 函数混合使用的效果。

UTF-16 LE的代码页是1200,UTF-16 BE的代码页是1201,在aardio 10中string.fromto() 函数也可以支持这两个代码页。

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过程如下:
utf8-2.gif
Unicode 0800 ~FFFF 转换为UTF8需要2字节,例如\uF03F (11110000 0011111) 转换为UTF8过程如下:
utf8-3.gif

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);




评分

参与人数 1 +90 收起 理由
aard_vip + 90 很给力!

查看全部评分

您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

手机版|未经许可严禁引用或转载本站文章|aardio.com|aardio 官方社区 ( 皖ICP备09012014号 )

GMT+8, 2025-2-13 07:42 , Processed in 0.065811 second(s), 30 queries .

Powered by Discuz! X3.5

Copyright © 2001-2024 Tencent Cloud.

快速回复 返回顶部 返回列表