字符串编码(utf8)

文章

Things about Unicode everyone needs to know
golang: Strings, bytes, runes and characters in Go

编码发展的历史

早期自由定义的编码集

ASCII码的起源:1.英文字符可用127以内的数字映射,需要7位;2.最初的计算机都是8位的
在ASCII码中,0~32范围的字符称为控制字符,是不可打印的。

字符串编码(utf8)_第1张图片

接着,不同的制造商就对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

首先要澄清一点,Unicode不是一个"所有字符都是用16位进行编码"的字符集。

在Unicode中,要思考几个问题:

  1. 不同字体的字符应当使用同一个编码吗?
    在这里插入图片描述
    在这里插入图片描述
  2. 在德语中,ß是一个真正的字符吗?还是仅仅是ss的一种书写方式?
  3. 如果一个字符的末端发生变化,它们还是同一个字符吗?希伯来文(Hebrew)认为不是,阿拉伯文则认为仍然相同

经过近10年的讨论,Unicode的制定者已经讨论出了上述问题的答案。

在Unicode中,这个星球上的任何一个字符都被映射到一个唯一的数字,表示为:U+0639,这个数字被称为code pointU+表示这是一个Unicode表示。可以使用charmap(Windows系统)查看所有的字符映射,也可以访问Unicode官网。

Unicode对于所要表示的字符数没有限制,因此并不是所有字符都是16位的。

同一个字符也有多种表达方式:à,作为单个字符时是U+00E0;是可以通过 U+0300 U+0061的组合来产生。U+0300表示音调,U+0061即a.

Unicode的计算机表示:编码

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

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-2UTF-16,对应的还有UCS-4UTF-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中字符串

在golang中,字符串实际上只是一组字节。但是因为go的源代码一定是UTF-8编码的,源代码中的字符串字面量在保存时就已经是UTF-8编码的字节了。
使用fmt.Printf时,实际上是向console写入了一组UTF-8编码的字节流。只有console的编码也是UTF-8时,才能正确显示出字符串。

code point或许显得有些拗口,所以go使用rune来表示这个概念,runecode 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字符串的性质

  • go源代码总是UTF-8编码
  • 一个字符串可以包含任何字节
  • 一个字符串字面量总是有效的UTF-8序列
  • code point被称为rune
  • 字符串不保证正则化

golang正则化: normalization

简而言之,一个字符,比如:à在unicode中有多种表示。正则化的意思是,使同一个字符尽量使用同一种表示。

你可能感兴趣的:(go,golang)