Things about Unicode everyone needs to know
golang: Strings, bytes, runes and characters in Go
ASCII码的起源:1.英文字符可用127以内的数字映射,需要7位;2.最初的计算机都是8位的
在ASCII码中,0~32范围的字符称为控制字符,是不可打印的。
接着,不同的制造商就对128~255编码进行自定义。IBM-PC定义了OEM字符集,提供对某些欧洲语言的字符进行表示。随着不同国家的人们开始使用PC,不同的OEM字符集也被定义出来。不同地区的对同一个数字表示的字符具有不同的释义。
最终,这些OEM映射的方案被加入ANSI标准中。在ANSI中,0~127的字符毫无疑问都使统一的ASCII编码,但在128~255的范围内,根据不同的地区采取不同的映射。这些映射被称为code pages。MS-DOS在系统中内置了这些编码映射,如以色列是862,希腊是737。但是,不可能在同一台计算机上同时使用两个编码映射。
同时,在亚洲,字符串的表示更加复杂。因为亚洲的语言通常包含几千个字符,无法使用8位表示。早期使用DBCS(Double Bytes Character Set, 双字节字符集), 有的文字使用单个字节表示,有的使用双字节表示。在这种不定长的编码中,像s++
这样的操作的其实就不再对应字符移动了。但是,大多数人还是按照处理8位字符的方式处理这些字符集,毕竟不需要将这些字符串传送到其他计算机或网络上。但是随着互联网的普及,这些问题逐渐暴露出来。
这是,Unicode出现了。
首先要澄清一点,Unicode不是一个"所有字符都是用16位进行编码"的字符集。
在Unicode中,要思考几个问题:
经过近10年的讨论,Unicode的制定者已经讨论出了上述问题的答案。
在Unicode中,这个星球上的任何一个字符都被映射到一个唯一的数字,表示为:U+0639
,这个数字被称为code point
。U+
表示这是一个Unicode表示。可以使用charmap
(Windows系统)查看所有的字符映射,也可以访问Unicode官网。
Unicode对于所要表示的字符数没有限制,因此并不是所有字符都是16位的。
同一个字符也有多种表达方式:à,作为单个字符时是U+00E0;是可以通过 U+0300 U+0061的组合来产生。U+0300表示音调,U+0061即a.
Hello,使用Unicode表示为:U+0048 U+0065 U+006C U+006C U+006F。
这是Unicode的最早编码方式,也是人们认为Unicode使用16位表示一个字符的起源。
在内存中,可以按照大端尾或小端尾分别表示:
大端尾:00 48 00 65 00 6C 00 6C 00 6F
小端尾:48 00 65 00 6C 00 6C 00 6F 00
所以,这就是FE FF
的起源。Unicode文本的开头使用FE FF
来标识字节的顺序,称为Unicode Byte Order Marker(BOM)。通过识别起始的两个字符是FE FF
(大端尾)还是FF FE
(小端尾)来判断文本是大端尾还是小端尾。
使用file
命令测试:
$ echo -ne '\xFE\xFF' > FEFF.txt
$ echo -ne '\xFF\xFE' > FFFE.txt
$ file FFFE.txt FEFF.txt
FFFE.txt: Little-endian UTF-16 Unicode text, with no line terminators
FEFF.txt: Big-endian UTF-16 Unicode text, with no line terminators
由于最初Unicode采用两个字符编码,导致仅英文的文本存储占用空间翻倍。此外,已经存在了大量的ASCII和DBCS编码的文本,Unicode的表示无法兼容。所以一开始,人们都选择无视Unicode。
UTF-8编码解决了兼容性和存储的问题。0~127的字符都是用单个字节表示,只有128以上的字符,才会使用2~6个字节来存储。
单字节: 0vvvvvvv
双字节:110vvvvv 10vvvvvv
3字节:1110vvvv 10vvvvvv 10vvvvvv
4字节:11110vvv 10vvvvvv 10vvvvvv 10vvvvvv
5字节:111110vv 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv
6字节:1111110v 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv
注:解释编码时,需要将所有的v组合在一起,得出code point.
此前使用16位编码的格式成为UCS-2
或UTF-16
,对应的还有UCS-4
。UTF-7
则是UTF-8
的一种特化,它保证最高位总是0,因为有些系统认为7位足以表示。
如果一个code point是不可打印的,unicode会打印出?或者�(0xfffd, 65533)。
�的UTF-8编码是 0xef 0xbf 0xbd(3字节编码), 如果使用python -e 'print(ord(“�”))'则会得到65533。
如果使用UTF-8解释一些包含128以上编码的文本,则可能得到一大堆�。
在计算机中,不存在Plain Text这样的东西。ASCII不意味着Plain Text。
如何表示字符串的编码?在Email中, 通过Header来表示:
Content-Type: text/plain; charset="UTF-8"
网页通常使用Content-Type
来指示文件的编码,但是站在Server的角度考虑,HTML的内容通常来自于文件,如果不同的文件使用不同的编码,就不能简单的使用一个固定的Content-Type
。一般是通过HTML HEADER
来表示:
注意:meta
必须成为HTML的第一个标签。
如果没有Content-Type,该如何决定文件编码呢?IE使用一个策略:通过统计不同语言编码中各个字节的出现频率,试图找出最匹配的那个编码。
皮斯特法则,鲁棒性原则(出现于TCP协议):对输出持保守态度,对输入持开放态度
程序表示和编码,在python中,运行下面代码:
>>> a='�����'
>>> len(a)
5
>>> a.encode('utf-8')
b'\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd'
>>> len(a.encode('utf-8'))
15
变量a
包含5个unicode字符,但是只有使用encode
才能将其转换成utf-8格式。
在c++中,使用L"Hello"
来表示一个UCS-2编码的字符串,字符的类型是wchar_t
,使用wcs
系列函数替换str
系列函数(wcslen
)。现如今,go默认使用UCS-2编码存储字符串。
在golang中,字符串实际上只是一组字节。但是因为go的源代码一定是UTF-8编码的,源代码中的字符串字面量在保存时就已经是UTF-8编码的字节了。
使用fmt.Printf
时,实际上是向console写入了一组UTF-8编码的字节流。只有console的编码也是UTF-8时,才能正确显示出字符串。
code point或许显得有些拗口,所以go使用rune
来表示这个概念,rune
和code point是等价的,除此之外,rune
使用32位存储。
range loop:在go中,对字符串进行range循环,会将字符串进行UTF-8解码:
const nihongo = "日本語"
for index, runeValue := range nihongo {
fmt.Printf("%#U starts at byte position %d\n", runeValue, index)
}
index还是对应字符串中的起始位置,但是得到的类型却是rune
。
U+65E5 '日' starts at byte position 0
U+672C '本' starts at byte position 3
U+8A9E '語' starts at byte position 6
那么,遍历一个包含非法UTF-8编码的字符串会发生什么呢?
package main
import (
"fmt"
)
func main() {
var s string = "\xef\x0a"
fmt.Print(s)
fmt.Print("\nwill loop\n")
for i,r := range s {
fmt.Printf("%d:%U\n",i,r)
}
}
go不会抛出异常,而是将其解释为�。
�
will loop
0:U+FFFD
1:U+000A
encoding/utf8
库的使用示例:
const nihongo = "日本語"
for i, w := 0, 0; i < len(nihongo); i += w {
runeValue, width := utf8.DecodeRuneInString(nihongo[i:])
fmt.Printf("%#U starts at byte position %d\n", runeValue, i)
w = width
}
总结:go字符串的性质
rune
简而言之,一个字符,比如:à在unicode中有多种表示。正则化的意思是,使同一个字符尽量使用同一种表示。