十几年前,国内 UTF-8 还不太流行,新手开发过程中,乱码问题是很常见的。记得我刚毕业那会,公司的系统是这样的:数据库 MySQL 使用的是 Latin-1 编码(IOS8859-1),程序源码使用的是 GBK,这样的情况,乱码真是满天飞~好在对数据库操作做了封装,编码问题在中间层处理了。那时候听到关于乱码问题的一个终极解决方案是:保证各个地方编码一致。
现如今,Unicode 相当普及,中文使用 UTF-8 编码虽然比 GBK 编码占用更多的空间,但一般都不会在乎。特别是使用了 Go 语言后,乱码问题更是没有了。在 Go 语言标准库中有 unicode 包,它下面还有 utf8 和 utf16 两个子包。也许有此疑问:又是 Unicode、又是 UTF-8、UTF-16,它们是什么关系?
本文试着为大家解决关于字符集和字符编码的困惑。
字符集,从字面看,是多个字符的集合。字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。因此不同的字符组合在一起就可以认为是不同的字符集。当然不是瞎组合。
大家都知晓,计算机只认 0 和 1 组成的二进制数。所以整数通过进制转换转为二进制就可以被计算机处理。然而像文本、音频、视频等这样的信息,如何转为二进制被计算机处理呢?这就需要进行编码。
编码(encode)是把数据从一种形式转换为另外一种形式的过程,它是一套算法。解码(decode)就是编码的逆过程。
比如对于 ASCII 字符集中的字符 1,转换为二进制是 00110001,这就是一次编码;将 00110001 显示为 1,涉及到一次解码。
字符编码,顾名思义,是对字符进行编码,是字符和二进制数据之间转换的算法,它们之间必须一一对应,这是本文需要讨论的主题。一个二进制叫做位,8 位称为“字节”,根据计算一个字节一共可组合出 256(2 的 8 次方)种不同的状态。
关于字符集和字符编码,使用 ASCII 进行总结说明:
ASCII 字符集 是字母、数字、标点符号以及控制符(回车、换行、退格)等组成的 128 个字符。
ASCII 字符编码 是将这 128 个字符转换为计算机可识别的二进制数据的一套规则(算法)。
因此当我们说 ASCII 时,一般同时指 ASCII 字符集和 ASCII 字符编码。通常,字符集会同时定义一套同名的字符编码规则。然而万事都有特例,比如 Unicode 就只是代表字符集,对应的字符编码有多种,比如 UTF-8、UTF-16 等。
现在知道了 Unicode 只是代表字符集,它的编码规则是通过 UTF 系列定义的。
那 Unicode 是什么? 官方 有这么一句话:
Unicode provides a unique number for every character, no matter what the platform, program, or language is.
即 Unicode 为每一个字符提供了一个唯一的数字编码(代号),叫做 Code Point(码点)。这里可以查找你要查询某个字符的码点(Code Point): https://www.unicode.org/cgi-bin/GetUnihanData.pl 。注意这里的码点不是字符编码,只是字符集而已。
所以,Unicode 字符集一个最主要的工作就是维护这样一个表,可以把它想象成是一个数据库表,里面存储着每一个字符对应的唯一 ID(即 Code Point),统一用 U+XXXX 来表示(X 为 16 进制的字符,因为字符很多,并不一定所有的字符都是 4 个 16 进制数,比如笑哭的 Emoji 表情,Code Point 是 U+1F602),如 「徐」U+5F90、「新」U+65B0。现在这个数据表已经拥有 100 多万的字符,还在不停的更新。
为什么会出现 Unicode 呢?因为之前的编码方式,大多只考虑自己国家,不同国家的编码方式不同,导致使用中国 GBK 编码的文档,在日本用本地编码打开就乱码了。标准总是在混乱中诞生的,于是一些国际组织制定出了全球统一的编码格式。这就是 Unicode。
大家可能见到过 USC,这是 ISO 制定的一种计算机行业标准,和 Unicode 目的是一样的。他们双方意识到不应该出现两种不同的国际标准。因此你可以认为它们是一样的。
了解了 Unicode,那为什么会有 UTF-8、UTF-16?
前面说了 Unicode 本身主要工作是维护一个表,它并没有规定一个字符到底用几个字节来表示,只规定了每个字符对应到唯一的码点(code point),码点可以从 0000 ~ 10FFFF 共 1114112 个值 。
那为什么 Unicode 不规定字符编码呢?如果将 Unicode 码点直接当编码规则会如何?其实是有的,这就是 UTF-32。因为计算机没法确认两字节到底是表示 1 个字符还是 2 个字符,因此 UTF-32 粗暴的取最大值,所有字符都按 4 字节编码。很显然,这在空间上是极浪费的(英文文档直接大 3 倍),因此 UTF-32 很少使用。
UTF 是 Unicode Transformation Format (Unicode 转换格式)的首字母缩写,专门解决 Unicode 的编码问题。根据编码规则的不同有 UTF-8、UTF-16 和 UTF-32 等几种具体的方案。
由于 UTF-32 浪费空间,使用不多,本文着重介绍 UTF-8 和 UTF-16。
UTF-8,也可写为 UTF8,是 Unicode 的一种变长编码方案。它完全兼容 ASCII,同时避免了 UTF-16 和 UTF-32 中的字节序等复杂性。UTF-8 能够被广泛接受,跟其完全兼容 ASCII 有很大关系。
为什么叫 -8 ?因为它将每个 Unicode 字符编码为 1~4 个八位元(octets)(八位元即一个字节),因此叫 -8 。其中字节的个数取决于分配给 Unicode 字符的整数值。
当面对 4 个字节,UTF-8 如何知晓应该把它当做 1 个字符解析、还是 2 个?亦或是 4 个?这就是 UTF-8 设计巧妙之处。
UTF-8 从首字节就可以判断一个字符的 UTF-8 编码有几个字节, 具体判断逻辑就是,根据首字节二进制的起始内容:
除首字节之外,其他字节也有规定,如下:
字节数 | 用来表示码点的位数 | Unicode 十六进制码点范围 | 字节1 | 字节2 | 字节3 | 字节4 |
---|---|---|---|---|---|---|
1 | 7 | 0000 0000 - 0000 007F | 0xxxxxxx | |||
2 | 11 | 0000 0080 - 0000 07FF | 110xxxxx | 10xxxxxx | ||
3 | 16 | 0000 0800 - 0000 FFFF | 1110xxxx | 10xxxxxx | 10xxxxxx | |
4 | 21 | 0001 0000 - 0010 FFFF | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
具体如何编码和解码呢?以中文“徐”为例,说明这个过程。
「徐」的 Unicode 码点是 0x5F90(二进制:101 1111 1001 0000),对照上面的表发现 0x5F90 位于第三行的范围,所以格式是 1110xxxx 10xxxxxx 10xxxxxx。接着从「徐」Unicode 码点的二进制数最后一位开始,按从右向左依次填充这个格式中的 x,多出的 x 用 0 补上。这样就得到了「徐」这个汉字的 UTF-8 编码:11101011 1011110 10010000,转成十六进制是 0xE5 0xBE 0x90。
解码的过程也十分简单:如果一个字节(编码的二进制表示)的第一位是 0 ,则说明这个字节对应一个字符;如果一个字节的第一位是 1,那么连续有多少个 1,就表示该字符占用多少个字节。还是以「徐」字为例,它的 UTF-8 编码的二进制表示是:11101011 1011110 10010000,根据规则,第一个字节的第一位是 1,且有三个 1,因此占用 3 个字节。根据上面表中第三行,将第一个字节开头的 1110 去掉,第二、第三个字节开头的 10 去掉,剩下的组合在一起,即:1011 11110 010000,这就是 0x5F90。
关于 UTF-8 的规范参考 rfc3629 ,这是 Ken Thompson 和 Rob Pike 等制定的,这两位也是 Go 语言的作者。
UTF-16 是 16-bit Unicode Transformation Format。有一点需要特别强调,它跟 UTF-8 一样,是可变长度的。代码点(Code Point)是用一个或两个 16 位代码单元编码的。至于网上有些资料说的 UTF-16 是固定 2 个字节长度编码,那其实不是 UTF-16,而是 UCS-2 (用于 2 字节通用字符集)。UTF-16 可以看做是它的父集。
这里涉及一个概念,简单介绍下。
基本多语言平面(BMP):UCS-2 只对这些字符进行了编码,一共能编码 65,536 个字符。后来不够用了,怎么办?IEEE 引入了 UCS-4,所有字符都用 4 字节编码,这太浪费空间了,因此出现了一个折中方案:UTF-16,即超出 U+FFFF 的部分使用 4 字节,这部分叫做辅助平面(SMP),码点范围 U+010000 到 U+10FFFF。
注意 UCS-2 已经过时了。
因为是变长的方案,就需要有办法标识到底是 2 个字节还是 4 个字节。上文讲解 UTF-8 时,判断首字节字节开头位即可。类似的,看如下一张表:
字节数 | 16 进制码点范围 | 16-bit code 1 | 16-bit code 2 |
---|---|---|---|
2 | U+0000 - U+D7FF | xxxxxxxxxxxxxxxx | |
2 | U+E000 - U+FFFF | xxxxxxxxxxxxxxxx | |
4 | U+00010000 - U+0010FFFF | 110110xxxxxxxxxx | 110111xxxxxxxxxx |
使用 UTF-16 最知名的是 Java,JVM 内部使用的就是 UTF-16 编码。因为 UTF-16 不兼容 ASCII,因此网络传输一般不会使用它。
但 Java 的编码方式其实没那么简单。一般地 Java 中是这样的:(来源于 《Java 核心技术手册》 一书)
接下来,以汉字"?“为例,说明 UTF-16 编码方式是如何工作的。
汉字”?“的 Unicode 码点为 0x20BB7 ,该码点显然超出了基本平面的范围(0x0000 - 0xFFFF),因此需要使用四个字节表示。首先用 0x20BB7 - 0x10000 计算出超出的部分,然后将其用 20 个二进制位表示(不足前面补 0 ),结果为 0001000010 1110110111 。接着,将前 10 位映射到 U+D800 到 U+DBFF 之间,后 10 位映射到 U+DC00 到 U+DFFF 即可。 U+D800 对应的二进制数为 1101100000000000 ,直接填充后面的 10 个二进制位即可,得到 1101100001000010 ,转成 16 进制数则为 0xD842 。同理可得,低位为 0xDFB7 。因此得出汉字”?“的 UTF-16 编码为 0xD842 0xDFB7 。
Go 语言让复杂的编码问题变得简单很多,极大的减轻了程序员的心智负担。为了方便对 unicode 字符串进行处理,Go 语言标准库提供三个包:unicode、unicode/utf8 和 unicode/utf16。
这里简单介绍下三个包的功能:
具体函数不讲解了,大家可以看标准库文档或通过我的开源书阅读相关章节: https://github.com/polaris1119/The-Golang-Standard-Library-by-Example 。
着重介绍下 Go 中字符串的写法。
在 Go 语言中,字符串字面值有 4 种写法,比如「徐新华」可以这么写:
s1 := "徐新华"
s2 := "\u5F90\u65B0\u534E"
s3 := "\U00005F90\U000065B0\U0000534E"
s4 := "\xe5\xbe\x90\xe6\x96\xb0\xe5\x8d\x8e"
简单来生活就是 \u 紧跟四个十六进制数,\U 紧跟八个十六进制数。其中 \u 或 \U 代表后面是 Unicode 码点。而 \x 紧跟两个十六进制数,这些十六进制不是 Unicode 码点,而是 UTF-8 编码。
下面的代码有利于你的理解:
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := `徐新华`
var (
buf = make([]byte, 4)
n int
)
fmt.Println("字符\tUnicode码点\tUTF-8编码十六进制\tUTF-8编码二进制")
for _, r := range s {
n = utf8.EncodeRune(buf, r)
fmt.Printf("%q\t%U\t\t%X\t\t%b\n", r, r, buf[:n], buf[:n])
}
s2 := "\u5F90\u65B0\u534E"
s3 := "\U00005F90\U000065B0\U0000534E"
s4 := "\xe5\xbe\x90\xe6\x96\xb0\xe5\x8d\x8e"
fmt.Println(s2)
fmt.Println(s3)
fmt.Println(s4)
}
运行结果:
字符 Unicode码点 UTF-8编码十六进制 UTF-8编码二进制
'徐' U+5F90 E5BE90 [11100101 10111110 10010000]
'新' U+65B0 E696B0 [11100110 10010110 10110000]
'华' U+534E E58D8E [11100101 10001101 10001110]
徐新华
徐新华
徐新华
此外,关于字符串其他方面的处理,比如编码转换等,可以到 https://pkg.go.dev/golang.org/x/text 里找。
最后聊一下大小端的问题。
一个字符使用多字节存储时,涉及到哪个在前哪个在后。以汉字「徐」为例,Unicode 码点是 5F90,需要用两个字节存储,一个字节是 5F ,另一个字节是 90 。存储的时候, 5F 在前, 90 在后,这就是 Big endian 方式; 90 在前, 5F 在后,这是 Little endian 方式。
这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-endian)敲开还是从小头(Little-endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。
第一个字节在前,就是”大端方式”(Big endian),第二个字节在前就是”小端方式"(Little endian)。
那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?
Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做"零宽度非换行空格"(zero width no-break space),用 FEFF 表示。这正好是两个字节,而且 FF 比 FE 大1。
如果一个文本文件的头两个字节是 FE FF,就表示该文件采用大端方式;如果头两个字节是 FF FE,就表示该文件采用小端方式。
但从上面关于 UTF-8 编码的说明可以看出,虽然 UTF-8 存在多字节表示一个字符的情况,但顺序是固定的,没有字节序的问题。Unix 系统下,UTF-8 没有任何前置字符,但 Windows 下记事本保存的 UTF-8 文件会带上 BOM(Byte Order Mark),即 EF BB BF 这三个字节。关于这一点,Unicode 之父 Rob Pike 明确说 UTF-8 不需要 BOM,所以一开始 Go 源文件是不允许有 BOM 的,否则编译不通过,不过现在已经可以有了。但建议还是别带 BOM。
UTF-8 带 BOM 说不是为了区分字节序,而是为了更方便的知晓这是一个 UTF-8 文件。
你可以通过 hexdump 工具查看文件开始的字符。
原文 https://polarisxu.studygolang.com/posts/basic/char-set-encoding/
最近在做加解密这块的开发,使用频率最高的就是 Nodejs 的 crypto 模块了,中间出现了很多种不同的编码方式。一直对这些编码方式处于熟练拼写的状态,但是到底有什么区别处于一知半解,借此机会正好整理下这之间的关系。
不管是爬虫获取的,浏览器收到的还是从本地硬盘读取的,都是二进制,选择正确的编码类型,才能把二进制或者说01序列解析为正确的字符。也就是用何种方式解析01数字
在这篇文章中,我描述了JavaScript中常见的5种不良编码习惯。重要的是,本文会给出一些可行的建议,如何的摆脱摆脱这些习惯。
之前一次使用post请求上传图片过多,post请求理论上对参数的大小没有限制,但是服务器有限制,导致上传失败,这时设置一下Tomcat的server.xml里面的maxPostSize就可以了。不过还是建议使用Form表单提交文件
Don Roberts 提出的一条重构准则:第一次做某件事时只管去做;第二次做类似的事时会产生反感,但无论如何还是可以去做;第三次再做类似的事时,你就应该重构
通过改变charset=utf-8中的utf-8就可以改变网页的编码。 一般我们在写CSS文件时候也需要在CSS文件顶部使用@charset utf-8;来定义此CSS文件编码类型。一般html源代码和css文件编码要统一,如果不统一会导致CSS hack
我们知道一个字节可表示的范围是 0 ~ 255(十六进制:0x00 ~ 0xFF), 其中 ASCII 值的范围为 0 ~ 127(十六进制:0x00 ~ 0x7F);而超过 ASCII 范围的 128~255
在我们进行前端开发时,针对项目优化,常会提到一条:针对较小图片,合理使用Base64字符串替换内嵌,可以减少页面http请求。并且还会特别强调下,必须是小图片,大小不要超过多少KB,等等。
JavaScript 是弱类型的编程语言,我们在写代码的时候充斥着大量的类型转换,在我之前的文章 【JS进阶】你真的掌握变量和类型了吗 中有过相关的介绍,其实上面代码的核心就是用到了下面三个类型转换:
在前端开发中,最常见的字符编码方案是 UTF-8。UTF-8是一种可变长度的 Unicode 编码方案,可以表示几乎所有的字符,并且与 ASCII 兼容。由于互联网的广泛应用和多语言的支持
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!