编程中用到的字符编码知识点

一口Linux 2022-05-17 00:00

击上方“一口Linux”,选择“置顶/星标公众号


干货福利,第一时间送达!


字符集和字符编码

字符集就是字符的集合,如常见的 ASCII字符集,GB2312字符集,Unicode字符集等。这些不同字符集之间最大的区别是所包含的字符数量的不同。

字符编码则代表字符集的实际编码规则,是用于计算机解析字符的,如 GB2312,GBK,UTF-8 等。字符编码的本质就是如何使用二进制字节来表示字符的问题。

字符集和编码是一对多的关系,同一字符集可能有多种字符编码,如Unicode字符集就有 UTF-8,UTF-16 等。

在前端开发中,Javascript程序是使用Unicode字符集,Javascript源码文本通常是基于UTF-8编码。
但js代码中的字符串类型是UTF-16编码的,这也是为什么会碰到api接口返回字符串在前端出现乱码,因为多数服务都使用utf-8编码,前后编码方式不一致。

说起字符集的发展历程,可以总结为一句话:几乎都是对ASCII字符集的扩展。

ASCII

我们知道,计算机是使用二进制来处理信息的。
其中,每一个二进制位(bit)有 0和1 两种状态。一个字节(byte)则有8个二进制位,可以有256种状态。

而ASCII就是基于拉丁字母、主要用于显示英文的一种单字节字符集,它的编码和字符是一一对应的,因为它就是使用一个字节8个二进制位来表示,不会超过256个字符。

标准的ASCII字符总计有128个字符(2^7),其中前面32个控制字符,后面96个是可打印字符,包括常用的大小写字母数字标点符号等。因为只占用了一个字节的后7位,那字节的最高位一般设置为0。

'a'.charCodeAt() // 97
'A'.charCodeAt() // 65
'9'.charCodeAt() // 57
'.'.charCodeAt() // 46

如上,每个字符会对应一个编码(使用数字标识),总共会从0-128。完整的ASCII码表,网上很容易找到。

通过ASCII码表,我们发现,小写字母并没有和大写字母挨着排序?这是为了方便大小写之间的转换, A 排在 65(64 + 1) 位,而 a 排在 97(64 + 32 + 1) 位。

65 ^ 32 = 97
// A ^ 32 = a

字符集的发展历史

ASCII是几乎所有字符集的基础。

标准的ASCII码最多只能标识128个字符,欧美国家可以很好的使用,但其他国家的字符变多,自然就不够用了。
这个时候,最高位就开始被惦记上,通过扩展ASCII码的最高位,又能满足用于特殊符号的一些国家的需求,这种就是扩展ASCII码。

但是亚非拉更多非拉丁语系的国家,字符成千上万,只能使用新的方式。
如中文,就又进行了扩展,小于127的字符的意义与标准ASCII码相同,当需要标识汉字时,使用2个字节,每个字节都大于127。这种多字节字符集即GB2312,后续因为不断的扩展,如繁体字和各种符号,甚至少数民族的语言符号等等,又使用了包括GBK等不同字符集。
因此,很多国家都制定了自己的编码字符集,基本都是在ASCII的基础上进行的。

各字符集虽然都能够兼容标准ASCII码,但在使用交流上的不便是显而易见的,乱码也是随处可见。为了解决这种各自为战的问题,Unicode字符集就诞生了。

Unicode

Unicode是国际组织制定的,用于收纳世界上所有文字和符号的字符集方案。
前128个字符同ASCII一样,进行扩充后,使用数字0-0x10FFFF来映射这些字符,最多可以有1114112个字符。目前仍然只使用了其中的一小部分。
Unicode一般使用两个字节来表示一个字符。

  • 码点
    Unicode 规定了每个字符的数字编号,这个编号被称为 码点(code point)。码点以 U+hex 的形式表示,U+是代表Unicode的前缀,而 hex 是一个16进制数。取值范围是从 U+0000 到 U+10FFFF。

每个码点对应一个字符,绝大部分的常见字符在最前面的 65536 个字符,范围是 U+0000到U+FFFF。
一般汉字的码点区间为 U+2E80 - U+9FFF。

  • 字符平面
    目前的Unicode分成了17个编组,也称平面,每个平面有65536个码点。第一个平面是基本多语言平面,范围:U+0000 - U+FFFF,多数常见字符都在该区间。其他平面则为辅助平面,范围:U+10000 到 U+10FFFF,如我们在网上常见 Emoji 表情。

  • 码元
    码元(Code Unit)可以理解为对码点进行编码时的最小基本单元,码元是一个整体。而字符编码的作用就是将Unicode码点转换成码元序列。Unicode常用的编码方式有 UTF-8 、UTF-16 和 UTF-32,UTF是Unicode TransferFormat的缩写。UTF-8是8位的单字节码元,UTF-16是16位的双字节码元,UTF-32是32位的四字节码元。

编码方式

码元

编码后字节数

UTF-8

8位

1-4字节

UTF-16

16位

2字节或者4字节

UTF-32

32位

4字节

另外,为什么总看到使用十六进制数据来表示如码点等各种数据呢?
因为,两位的十六进制正好等于一个字节8位,0xff = 0b11111111。

UTF-8

UTF-8是一种可变长度的字符编码方式。目前是使用 1 到 4 个字节来编码字符。
是互联网时代应用最广的一种编码方式,前端接触的相对最多。

需要注意的是:汉字一般占3个字节,表情符号一般占4个字节。

UTF-8的编码规则:

  • 1个字节的字符,第一位为0,后7位为码点,与ASCII相同。

  • n个字节的字符,第一个字节前面 n 位都是1,n+1位是0,可据此判断有几个字节。后面的几个字节都是 10 为开头2位。
    这里规定的都是前缀,对于字符的码点,需要进行截取后依次放入除前缀外的其他位,所以UTF-8又被称为前缀码。格式如表:

字节数

码点位数

码点范围

编码方式

1

7

U+0000~U+007F

0×××××××

2

11

U+0080~U+07FF

110××××× 10××××××

3

16

U+0800~U+FFFF

1110×××× 10×××××× 10××××××

4

21

U+10000~U+10FFFF

11110××× 10×××××× 10×××××× 10××××××

通过上表的编码规则,我们就可以进行各种转换了。
下面我们以一个中文字符的编码转换为例,如汉字 '好':

'好'的Unicode码点:'好'.codePointAt() \\ 22909,结果是22909;
22909在UTF-8的3字节数的编码区间 U+0800 (2048) ~ U+FFFF (65535);
22909的二进制值:101100101111101,有15位;
而3字节数的编码需要16位,前面补0,根据表中规则分成3组:0101 100101 111101;
依次填入对应的前缀:11100101 10100101 10111101,得到3个字节;
将得到的三个字节转成十六进制数据:E5 A5 BD,所以汉字 '好' 的UTF-8就是:E5 A5 BD。

我们使用 encodeURI 进行验证————encodeURI函数支持将中文进行 UTF-8 编码:

encodeURI('好') // '%E5%A5%BD'

去除百分号,结果正好一致。

UTF-16

UTF-16的编码方式:基本平面的字符占用 2 个字节(U+0000到U+FFFF),辅助平面的字符占用 4 个字节(U+010000到U+10FFFF)。
也就是说,UTF-16的编码长度要么是2个字节要么是4个字节。当为2字节时,则实际上与Unicode相同。

并且还有个原则,在Unicode基本多语言平面内,从U+D800到U+DFFF之间的码点区间是不对应字符的。而UTF-16需要利用这块码位来对辅助平面的字符进行编码。
它的具体规则:

  • 码点小于U+FFFF,基本字符,不需处理,直接使用,占两个字节。

  • 否则,拆分成两个码元,四个字节,cp表示码点:低位——((cp - 65536) / 1024) + 0xD800,值范围是 0xD800~0xDBFF;高位——((cp - 65536) % 1024) + 0xDC00,值范围是 0xDC00~0xDFFF。

看下面的示例:

汉字 '好','好'.codePointAt() // 22909,码点小于U+FFFF,直接进行十六进制转换:579D。表情符号 '',''.codePointAt() // 128516,码点需要拆分:低位:Math.floor(((128516 - 65536) / 1024)) + 0xD800 // 55357, 得到 D83D高位:((128516 - 65536) % 1024) + 0xDC00 // 56836,得到 DE04

使用 String.fromCharCode 方法进行验证:

String.fromCharCode(0xD83D, 0xDE04)  // ''

需要明确的一点,Javascript中的字符串是基于UTF-16编码的,大端序字节。

UTF-32是定长的编码,每个码位使用四个字节进行编码。优点是和unicode一一对应,缺点是太浪费空间。

比较

下面将选取字母、汉字、表情字符,进行编码对比查看:

// UTF-8
'a': 97 - 0x61
'好': 22909 - (0xE5 0xA5 0xBD)
'': 128516 - (0xF0 0x9F 0x98 0x84)

// UTF-16
'a': 97 - 0x0061
'好': 22909 - 0x597d
'': 128516 - (0xD83D, 0xDE04)

可以看到,UTF-8是变长1-4个字节,码元为8位;UTF-16是2或4字节,码元是16位。
这里记住UTF-16的码元,对于我们理解下面的问题,比较有帮助。

前端开发中的编码

前面已提到过,javascript中的字符串是基于UTF-16编码的,所以在计算字符串长度时,我们需要先理解UTF-16编码。
下面看下处理字符串时可能会遇到的问题。

字符串长度计算

字符串的length属性,实际上是使用UTF-16的码元个数来进行计算的:

  • ASCII码和大部分中文,都是一个码元

  • 而表情字符和其他特殊字符都是两个码元

所以当某个字符中存在2个码元时,就算显示的是一个字符,length却等于2。

'a'.length // 1
'好'.length // 1,多数汉字都是基本字符平面,只有一个码元,长度就为1。
''.length // 2

组合字符的长度

还有一种特殊的,组合字符,一般指一些带标点符号的字符:é。

'é'.length // 2
'e\u0301'.length // 2

// 获取码点时,忽略了标点符号,显示的是字母的码点
'é'.codePointAt() // 101
'e'.codePointAt() // 101

如要正常操作组合字符,使用normalize()。

'é'.normalize().length = 1。

多码元字符操作

对于多码元字符使用下标取值时,得到的将是它的码元:

''[0] // '\uD83D'
''[1] // '\uDE04'
'123'[0] // '1'

循环时,使用 for 会乱码,而 for-of 则正常:

let smile = ''
for(let i = 0; i < smile.length; i++) {
console.log(smile[i])
}
// �
// �

for (let tt of smile) {
console.log(tt)
}
//

但,可以使用转换成扩展数组的方式访问:

[...''][0] // ''
Array.from('') // ['']

还可以使用码点的方式:

String.fromCodePoint(''.codePointAt()) // ''

对于这种特殊字符,使用下面的字符串方法都会分割码元:
split(),slice(),charAt(),charCodeAt(),substr(),substring()。

''.slice(0, 2) // ''
''.slice(0, 1) // '\uD83D'
''.slice(1, 2) // '\uDE04'
''.substr(0,1) // '\uD83D'
''.substr(0,2) // ''

''.split('') // ['\uD83D', '\uDE04']

正则中的 u 修饰符

ES6在正则中添加了u修饰符,用来正确处理大于\uFFFF的 Unicode 字符。
也就是能够正确处理四个字节的 UTF-16 编码。

/^\S$/.test('') // false
/^\S$/u.test('') // true

但对组合字符,u修饰符不起作用:

/^\S$/u.test('é') // false
/^\S$/u.test('e\u0301') // false

转义字符

我们还需要注意的,是转义字符的计算,结果会以实际字符为准:

'\x3f'.length // 1
'?'.length // 1

读取操作时,也能正常处理:

'\x3f'[0] // '?'
'\x3f'.split('') // ['?']

常用API

前端在对Unicode编码处理时,提供了一些可以使用的API,在实际工作中,会方便我们处理这方面的问题。

处理码点和字符

  • charAt(index)
    从一个字符串中返回指定的字符,对于多码元字符,仍会返回码元字符:

'a'.charAt() // 'a'
''.charAt() // '\uD83D'
''.charAt(1) // '\uDE04'
  • charCodeAt(index)
    返回0到65535之间的整数码点值。对于多码元如果字符的码点大于U+FFFF,则返回第一个码元值,还可以加索引参数取后面码元的值。

  • codePointAt(pos)
    返回Unicode码点,多码元也能返回完整的码点值。codePointAt可以传入索引参数,对多码元字符取第二个码元值。

// 小于 U+FFFF
'好'.codePointAt() // 22909
'好'.charCodeAt() // 22909

// 大于 U+FFFF
''.charCodeAt() // 55357
''.charCodeAt(1) // 56836
''.codePointAt() // 128516
''.codePointAt(1) // 56836
  • String.fromCharCode(num1[, ...[, numN]])
    返回由指定的UTF-16码点序列创建的字符串。参数范围0到65535,大于65535的数据将被截断,结果不准确。对于多码元字符,则会将两个码元组合得到该字符。

  • String.fromCodePoint(num1[, ...[, numN]])
    返回使用指定的代码点序列创建的字符串。可以处理多码元字符的完整码点值。

String.fromCharCode(55357, 56836, 123) // '{'
String.fromCodePoint(128516, 123, 8776) // '{≈'

TextEncoder

TextEncoder,使用 UTF-8 编码将代码点流转换成字节流。
TextDecoder:解码。
默认编码方式就是UTF-8,可以解决字符转UTF-8编码的问题。

const txtEn = new TextEncoder()
const enVal = txtEn.encode('好')
// Uint8Array(3) [229, 165, 189]
const txtDe = new TextDecoder()
txtDe.decode(enVal) // '好'

IE不支持。

String.prototype.normalize()

对于语调符号和重音符号,Unicode提供了两种方法,一种是直接提供带符号的字符,如 é (码点233);另一种是组合字符,如上文提到的  (码点101)。
针对这种码点不同,但实质一样的字符,Javascript识别不了:

'é' === 'é' // false

而 normalize() 方法的引入,正是为了解决这一问题,它会按照一定的方式将字符的不同表示方法统一为标准形式:

'é' === 'é'.normalize() // true

URL的UTF8编解码

另外,在前端常接触的网页中,URL链接编码也是非常常见的。诸如:'http%3A%2F%2Fbaidu.com%2F%E4%B8%AD%E5%9B%BD'。这里面涉及到的就是关于UTF-8的编码。
而JavaScript提供了四个URL的编码/解码方法,可以用于将非ASCII码的字符,如中文字符、特殊字符、表情字符等,进行UTF-8的编解码操作:

  • encodeURI() 和 encodeURIComponent()

  • decodeURI() 和 decodeURIComponent()

他们的短处也很明显,对ASCII字符如英文数字等字符无法处理。
这里的转换方式:先转为UTF-8的字节码,然后前面加个 
% 进行拼接得到编码结果。

encodeURI('好') // '%E5%A5%BD'
decodeURI('%E5%A5%BD') // '好'
encodeURIComponent('好') // '%E5%A5%BD'
decodeURIComponent('%E5%A5%BD') // '好'
encodeURI('hello') // 'hello'
encodeURIComponent('hello') // 'hello'
encodeURIComponent('') // '%F0%9F%98%84'

encodeURI和encodeURIComponent的区别

这两者的不同之处,在于对部分URL元字符符号的处理上。

URL元字符:分号(;),逗号(’,’),斜杠(/),问号(?),冒号(:),at(@),&,等号(=),加号(+),美元符号($),井号(#)。

encodeURIComponent会对这些URL元字符进行编码,但是encodeURI则不会:

encodeURIComponent(';,/@&=') // '%3B%2C%2F%40%26%3D'
encodeURI(';,/@&=') // ';,/@&='

end



一口Linux 


关注,回复【1024】海量Linux资料赠送

精彩文章合集

文章推荐

【专辑】ARM
【专辑】粉丝问答
【专辑】所有原创
专辑linux入门
专辑计算机网络
专辑Linux驱动
【干货】嵌入式驱动工程师学习路线
【干货】Linux嵌入式所有知识点-思维导图


点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看

一口Linux 写点代码,写点人生!
评论
  • 车身域是指负责管理和控制汽车车身相关功能的一个功能域,在汽车域控系统中起着至关重要的作用。它涵盖了车门、车窗、车灯、雨刮器等各种与车身相关的功能模块。与汽车电子电气架构升级相一致,车身域发展亦可以划分为三个阶段,功能集成愈加丰富:第一阶段为分布式架构:对应BCM车身控制模块,包含灯光、雨刮、门窗等传统车身控制功能。第二阶段为域集中架构:对应BDC/CEM域控制器,在BCM基础上集成网关、PEPS等。第三阶段为SOA理念下的中央集中架构:VIU/ZCU区域控制器,在BDC/CEM基础上集成VCU、
    北汇信息 2025-01-03 16:01 91浏览
  • 在快速发展的能源领域,发电厂是发电的支柱,效率和安全性至关重要。在这种背景下,国产数字隔离器已成为现代化和优化发电厂运营的重要组成部分。本文探讨了这些设备在提高性能方面的重要性,同时展示了中国在生产可靠且具有成本效益的数字隔离器方面的进步。什么是数字隔离器?数字隔离器充当屏障,在电气上将系统的不同部分隔离开来,同时允许无缝数据传输。在发电厂中,它们保护敏感的控制电路免受高压尖峰的影响,确保准确的信号处理,并在恶劣条件下保持系统完整性。中国国产数字隔离器经历了重大创新,在许多方面达到甚至超过了全球
    克里雅半导体科技 2025-01-03 16:10 58浏览
  • 本文继续介绍Linux系统查看硬件配置及常用调试命令,方便开发者快速了解开发板硬件信息及进行相关调试。触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。查看系统版本信息查看操作系统版本信息root@ido:/# cat /etc/*releaseDISTRIB_ID=UbuntuDISTRIB_RELEASE=20.04DISTRIB_CODENAME=focalDIS
    Industio_触觉智能 2025-01-03 11:37 67浏览
  • 影像质量应用于多个不同领域,无论是在娱乐、医疗或工业应用中,高质量的影像都是决策的关键基础。清晰的影像不仅能提升观看体验,还能保证关键细节的准确传达,例如:在医学影像中,它对诊断结果有着直接的影响!不仅如此,影像质量还影响了:▶ 压缩技术▶ 存储需求▶ 传输效率随着技术进步,影像质量的标准不断提高,对于研究与开发领域,理解并提升影像质量已成为不可忽视的重要课题。在图像处理的过程中,硬件与软件除了各自扮演着不可或缺的基础角色,有效地协作能够确保图像处理过程既高效又具有优异的质量。软硬件各扮演了什么
    百佳泰测试实验室 2025-01-03 10:39 63浏览
  • 自动化已成为现代制造业的基石,而驱动隔离器作为关键组件,在提升效率、精度和可靠性方面起到了不可或缺的作用。随着工业技术不断革新,驱动隔离器正助力自动化生产设备适应新兴趋势,并推动行业未来的发展。本文将探讨自动化的核心趋势及驱动隔离器在其中的重要角色。自动化领域的新兴趋势智能工厂的崛起智能工厂已成为自动化生产的新标杆。通过结合物联网(IoT)、人工智能(AI)和机器学习(ML),智能工厂实现了实时监控和动态决策。驱动隔离器在其中至关重要,它确保了传感器、执行器和控制单元之间的信号完整性,同时提供高
    腾恩科技-彭工 2025-01-03 16:28 97浏览
  • 光耦合器,也称为光隔离器,是一种利用光在两个隔离电路之间传输电信号的组件。在医疗领域,确保患者安全和设备可靠性至关重要。在众多有助于医疗设备安全性和效率的组件中,光耦合器起着至关重要的作用。这些紧凑型设备经常被忽视,但对于隔离高压和防止敏感医疗设备中的电气危害却是必不可少的。本文深入探讨了光耦合器的功能、其在医疗应用中的重要性以及其实际使用示例。什么是光耦合器?它通常由以下部分组成:LED(发光二极管):将电信号转换为光。光电探测器(例如光电晶体管):检测光并将其转换回电信号。这种布置确保输入和
    腾恩科技-彭工 2025-01-03 16:27 88浏览
  • 【工程师故事】+半年的经历依然忧伤,带着焦虑和绝望  对于一个企业来说,赚钱才是第一位的,对于一个人来说,赚钱也是第一位的。因为企业要活下去,因为个人也要活下去。企业打不了倒闭。个人还是要吃饭的。企业倒闭了,打不了从头再来。个人失业了,面对的不仅是房贷车贷和教育,还有找工作的焦虑。企业说,一个公司倒闭了,说明不了什么,这是正常的一个现象。个人说,一个中年男人失业了,面对的压力太大了,焦虑会摧毁你的一切。企业说,是个公司倒闭了,也不是什么大的问题,只不过是这些公司经营有问题吧。
    curton 2025-01-02 23:08 183浏览
  • 物联网(IoT)的快速发展彻底改变了从智能家居到工业自动化等各个行业。由于物联网系统需要高效、可靠且紧凑的组件来处理众多传感器、执行器和通信设备,国产固态继电器(SSR)已成为满足中国这些需求的关键解决方案。本文探讨了国产SSR如何满足物联网应用的需求,重点介绍了它们的优势、技术能力以及在现实场景中的应用。了解物联网中的固态继电器固态继电器是一种电子开关设备,它使用半导体而不是机械触点来控制负载。与传统的机械继电器不同,固态继电器具有以下优势:快速切换:确保精确快速的响应,这对于实时物联网系统至
    克里雅半导体科技 2025-01-03 16:11 93浏览
  • Matter加持:新世代串流装置如何改变智能家居体验?随着现在智能家庭快速成长,串流装置(Streaming Device,以下简称Streaming Device)除了提供更卓越的影音体验,越来越多厂商开始推出支持Matter标准的串流产品,使其能作为智能家庭中枢,连结多种智能家电。消费者可以透过Matter的功能执行多样化功能,例如:开关灯、控制窗帘、对讲机开门,以及操作所有支持Matter的智能家电。此外,再搭配语音遥控器与语音助理,打造出一个更加智能、便捷的居家生活。支持Matter协议
    百佳泰测试实验室 2025-01-03 10:29 69浏览
  • 在测试XTS时会遇到修改产品属性、SElinux权限、等一些内容,修改源码再编译很费时。今天为大家介绍一个便捷的方法,让OpenHarmony通过挂载镜像来修改镜像内容!触觉智能Purple Pi OH鸿蒙开发板演示。搭载了瑞芯微RK3566四核处理器,树莓派卡片电脑设计,支持开源鸿蒙OpenHarmony3.2-5.0系统,适合鸿蒙开发入门学习。挂载镜像首先,将要修改内容的镜像传入虚拟机当中,并创建一个要挂载镜像的文件夹,如下图:之后通过挂载命令将system.img镜像挂载到sys
    Industio_触觉智能 2025-01-03 11:39 62浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦