ASCII
用1个字节表示所有字符,共可以表示2^8=256 个字符。用十六进制表示时只需2位即可表示所有ASCII字符。ASCII 表参见此处
ANSI
在早期,以英语为主的国家使用 ASCII 是完全够用的,但随着计算机的普及,ASCII 对非英语国家就显得捉襟见肘了,比如中国的汉字就有数万个,而 ASCII 最多只能表示 256 个,因此其他国家对ASCII编码进行扩展,用于显示本国的语言,用2个字节来表示,共可以表示2^16=65536个字符。
- ANSI在简体中文代表GB2312,繁体中文代表BIG5,日文代表JIS
- GB2312中共收录6763个汉字。中文通常有拼音和笔画两种排序方式,GB2312 中,3755 个一级中文汉字是按照拼音序进行编码的,而 3008 个二级汉字则是按部首笔画排列,因此并不能根据字符编码进行拼音排序,很多资料都说 GB2312(GBK) 是按照拼音排序的,实际上是不准确的,比如“赵钱孙李佘”排序的结果是“李钱孙赵佘”,显然不是我们想要的1。
- GBK编码:对GB2312进行扩充,收录了一些偏僻字、古汉字等。
1 |
|
在 PHP 中,也可以通过 (new \Collator('zh-CN'))->asort($name_list);
实现按拼音排序2,前提是你可以正常安装 intl,我通过sudo pecl install intl
折腾了半天,总是报错ERROR: /private/tmp/pear/temp/intl/configure --with-php-config=/usr/local/opt/php/bin/php-config --with-icu-dir=DEFAULT' failed
,我用 pecl 好像就从来没有安装成功过东西:(
Unicode 字符集
由于各国都制定了自己的编码,导致了群雄割据的局面,很不利于传播与使用,就在此时,Unicode 站了出来,计划将世界上所有字符统一编码,用4个字节表示一个字符,如汉字的“好”Unicode 编码为\u597d
,英文的字母“H”编码为\u0048
。
这样,所有字符都可以以统一的标准进行管理和表示了,因此迅速得到了Adobe、Apple、HP、IBM、Microsoft 等巨头的支持,并风靡全球。目前最新的版本为 2019 年 5 月公布的 12.1.0,已经收录超过 13 万个字符。
我们可以在字体编辑用中日韩汉字 Unicode 编码表查看到 Unicode 的 16 进制表示。
UTF-8 编码
你以为了有了Unicode就世界和平了?Too young, too simple!
Unicode 虽然结束了各国编码混战的局面,但一个英文字符也要为了符合标准而强行用两个字节来表示,这就导致浪费了一个字节的空间,将 16 进制的 Unicode 码转换成二进制得:
1 | 01011001 01111101 // 好 |
英文字符在 Unicode 中的每个字符都要浪费 1 个字节的空间,这造成了硬盘存储于带宽流量的巨大浪费,因此,UTF-8 横空出世!
那么,UTF-8是如何平衡统一与空间呢?
- 单字节的字符,字节的首位设为 0,对于英语文本,UTF-8 码只占用一个字节,与 ASCII 码完全相同(ASCII 最大值是
0111 1111
,因此首位必然是 0,也就与 ASCII 完全相同) - n 个字节的字符 (n>1),第一个字节的前 n 位设为 1(示例中的蓝色),第 n+1 位设为 0(示例中的黑色),后面字节的前两位都设为 10,这 n 个字节的其余空位填充该字符 Unicode 码,高位用 0 补足(示例中的红色)。
字符所占字节数 | 字节码的有效位数 | 起始字节码 | 末位字节码 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
---|---|---|---|---|---|---|---|
1 | 7 | U+0000 | U+007F | 0bbbbbbb | |||
2 | 11 | U+0080 | U+07FF | 110bbbbb | 10bbbbbb | ||
3 | 16 | U+0800 | U+FFFF | 1110bbbb | 10bbbbbb | 10bbbbbb | |
4 | 21 | U+10000 | U+10FFFF | 11110bbb | 10bbbbbb | 10bbbbbb | 10bbbbbb |
明白了 UTF-8 的规则,我们再看下汉字“好”的 UTF8 表示:11100101 10100101 10111101
根据 Wikipedia 的UTF8定义,UTF-8(8-bit Unicode Transformation Format)是一种可容纳1,112,064个Unicode码位的变长字符编码,每个字符会占用 1~4 个字节。通过其定义,我们明白 UTF-8 就是 Unicode 的一种转换格式,通过标记位和 Unicode 码位将 Unicode 进行转换。
明白了 Unicode 与 UTF-8,就能理解为什么绝大多数的代码都是以 UTF-8 编码,因为 UTF-8 能兼容绝大多数的语言,通用性非常好,而代码绝大部分又是英文字符,因此达到了通用性与空间的绝妙平衡。如果要储存一本《红楼梦》,使用 Unicode 是更明智的选择,它不仅包含了绝大多数的汉字,也使得每个汉字以 2 字节存储,达到兼容与空间的平衡。
UFT-8 with BOM 和 UFT-8 without BOM
UTF-8 不需要 BOM,尽管 Unicode 标准允许在 UTF-8 中使用 BOM。所以不含 BOM 的 UTF-8 才是标准形式,在 UTF-8 文件中放置 BOM 主要是微软的习惯(顺便提一下:把带有 BOM 的小端序 UTF-16 称作「Unicode」而又不详细说明,这也是微软的习惯)。BOM(byte order mark)是为 UTF-16 和 UTF-32 准备的,用于标记字节序(byte order)。微软在 UTF-8 中使用 BOM 是因为这样可以把 UTF-8 和 ASCII 等编码明确区分开,但这样的文件在 Windows 之外的操作系统里会带来问题。「UTF-8」和「带 BOM 的 UTF-8」的区别就是有没有 BOM。即文件开头有没有 U+FEFF。
但是,文字有 GBK、UTF-8、UFT-16 等多种编码方式,当我们将编写好的文件存储并发送给其他人,计算机识别的都是二进制啊,它怎么知道哪些是标志位,哪些是字符位呢?因此,我们在编写文件时,需要将该文件的编码类型一同写入文件,这样在解读文件内容时,按照对应的规则解读就可以还原了,具体可阅读之前的文章计算机是如何存储与解读各种文件的。
UTF8MB4
如今,Emoji 表情已经成为社交中最常用的符号😂,如果我们采用 UTF-8 存储 Emoji,则会导致溢出问题,因此 MySQL <= 5.7 存储 Emoji 需手动将编码设置为 Emoji,MySQL 8.0+ 则默认为 UTF8MB4 编码。
我们可以在这里查询到常用的 Emoji 🤗
URL 编码
URL 编码同样是为了将 URL 中的非 ASCII 字符转换为 ASCII 字符传输,URL 编码也是 ASCII 编码,如空格的 URL 编码为%20
,空格在 ASCII 中的十六进制表示也是20
。其保留字符的百分号编码如下:
|
! |
# |
$ |
% |
& |
' |
( |
) |
* |
+ |
, |
/ |
: |
; |
= |
? |
@ |
[ |
] |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
%20 | %21 | %23 | %24 | %25 | %26 | %27 | %28 | %29 | %2A | %2B | %2C | %2F | %3A | %3B | %3D | %3F | %40 | %5B | %5D |
根据最新的RFC 3986标准,非 ASCII 字符建议先转换为 UTF-8 字节序列,然后对其字节值使用百分号编码。因此,汉字“好”的 UTF-8 编码为E5A5BD
,URL 编码为%E5%A5%BD
。
另外早期的 URL 编码规则会将空格编码为+
,而现在会处理为%20
,PHP 中则分别对应处理函数urlencode()
与rawurlencode()
。
关于 URL 编码的历史课查看阮一峰老师于十年前所写的文章关于 URL 编码
Base64 编码
顾名思义,Base64 是一种基于 64 个可打印字符(A~Z
、a~z
、0~9
、+
、/
)来表示二进制数据的表示方法。由于2^6=64,所以每 6 个位为一组,对应某个可打印字符。完整的 Base64 定义可见 RFC 1421 和 RFC 2045。由于现在需要 6bit 表示原本用 8bit 表示的 1 个字节,因此编码后的数据比原始数据大 30%。
最初的电子邮件只能使用 ASCII 字符,因此 Base64 的发明就被用于将图片或非 ASCII 字符转换为 ASCII 字符。其字符列表如下:
数值 | 字符 | 数值 | 字符 | 数值 | 字符 | 数值 | 字符 |
---|---|---|---|---|---|---|---|
0 | A | 16 | Q | 32 | g | 48 | w |
1 | B | 17 | R | 33 | h | 49 | x |
2 | C | 18 | S | 34 | i | 50 | y |
3 | D | 19 | T | 35 | j | 51 | z |
4 | E | 20 | U | 36 | k | 52 | 0 |
5 | F | 21 | V | 37 | l | 53 | 1 |
6 | G | 22 | W | 38 | m | 54 | 2 |
7 | H | 23 | X | 39 | n | 55 | 3 |
8 | I | 24 | Y | 40 | o | 56 | 4 |
9 | J | 25 | Z | 41 | p | 57 | 5 |
10 | K | 26 | a | 42 | q | 58 | 6 |
11 | L | 27 | b | 43 | r | 59 | 7 |
12 | M | 28 | c | 44 | s | 60 | 8 |
13 | N | 29 | d | 45 | t | 61 | 9 |
14 | O | 30 | e | 46 | u | 62 | + |
15 | P | 31 | f | 47 | v | 63 | / |
编码示例
我们已经了解了 Base64 的编码规则,现在就来徒手编码字符Man
。
汉字本身可以有多种编码,比如 GB2312、UTF-8、GBK 等等,每一种编码的 Base64 对应值都不一样。我们以UTF-8编码的汉字“好”(其十六进制为597D
)为例:
你可能会注意到,我们经常会在 Base64 编码中发现有=
字符,比如moky
的 Base64 编码为bW9reQ==
,而=
却没有出现在字符列表中,这是为什么呢?这是因为Base64末位的=
仅仅是为了代表补足的字节数,每个=
代表了 2bit。如果我们仅解码bW9reQ
,同样能得到moky
。
这手动编码还是太费劲了,我们还是用代码自动编码吧。由于非 ASCII 字符在不同的编码规则下结果不同,因此我们只编写 ASCII 字符的 Base64 编码:
1 |
|
Base64 解码自然就是编码的逆运算,在此不做赘述。
Base64 有什么用?
经过Base64编码后的空间会增大 30%,我们为什么会这样做?我们用空间换取了什么?
- Base64 将 ASCII 不可见字符转换为可见字符,因此解决了早期电子邮件中的图片传输问题。
- 其次,Base64 能做简单的对称加密。
- 最后,通过 Base64 将所有非 ASCII 字符转换为 ASCII,便于计算机的统一传输处理。
base64 图片
以下是一张 5*5 像素的黑色图片的 Base64 编码
1 | data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAyMDJwMfAxcCbmFxc4BgQ4ANUwgCjUcG3a0DVQHBZF2TWJRnvFbbWr5028n1bzP/4mz2mehTAlZJanAyk/wBxWnJBUQkDA2MKkK1cXlIAYncA2SJFQEcB2XNA7HQIewOInQRhHwGrCQlyBrJvANkCyRmJQDMYXwDZOklI4ulIbKi9IMDj4urjoxBgZGJo7kHAuaSDktSKEhDtnF9QWZSZnlGi4AgMpVQFz7xkPR0FIwMjAwYGUJhDVH++AQ5LRjEOhFjKUwYG41ygoAZCLEuAgWH3NwYGwa0IMfWHQG/NZWA4EFCQWJQIdwDjN5biNGMjCJt7OwMD67T//z+HMzCwazIw/L3+///v7f///13GwMB8C6j3GwAfrGAmadjDTwAAAGxlWElmTU0AKgAAAAgABAEaAAUAAAABAAAAPgEbAAUAAAABAAAARgEoAAMAAAABAAIAAIdpAAQAAAABAAAATgAAAAAAAABIAAAAAQAAAEgAAAABAAKgAgAEAAAAAQAAAAWgAwAEAAAAAQAAAAUAAAAADJnIDgAAAAlwSFlzAAALEwAACxMBAJqcGAAAABVJREFUCB1jlJGRYUACTEhsEJNUPgAbeABexhoAlQAAAABJRU5ErkJggg== |
以下这张图片的十六进制内存地址表示。想必图片编码为 Base64 形式同字符相同,将二进制每 6 位为一组,用可见字符表示。但我现在读不懂内存地址😂,所以不知道从何处开始进行分组,如果起始位错误,那结果将引发雪崩效应。如果有大佬能指点一二,不胜感激。
自定义编码
了解了 Base64 的原理后,我们完全可以自行定义一套编码规则,比如比特币的钱包地址就是中本聪采用 Base58 编码,其中字符去除了 0
- O
,I
- l
等易混淆的字符,以及非字母数字字符+
、/
。
数值 | 字符 | 数值 | 字符 | 数值 | 字符 | 数值 | 字符 |
---|---|---|---|---|---|---|---|
0 | 1 | 15 | G | 30 | X | 45 | n |
1 | 2 | 16 | H | 31 | Y | 46 | o |
2 | 3 | 17 | J | 32 | Z | 47 | p |
3 | 4 | 18 | K | 33 | a | 48 | q |
4 | 5 | 19 | L | 34 | b | 49 | r |
5 | 6 | 20 | M | 35 | c | 50 | s |
6 | 7 | 21 | N | 36 | d | 51 | t |
7 | 8 | 22 | P | 37 | e | 52 | u |
8 | 9 | 23 | Q | 38 | f | 53 | v |
9 | A | 24 | R | 39 | g | 54 | w |
10 | B | 25 | S | 40 | h | 55 | x |
11 | C | 26 | T | 41 | i | 56 | y |
12 | D | 27 | U | 42 | j | 57 | z |
13 | E | 28 | V | 43 | k | ||
14 | F | 29 | W | 44 | m |
关于 Base64 编码可查看阮一峰于 2008 年所作文章Base64 笔记
JSON编码
json 在计算机中应用广泛,相信很多读者对它都不陌生。JSON 是 JavaScript Object Notation 的缩写,尽管 JSON 是 JavaScript 的一个子集,但 JSON 是独立于语言的 文本格式,并且采用了类似于 C 语言家族的一些习惯。JSON 被广泛应用于数据传输格式、配置文件等场景,其 MIME类型是application/json
,文件扩展名是 .json
。以下是一个简短的 JSON 示例,具体的 JSON 格式描述可以参考 RFC 4627。需要注意一点的是,JSON 中的字符串引号必须为双引号,单引号是非法的。
1 | { |
PHP 中的 JSON
中文被编码问题
在 PHP < 5.4的版本中,当进行json_encode()
的数据有中文时,中文会被Unicode编码,所以需要在json编码前先urlencode,编码后再urldecode,以免中文被Unicode编码。在 PHP >= 5.4的版本中,只需加入JSON_UNESCAPED_UNICODE
这个参数,就无需urlencode了,如json_encode($str, JSON_UNESCAPED_UNICODE)
。
json_encode 浮点数精度丢失问题
在 PHP >= 7.1 的版本中,json_encode(277.2)
的结果是277.19999999999999
,即使最新发布的 PHP7.4 中仍存在同样的问题。
解决该问题的方式是修改 php.ini 中的serialize_precision
参数,将默认值 17
修改为 -1
即可。
轶事
由于创作者认为 JSON 已足够简洁,无需再进行优化,因此JSON没有版本号。
总结
在很多命名中,数字都有其明确的含义,如 UTF-8 中的 8 表示该编码方式中,最短的文本只需 8 个比特位,Base64 中的 64 表示基于 64 个字符进行编码,而 SHA-256 中的 256 则表示 256 bit 参与散列运算,我们看到命名中的数字,应该有一定的敏锐性,也许这会让你有更好的理解,让你了解其本质时打开一扇新的大门,获取新的启发。
诸如 Base64 和 URL 编码,都是将非 ASCII 字符编码为 ASCII 字符处理,毕竟计算机能完美兼容 ASCII。世界上的字符形式有千千万,在计算机的世界只认 0 和 1,将非 ASCII 转换为 ASCII 统一处理,化繁为简,是一个不错的解决思路。