字符编码

本文介绍了字符编码的基本概念,包括ASCII字符集的局限性、GB2312为中文增设的编码、Unicode的统一序号系统及其变长编码UTF-8和UTF-16。重点讲解了不同字符集的编码规则和转换,以及乱码产生的原因及解决办法。
摘要由CSDN通过智能技术生成

3.4 字符编码

内容导视:

  • 字符集与字符编码

  • ASCII 字符集

  • GB2312 字符集

  • Unicode 字符集

计算机是以二进制的形式来存储数据的,我们在屏幕上看到的文字,在存储之前都被转换成了二进制(编码),在显示时也要根据二进制找到对应的字符。(解码)

字符集定义了文字和二进制的对应关系,为字符分配了唯一的编码。

如 ASCII 字符集定义的 ‘a’ 的编码是 01100001,如果把它当作整数的补码,则为 97。

3.4.1 字符集与字符编码

字符集规定了某个文字对应的二进制数字存放方式(编码)和某串二进制数值代表了哪个文字(解码)的转换关系。

字符集只是一个规则集合的名字,对应到真实生活中,字符集就是对某种语言的称呼。例如:英语,汉语。

对于一个字符集来说,要正确编码转码一个字符需要三个关键元素:字库表(character repertoire)、编码字符集(coded character set)、字符编码(character encoding form)。

  • 字库表中装着所有字符
  • 编码字符集:即用一个序号(code point)来表示一个字符在字库中的位置(所使用的进制任意),如 ‘a’ 在 ASCII 字库表中的位置为 0x61。
  • 字符编码:编码字符集和实际存储二进制之间的转换关系。一般来说直接将 code point 的值转为二进制直接存储。例如在 ASCII 字库中 ‘A’ 的序号为 65,转为二进制 01000001,直接存储。

3.4.2 ASCII 字符集

American Standard Code for Information Interchange:美国信息交换标准代码

ASCII 字符集是由美国人发明的,没有考虑欧洲那些扩展的拉丁字母,也没有考虑韩语和日语、汉字。

ASCII 的标准版本于 1967 年第一次发布,最后一次更新则是在 1986 年,迄今为止共收录了 128 个字符,包含了基本的拉丁字母(英文字母)、阿拉伯数字(1234567890)、标点符号(,.! 等)、特殊符号(@#$%^& 等)以及一些具有控制功能的字符(往往不会显示出来)。1 个字节表示这些符号绰绰有余。

在 ASCII 编码中,大写字母、小写字母和阿拉伯数字都是连续分布的(见下表),这给程序设计带来了很大的方便。例如要判断一个字符是否是大写字母,就可以判断该字符的 ASCII 编码值是否在 65~90 的范围内。

3.4.2-1 ASCII 字库表
二进制十进制十六进制字符/缩写解释
00000000000NUL (NULL)空字符
00000001101SOH (Start Of Headling)标题开始
00000010202STX (Start Of Text)正文开始
00000011303ETX (End Of Text)正文结束
00000100404EOT (End Of Transmission)传输结束
00000101505ENQ (Enquiry)请求
00000110606ACK (Acknowledge)回应/响应/收到通知
00000111707BEL (Bell)响铃
00001000808BS (Backspace)退格
00001001909HT (Horizontal Tab)水平制表符
00001010100ALF/NL(Line Feed/New Line)换行键
00001011110BVT (Vertical Tab)垂直制表符
00001100120CFF/NP (Form Feed/New Page)换页键
00001101130DCR (Carriage Return)回车键
00001110140ESO (Shift Out)不用切换
00001111150FSI (Shift In)启用切换
000100001610DLE (Data Link Escape)数据链路转义
000100011711DC1/XON (Device Control 1/Transmission On)设备控制1/传输开始
000100101812DC2 (Device Control 2)设备控制2
000100111913DC3/XOFF (Device Control 3/Transmission Off)设备控制3/传输中断
000101002014DC4 (Device Control 4)设备控制4
000101012115NAK (Negative Acknowledge)无响应/非正常响应/拒绝接收
000101102216SYN (Synchronous Idle)同步空闲
000101112317ETB (End of Transmission Block)传输块结束/块传输终止
000110002418CAN (Cancel)取消
000110012519EM (End of Medium)已到介质末端/介质存储已满/介质中断
00011010261ASUB (Substitute)替补/替换
00011011271BESC (Escape)逃离/取消
00011100281CFS (File Separator)文件分割符
00011101291DGS (Group Separator)组分隔符/分组符
00011110301ERS (Record Separator)记录分离符
00011111311FUS (Unit Separator)单元分隔符
001000003220(Space)空格
001000013321!
001000103422"
001000113523#
001001003624$
001001013725%
001001103826&
001001113927
001010004028(
001010014129)
00101010422A*
00101011432B+
00101100442C,
00101101452D-
00101110462E.
00101111472F/
0011000048300
0011000149311
0011001050322
0011001151333
0011010052344
0011010153355
0011011054366
0011011155377
0011100056388
0011100157399
00111010583A:
00111011593B;
00111100603C<
00111101613D=
00111110623E>
00111111633F?
010000006440@
010000016541A
010000106642B
010000116743C
010001006844D
010001016945E
010001107046F
010001117147G
010010007248H
010010017349I
01001010744AJ
01001011754BK
01001100764CL
01001101774DM
01001110784EN
01001111794FO
010100008050P
010100018151Q
010100108252R
010100118353S
010101008454T
010101018555U
010101108656V
010101118757W
010110008858X
010110018959Y
01011010905AZ
01011011915B[
01011100925C\
01011101935D]
01011110945E^
01011111955F_
011000009660`
011000019761a
011000109862b
011000119963c
0110010010064d
0110010110165e
0110011010266f
0110011110367g
0110100010468h
0110100110569i
011010101066Aj
011010111076Bk
011011001086Cl
011011011096Dm
011011101106En
011011111116Fo
0111000011270p
0111000111371q
0111001011472r
0111001111573s
0111010011674t
0111010111775u
0111011011876v
0111011111977w
0111100012078x
0111100112179y
011110101227Az
011110111237B{
011111001247C|
011111011257D}
011111101267E~
011111111277FDEL (Delete)删除

如 ‘a’ 存储时以 97 对应的二进制 01100001 存储。

后来欧洲人发明了 ISO8859-1。ISO8859-1 包含了 ASCII 中的所有字符(兼容),还加入了一些西欧字符。

兼容:新字符集包括原有字符集中的所有字符,且这些字符的编码在原有字符集中也是如此,如 ‘a’ 在 ASCII 的编码是 01100001,在 GBK 的编码也是 01100001,就称新字符集兼容原有字符集。

3.4.3 GB2312 字符集

ASCII 字符集中没有包含中文,国人规定了 GB2312 字符集,使用 2 个字节表示一个汉字(因为汉字太多,1 个字节最多表示 256 个字符),英文还是 1 个字节,兼容 ASCII 码。

那么存储时好存,找到字符对应的字符编码(序号转为二进制)存储即可,但对于非定长编码,读取时怎么知道是按 1 个字节读还是 2 个字节读。

101110011111101001100001,凡是汉字,对应字符编码都以 1 开头,所以读取 2 个字节,1011100111111010 对应汉字 ‘国’,以 0 开头,读取 1 个字节,01100001 对应 ‘a’。

GB2312 编码范围:0xA1A1 ~ 0xFEFE,其中汉字编码范围:0xB0A1 ~ 0xF7FE。

分为 94 个区,每个区有 94 个数,点击 GB2312 编码表 查看详细。

通过序号查字符

0xA1A2,前两位是区号,后两位是区中的第几个数。

A1A2 代表 01 区的第 2 个数。(可以看出规律:01 + A0 = A1,02 + A0 = A2)

3.4.3-1 01 区中的字符

请添加图片描述

通过字符查序号

以 ‘包’ 为例,它在 16 区的第 92 个数,那么对应的十六进制的序号是多少?把它们转成 16 进制分别与 A0 相加就可以了。

16 转成十六进制是 0x10
92 转成十六进制是 0x5C

  10
+ A0 
= B0

  5C
+ A0
= FC

即 ‘包’ 对应的序号是 0xB0FC,转成二进制是 1011000011111100。

3.4.3-2 16 区中的字符

可以查看 GB2312 中文简体字库表 直观一点。

3.4.3-3 字符编码集

‘、’ 对应的是 A1A0+2,即 A1A2,比较一目了然。

GBK和GB18030是后来的扩展编码,兼容 GK2312。

3.4.4 乱码的产生

不同国家有不同的编码方式,同一串二进制经不同的规则解码得到的结果很可能不一样,或者干脆显示一大堆的 ?。

如下面的例子:

使用 UTF-8 编码输入 “天下”,将文件转成 GBK 编码,会显示什么?

工具:字符集编码转换

使用 UTF-8 编码(此编码中一个汉字占 3 个字节)储存 “天下” 得到的二进制是 11100101 10100100 10101001 11100100 10111000 10001011

使用 GBK 解码,1 开头的读 2 个字节,11100101 10100100 转成十六进制是 0xE5A4,即 69 区的第 4 个数:“澶”,再读 2 个字节转 0xA9E4是 “╀”,(因为 GBK 兼容 GB2312,所有前两字可以查 GB2312 表)

最后两个字节转成的十六进制 0xB88B 不属于 GB2312 的范围,查下 GBK编码表,得 “笅”。

E5 - A0 = 0x45 = 69
A4 - A0 = 0x4 = 4

UTF-8 编码转为 GBK,“天下” -> “澶╀笅”。

所以要显示正确的结果,编码、解码需要使用同一种字符编码。

事实真的是这样吗?

创建一个 a.txt 文件,使用 UTF-8 编码,输入 “天下”。

右键此文件/属性:

请添加图片描述

两个汉字一共占用 6 个字节,说明 UTF-8 一个汉字占 3 个字节。

右键使用 Notepad++ 打开:

使用 GBK 编码读取这段二进制:

GBK 编码中,3 个汉字 6 个字节,一个汉字占 2 个字节。

3.4.5 BOM

BOM:byte order mark(标记字节顺序),因为网络传输中分为两种,大端(Big Endian)和小端(Little Endian)。[1]

在文件开头添加 BOM 标记(0xFEFF:大端、0xFFFE:小端)表明所使用的字节顺序,为 UTF-16、UTF-32 准备的。

UTF-8 不需要 BOM 表明字节顺序,但可以用 BOM 来表示编码方式,Windows 就是采用 BOM 来标记文本文件的编码方式的。

微软在 UTF-8 中使用 BOM 是因为这样可以把 UTF-8 和 ASCII 等编码区分开来,但这样的文件在 Windows 之外的操作系统里会带来问题。

图 3.4.5-1 IDEA 中应选择不带 BOM 的 UTF-8

不含 BOM 的 UTF-8 才是标准形式,即文件开头没有 0xEFBBBF = 0b1110 1111 10 111011 10 111111。

0b1111 111011 111111 = 0xFEFF。[2]

[1] 详见 2.3.5 解析的第 6 条注释。存储器以字节为单位存储,字节序是对于超过 1 个字节的数据而言的,如存储 0x00009A6C,先存储低字节 0x6C(小端: 0x6C9A0000),还是先存储高字节 0x00(大端:0x00009A6C)。

字节序是指编码单元内部字节与字节之间的顺序,而不是位之间的顺序,如 11011100 10110001 以大端存储:11011100 10110001 = 0xDCB1,小端存储:10110001 11011100 = 0xB1DC。

UTF-16、UTF-32 分别以 2、4 个字节为编码单元存储,是需要区分大小端的。如以 UTF-16LE 编码存储字符 “𐌀” ,对应编码是 0xD800DF00,编码单元之间的顺序确定,即 0xD800 与 0xDF00 这两个编码单元顺序确定,但编码单元内部,字节与字节之间的顺序不确定,即 0xD800 内究竟是 00 在前还是 D8 在前,没有规定;以小端存储应是低位在前,存储 0x00D8,所以 “𐌀” 的 UTF-16LE 编码为 0x00D800DF。

UTF-8 以 1 个字节为编码单元存储,不存在编码单元内的字节谁先谁后的问题,就 1 个也没法排序。

[2] 详见 3.4.9 UTF-8 的编码规则。

3.4.6 Unicode 字符集

Unicode 是为了解决传统的字符编码方案的局限而产生的,它为所有语言中的每个字符设定了统一并且唯一的序号(code point),以供全球人使用,序号也被称为代码点。

UCS-2 和 UCS-4

这个序号一般写成十六进制,在前面加上 U+。例如:“马” 的 Unicode 序号是 U+9A6C,这个无所谓,写成 0x 也行。(Unicode 码兼容 ASCII 码)

文字和序号之间的对应关系就是 UCS-2(Universal Character Set coded in 2 octets),UCS-2 使用 2 个字节表示序号,取值范围为 U+0000 ~ U+FFFF。

为了能表示更多的文字,人们又提出了 UCS-4,即用 4 个字节表示序号。它的范围为 U+00000000 ~ U+7FFFFFFF,其中 U+00000000 ~ U+0000FFFF 与 UCS-2 一样。

Unicode 本身只规定了每个字符与序号的对应关系,并没有规定这个序号在计算机中如何存储,你当然可以直接将序号转成二进制存储,但是对于 UCS-4 而言,序号直接转成二进制,每个字符需要 4 个字节,使用 ASCII 编码的地区国家,存储体积是原来的 4 倍,这是不太容易接受的,为此诞生了 UTF-8 可变长编码,英文字符还是只占一个字节。

规定存储方式的称为 UTF(Unicode Transformation Format),其中应用较多的就是 UTF-16 和 UTF-8 了。

3.4.7 UTF-32

直接存储序号转成的二进制,每个字符占用 4 个字节(包括英文字符)。比如马在 Unicode 中的序号为:U+9A6C,对应的二进制:00000000 00000000 10011010 01101100。

UTF-32 包括 UTF-32、UTF-32BE(Big Endian:大端),UTF-32LE(Little Endian:小端)。

表 3.4.7-1 使用十六进制编辑器打开文件
使用不同方式存储存储 “马”
UTF-32(Big Endian)00 00 FE FF 00 00 9A 6C
UTF-32(Little Endian)FF FE 00 00 6C 9A 00 00
UTF-32(不带 BOM)00 00 9A 6C

没有提供 BOM,默认以大端解码。

工具网址

图 3.4.7-1 记得去掉空格

请添加图片描述

3.4.8 UTF-16

UTF-16 使用变长字节表示:

  • 序号在 U+0000 ~ U+D7FF 的字符,使用 2 个字节表示,直接将序号转成二进制。
  • 序号在 U+10000 ~ U+10FFFF 的字符,使用 4 个字节表示,编码规则如下。

0xD800 ~ 0xDFFF:0b11011 00000 000000 ~ 0b11011 11111 111111 是空,没有对应字符,如果编码以 11011 开头,那么必是读取 4 个字节,紧跟着的 5 位,如 00011,再加 1,得到序号的二进制的前缀 100。

表 3.4.8-1 UTF-16 编码与序号的对应关系
序号序号对应二进制编码
0x0000 ~ 0xD7FF、0xE000 ~ 0xFFFFxxxxxxxx xxxxxxxxxxxxxxxx xxxxxxxx
0x10000 ~ 0x1FFFF1xxxxxx xxxxxxxxxx11011 00000xxxxxx 110111xxxxxxxxxx
0x20000 ~ 0x2FFFF10xxxxxx xxxxxxxxxx11011 00001xxxxxx 110111xxxxxxxxxx
0x30000 ~ 0x3FFFF11xxxxxx xxxxxxxxxx11011 00010xxxxxx 110111xxxxxxxxxx
0x40000 ~ 0x4FFFF100xxxxxx xxxxxxxxxx11011 00011xxxxxx 110111xxxxxxxxxx
0x50000 ~ 0x5FFFF101xxxxxx xxxxxxxxxx11011 00100xxxxxx 110111xxxxxxxxxx
0x60000 ~ 0x6FFFF110xxxxxx xxxxxxxxxx11011 00101xxxxxx 110111xxxxxxxxxx
0x70000 ~ 0x7FFFF111xxxxxx xxxxxxxxxx11011 00110xxxxxx 110111xxxxxxxxxx
0x80000 ~ 0x8FFFF1000xxxxxx xxxxxxxxxx11011 00111xxxxxx 110111xxxxxxxxxx
0x90000 ~ 0x9FFFF1001xxxxxx xxxxxxxxxx11011 01000xxxxxx 110111xxxxxxxxxx
0xA0000 ~ 0xAFFFF1010xxxxxx xxxxxxxxxx11011 01001xxxxxx 110111xxxxxxxxxx
0xB0000 ~ 0xBFFFF1011xxxxxx xxxxxxxxxx11011 01010xxxxxx 110111xxxxxxxxxx
0xC0000 ~ 0xCFFFF1100xxxxxx xxxxxxxxxx11011 01011xxxxxx 110111xxxxxxxxxx
0xD0000 ~ 0xDFFFF1101xxxxxx xxxxxxxxxx11011 01100xxxxxx 110111xxxxxxxxxx
0xE0000 ~ 0xEFFFF1110xxxxxx xxxxxxxxxx11011 01101xxxxxx 110111xxxxxxxxxx
0xF0000 ~ 0xFFFFF1111xxxxxx xxxxxxxxxx11011 01110xxxxxx 110111xxxxxxxxxx
0x100000 ~ 0x10FFFF10000xxxxxx xxxxxxxxxx11011 01111xxxxxx 110111xxxxxxxxxx
表 3.4.8-2 0x0000 ~ 0xFFFF 之内范围的字符存储
使用不同方式存储存储 “马”
UTF-16(Big Endian)FE FF 9A 6C
UTF-16(Little Endian)FF FE 6C 9A
UTF-16(不带 BOM)9A 6C

𐌀 的序号 U+10300 的二进制:1000000 1100000000,对应编码在表中第 2 项,将 000000 1100000000 填入 11011 00000xxxxxx 110111xxxxxxxxxx 得到编码:11011 00000000000 1101111100000000 = 0xD800DF00。

表 3.4.8-3 0x10000 ~ 0x1FFFF 之内范围的字符存储
使用不同方式存储存储 “𐌀”
UTF-16(Big Endian)FE FF D8 00 DF 00
UTF-16(Little Endian)FF FE 00 D8 00 DF
UTF-16(不带 BOM)D8 00 DF 00

例 1:UTF-16 编码 0101010101001010 对应的字符。

0b0101010101001010,读取 2 个字节,不需要做特殊处理,转成十六进制 0x554A,直接在 Unicode 字库表 中找到 ‘啊’。

例 2:UTF-16 编码 11011000001111001101111000110011 对应的字符。

编码的第 6 ~ 10 位需要加 1 得到序号的二进制前缀,如 0b01111 + 1 = 0b10000。

11011 开头,应读取 4 个字节,11011 00000111100 1101111000110011。

紧接着读取 5 位:00000,得到序号的二进制前缀:0b00000 + 0b1 = 0b1。

读取 6 位,得到 111100;

剩余位数:1101111000110011,去掉 110111,得到 1000110011;

将这三部分拼接得到:0b1 111100 1000110011 = 0x1F233,然后根据此序号在字库中查询到 “🈳”。

例 3:“🈴” 在 Unicode 的序号是 0x1F234,求它的 UTF-16LE 编码对应的十六进制。

序号的二进制前缀需要 - 1,得到编码的第 6 ~ 10 位,如 0b1100 - 1 = 0b01011。(不足 5 位前面补 0)

0x1F234 = 0b1 1111001000110100,这种 17 位的没法用 2 个字节存储,取出二进制的后 16 位:111100 1000110100,前缀为 1。

编码的开头为 11011;

紧接着的 5 位等于前缀 - 1 = 00000;

接着 6 位是取出的二进制的前 6 位:111100;

接着 6 位是固定的:110111;

最后 10 位是取出的二进制的最后 10 位:1000110100;

将其拼接得到:0b11011 00000 111100 110111 1000110100 = 0xD83C DE34。

使用小端表示,每两个字节交换顺序,再加上 BOM 表明字节顺序:0xFFFE 3CD8 34DE。

请添加图片描述

工具网址

图 3.4.8-1 添加前缀 FFFE 表明字节顺序

3.4.9 UTF-8

UTF-8 使用变长字节表示:

  • 序号在 U+00 ~ U+7F 的字符,编码使用 1 个字节表示,直接将序号转成二进制。
  • 序号在 U+80 ~ U+7FF 的字符,使用 2 个字节表示。
  • 序号在 U+800 ~ U+FFFF 的字符,使用 3 个字节表示。
  • 序号在 U+10000 ~ U+10FFFF 的字符,使用 4 个字节表示。
表 3.4.9-1 UTF-8 编码与序号的对应关系
序号编码
0x00 ~ 0x7F(0 ~ 127)0xxxxxxx
0x80 ~ 0x7FF(128 ~ 2047)110xxxxx 10xxxxxx
0x800 ~ 0xFFFF(2048 ~ 65535)1110xxxx 10xxxxxx 10xxxxxx
0x10000 ~ 0x10FFFF(65536 ~ 1114111)11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

BOM:0xFEFF = 0b1111 111011 111111,填充到 1110xxxx 10xxxxxx 10xxxxxx 中,得到编码:11101111 10111011 10111111 = 0xEFBBBF。

例 1:“倩” 在 Unicode 的序号为 0x5029,求它的 UTF-8 编码对应的十六进制。

0x5029 处于 0x800 ~ 0xFFFF 范围内,对应编码 1110xxxx 10xxxxxx 10xxxxxx。

0x5029 = 0b0101000000101001,补足 16 位,不够前面补 0,截成三段,长度分别为 4、6、6 位:0101 000000 101001,然后填充到对应编码,11100101 10000000 10101001 = 0xE580A9。

带 BOM 的 UTF-8:0xEF BB BF E5 80 A9。

例 2:UTF-8 编码 111001101001011110100000,求它对应的字符。

观察前 4 位:1110,应读取 3 个字节,11100110 10010111 10100000,去掉第 1 个字节的 1110、第 2 个字节的 10、第三个字节的 10,得到 0110 010111 100000 = 0x65E0,在 Unicode 字库的 0 号平面查找到 0x65E0 对应的字符 “无”。

由于 UTF-8 的处理单元为一个字节(也就是一次处理一个字节),所以处理器在处理的时候就不需要考虑这一个字节的存储是在高位还是在低位,直接拿到这个字节进行处理就行了,因为大小端是针对大于一个字节的数的存储问题而言的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值