我们经常会听到ascii、unicode、utf-8,对于这些名词概念总是很模糊,似懂非懂,大部分时候不会遇到问题,即使遇到问题百度一下也能解决。其实花几分钟弄清楚这些概念其实很容易,让知识变得具体,下次遇到字符问题,就可以做到心中有数
什么是ASCII和Unicode?
ASCII和Unicode都是字符集,说简单点,就是一个表格,规定97对应英文字母"a",98对应英文字母"b",23383对应中文"字"等等,ASCII和Unicode的区别是Unicode这个"表格"更大
什么是utf-8?
utf-8是编码方式的一种,所谓的编码就是一种算法,字符集经过这种算法计算后,变成了另外的数字。比如英文字母a,经过utf-8编码后变成了\u0061,中文"字"经过utf-8编码后变成了\u5b57(十六进制)
深入了解
所以,ASCII和Unicode是字符集,而utf-8只是众多编码算法中的一种。接下来我们讨论几个问题,彻底弄明白这些概念
为什么会有ASCII?
ASCII全称是American Standard Code for Information Interchange,美国信息交换标准代码,计算最早是在美国被发明,为了能表示英文字母,就需要有一套处理字符的标准,于是就出现了ASCII
ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符,用一个字节就可以存储,它等同于国际标准 ISO/IEC 646。
有了ASCII为什么还需要另一种字符集Unicode?
ASCII 编码是美国人给自己设计的,英文字母大写加小写总共52个,再算上数字和符号,1字节也足够了(1byte=8bit,最大能表示的数是255)。
于是欧洲那些扩展的拉丁字母,中文、韩语和日语都无法被加入ASCII。
各个国家为了让本国公民也能正常使用计算机,开始效仿 ASCII 开发自己的字符编码,例如 ISO/IEC 8859(欧洲字符集)、shift_Jis(日语字符集)、GBK(中文字符集)等。
这些字符集都有自己的编码,不同语言之间交流因为编码不同而出现乱码,为了解决这个问题就出现了Unicode字符集,Unicode 是国际标准字符集,它将世界各种语言的每个字符定义一个唯一的编码,字符所在码表的位置叫码点(code point),Unicode字符集的编码范围是 0x0000 - 0x10FFFF(0-1114111),可以容纳一百多万个字符
为什么需要utf-8?
unicode字符集只是规定了字符的二进制代码,却没有规定字符应该如何存储,最简单的方式就是直接存储字符对应的二进制(code point),这种编码方式叫UCS-4,UCS-4使用4个字节来储存1个字符。
但直接存储code point
有一个问题:浪费空间,尤其是英文字母1字节就足够了,但用了4倍的空间来存储,造成了严重的空间浪费。
所以才出现了utf-8编码,他是变长编码方式,很好的解决了直接存储code point
的问题
utf-8为什么变成了世界上最流行的编码?
先看看utf-8编码算法,他规定一个code point
满足:
- 0 ~ 127 --------> 使用0xxxxxxx 模板存储
- 128 ~ 2047 -----> 使用110xxxxx 10xxxxxx 模板存储
- 2048 ~ 65535 ---> 使用1110xxxx 10xxxxxx 10xxxxxx 模板存储
- 65536 ~ 2097151-> 使用11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 模板存储
例如,汉字"字"utf-8编码过程:
查unicode中的code point
是23383
23383在2048 ~ 65535范围内,用1110xxxx 10xxxxxx 10xxxxxx模板。
23383转为二进制是1011_0110_1010_111
这个二进制不足16位,先补足16位:0101_1011_0101_0111
带入模板中:1110_0101 _10101101 _10010111
转为16进制:\xe5ad97。这就是utf-8编码的"字"
这样设计有什么好处?
不同范围的code point使用不同的字节存储,可以节省大量的空间
为什么开头要设计成那样的?
这是为了方便解码,从第一个bit开始
- 如果是0,那后面的1字节(8bit)存储的1个字符
- 如果非0,就看几个1开头,2个1开头,那后面2个字节(16bit)存储1个字符
- 3个1开头,后面3个字节(24bit)存储1个字符
- 4个1开头,后面4个字节(32bit)存储1个字符
那为什么从第二个字节开始,都是10开头呢?
这是为了保证在传输过程中出现中断,也能顺利的找到下一个字符的开头,只需要找到第一个非10开头的字节,就是下一个字符的开头了。
utf-8能否兼容ascii呢?
可以,因为0-127的存储方式0xxxxxxx正好就是ascii的存储方式。
以上这些原因使得utf-8成为了世界上最流行的编码。
动手实现一遍utf-8编码
js
function codePointToUtf8(codePoint) {
var utf8Bytes = [];
var code = codePoint;
// 判断代码点的大小,确定使用多少个字节进行编码
if (code <= 0x7F) {
utf8Bytes.push(code);
} else if (code <= 0x7FF) {
utf8Bytes.push(0xC0 | (code >> 6));
utf8Bytes.push(0x80 | (code & 0x3F));
} else if (code <= 0xFFFF) {
utf8Bytes.push(0xE0 | (code >> 12));
utf8Bytes.push(0x80 | ((code >> 6) & 0x3F));
utf8Bytes.push(0x80 | (code & 0x3F));
} else if (code <= 0x1FFFFF) {
utf8Bytes.push(0xF0 | (code >> 18));
utf8Bytes.push(0x80 | ((code >> 12) & 0x3F));
utf8Bytes.push(0x80 | ((code >> 6) & 0x3F));
utf8Bytes.push(0x80 | (code & 0x3F));
} else {
throw new Error("Invalid code point");
}
// 返回字节数组 return utf8Bytes;
}
// 示例:将 Unicode 代码点 U+5B57 (中文字符"字") 转换为 UTF-8
var utf8Array = codePointToUtf8(0x5B57);
console.log(`UTF-8 encoding of "字" (U+5B57): ${utf8Array.map(byte => byte.toString(16)).join('')}`);
elixir
defmodule Utf8Converter do
def code_point_to_utf8(code_point) when code_point < 0x80 do
<<code_point>>
end
def code_point_to_utf8(code_point) when code_point < 0x800 do
<<0xC0 + (code_point >>> 6), 0x80 + (code_point & 0x3F)>>
end
def code_point_to_utf8(code_point) when code_point < 0x10000 do
<<0xE0 + (code_point >>> 12),
0x80 + ((code_point >>> 6) & 0x3F),
0x80 + (code_point & 0x3F)>>
end
def code_point_to_utf8(code_point) do
<<0xF0 + (code_point >>> 18),
0x80 + ((code_point >>> 12) & 0x3F),
0x80 + ((code_point >>> 6) & 0x3F),
0x80 + (code_point & 0x3F)>>
end
end
# 示例:将 Unicode 代码点 0x5B57 (中文字符"字") 转换为 UTF-8
binary = Utf8Converter.code_point_to_utf8(0x5B57)
IO.inspect binary
MySQL中的utf8和utf8mb4
MySQL旧版本中的utf8实际上不是真正的UTF-8编码, 因为他每个字符最多3个字节存储, 对于超过3个字节的字符就会出错,MySQL5.5.3开始引入utf8mb4,这才是真正的UTF-8编码,采用最多4个字节存储。