文字编码的那些事

咱们常常听到纯文本格式和二进制编码,什么是纯文本,什么是二进制呢?以一个例子作说明。新建一个文件叫hello.txt,内容为:html

hello, world复制代码

这个文件有12个字节:git

借助Node.js能够看这个文件在硬盘的原始二进制存储是什么,以下代码:web

let fs = require("fs");
// 读取原始二进制内容
let buffer = fs.readFileSync("hello.txt"); 
console.log(buffer);复制代码

运行后控制台输出12个字节的二进制内容(以十六进制显示):正则表达式

<Buffer 68 65 6c 6c 6f 2c 20 77 6f 72 6c 64>复制代码

参考ASCII表,咱们发现这些数字恰好是英文对应的ASCII编码,以下图所示:算法

若是这个文本文件以utf-8读取的方式:sql

let fs = require("fs");
let text = fs.readFileSync("hello.txt", "utf-8"); 
console.log(text);复制代码

输出的就是文本:windows

这里看到了两种大相径庭的输出结果,但实际上不论是纯文本文件仍是二进制文件,硬盘或者内存里存储的都是0101,就看你如何解读它,或者说怎么解码。(只不过咱们一般说的纯文本是指那种可以解码成可读文本的格式,二进制文件格式指那种没法用UTF-8等文本解码的文件如图片。)浏览器

以下图所示:bash


若是认为是UTF-8的话,一个编码能够对应一个文字。文字的字形又是怎么来的呢?它是在字体文件里面, 以svg矢量格式存储着每一个字符的形状。究竟什么是UTF/UTF-8编码呢?编辑器

UTF编码

1个字节最多只表示0 ~ (2^8 – 1)共256个字符,ASCII使用7位表示128个字符,可以知足现代英语的要求,对于特殊符号、亚洲语言、Emoj又应该如何表示?咱们按上面的方法,查看如下包含中文和Emoji字符的文件存储的是什么:

we 发 财 🤑复制代码

以下图所示:

其中20是空格的编码,能够看到一个英文仍是1个字节,一个中文用了3个字节,而一个Emoj用了4个字节。它怎么知道每次应该读取多少个字节呢?以下图所示:

若是一个字节是0开头,表示这个字节就表示一个字符,若是是3个1开头表示这个字符要占3个字节,有多少个1就表示当前字符占用了多少个字节。这个就是UTF-8的存储特色,UTF规定了每一个字符的编号,而UTF-8定义了字符应该怎么存储。从unicode官网能够查到,“我”的UTF编码是6211,以下图所示:

6211怎么变成utf-8编码呢?由于6211落在下面这个范围:

U+ 0800 ~ U+ FFFF: 1110XXXX 10XXXXXX 10XXXXXX复制代码

因此是这么转的:

“我”的utf-8就是E6 88 91,能够对比encodeURIComponent的结果:

能够说utf-8让utf获得了实现,utf-8是当前互联网上使用最广的文本编码方式。除了utf-8还有utf-16,它们和utf的转换关系以下所示:

UTF-8 
U+ 0000 ~ U+ 007F: 0XXXXXXX
U+ 0080 ~ U+ 07FF: 110XXXXX 10XXXXXX 
U+ 0800 ~ U+ FFFF: 1110XXXX 10XXXXXX 10XXXXXX 
U+10000 ~ U+10FFFF: 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX

UTF-16 
U+0000 - U+FFFF         xxxxxxxx xxxxxxxx
U+10000 - U+10FFFF   110110xx xxxxxxxx 110111xx xxxxxxxx
复制代码

utf-8的优势在于一个英文只要一个字节,可是一个中文倒是3个字节,utf-16的优势在于编码长度固定,一个中文只要两2个字节,可是一个英文也要两个字节。因此对于英文网页utf-8编码更加有利,而对于中文网页使用utf-16应该更加有利。由于绝大部分的中文都是落在U+0000到U+FFFF。而对于Emoj这种不太经常使用排在后面的符号,不论是utf-8仍是utf-16都须要4个字节。同时utf-32都是固定4个字节。

完整的UTF编码能够在官网查到,这里列一些符号及其编码的范围,以下图所示:

中文汉字是从4E00到9FFF,大约有两万个。FXXXX和10XXXX的编码是用于自定义的,如能够用于图标字体,可是图标字体一般没有使用这个范围,而是用更简短的编码,这些编码恰好映射到其它正常字符集如繁体字,这样就致使图标字体还没加载好以前系统会使用默认的字体,页面就会先显示繁体字而后再恢复成图标。这种问题在安卓手机会出现。

咱们能够直接在html上使用UTF编码,如:

那么网页上将会显示:

这个也叫html实体(entity),一般用于特殊符号的转义或者图标字体。

而后咱们再讨论下乱码。

乱码

用文本编辑器打开一个二进制文件,例如打开一个图片文件:

不少文本编辑器默认是使用utf-8编码,如submlime:

每一个编码若是对应一个符号就显示出来,可是这些符号连起来看起来比较乱,因此就“乱码”了。

这里举一个实际的乱码问题,windows的压缩包,在mac解压文件名一般会乱码,以下图所示,这是为何呢?

windows的默认编码方式是ANSI,使用windows自带的文本编辑器能够保存如下几种编码:

ANSI根据locale,简体中文是使用GBK。什么是GBK呢?GBK是中文的地方性编码,如“我”的GBK编码是CED2,最开始的中文编码标准是GB2312,收录了6000多个经常使用汉字,后来又出了GBK,把繁体字收进去了,再后来又出了GB18030把少数民族的语言收录了,各类编码关系以下图所示:

可是Mac的软件通常是使用utf-8,中文应该是3个字节,可是如今只有2个字节,还对不上,因此解压的时候就变成了其它的符号,看起来乱码了。Mac能够装一个叫the unarchive的软件,它有算法自动检测字符编码:

用它解压的文件名就没有问题。另外,不少代码编辑器通常默认是使用utf-8,在windows自带的文本编辑器保存的txt在这种编辑器打开将会乱码:

当选择了正确的编码方式后显示就正常了:

关于文字编码还有一个常常会遇到的问题,那就是回车与换行。

回车与换行

Sublime的默认换行方式是根据系统设置,以下图Sublime的设置:

从它的注释还能够看到windows是使用CRLF,而unix系的系统是使用LF:

CR:Carriage Return (回车\r)

LF:Line Feed(换行\n)

也就是说windows的换行是\r\n,而unix系的如Mac是使用\n。回车和换行有什么区别呢?回车是指把光标移到行首,而换行是把光标换到下一行。若是在Node.js里面运行如下代码:

console.log("hello, world\rgoodbye, world");复制代码

那么将会输出"goodbye, world":

"hello, world"被覆盖了,这就是回车符的做用。后来可能出于节省空间的目的,unix的换行就只有\n了。

"\r"在git里面会被显示成^M:

有时候还会遇到另一个问题:在绑host的时候,从QQ复制的消息粘贴到host文件里,MAC可能会多了个\r致使没有生效,而windows可能会少了\r出问题,虽然在你的编辑器里看起来可能都是正常换行的。

再讨论下字符串长度的问题。

字符串长度

Java和JS的字符串都是使用UTF-16编码,由于它有长度比较固定的优点,不像UTF-8字节数可能从1变到4。以下图所示:

英文和中文长度都是1,而Emoj的长度是2,由于长度单位是2个字节做为1,Emoj的须要4个字节,所以长度是2。

可使用charCodeAt返回当前字符的utf编码:

若是是要检测中文的话可使用正则表达式,看当前符号是否落在中文编码的范围内:

在Mysql里面,若是一个字段的类型为VARCHAR(10)的话,那么它最多能够存储10个英文或者10个中文,若是这个字段使用的是默认的utf-8编码,那么它须要占10 * 3 = 30个字节,若是使用了GBK编码,那么它须要使用10 * 2 = 20个字节。

meta charset标签

咱们一般会给页面head标签加一个charset的meta,指明当前页面的编码方式:

<meta charset="utf-8">复制代码

在html4里面是这么写的:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />复制代码

这两个做用同样,只是后者已通过时。这个编码究竟有什么用呢?

如下以code.html做为研究:

<!DOCType html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>
    你好world
</body>
</html>复制代码

而后咱们用Node.js写一个最简单的server:

let http = require("http");
let fs = require("fs");

http.createServer((req, res) => {
    let content = fs.readFileSync("./code.html", "utf-8");
    console.log(req.url);
    res.end(content);
}).listen("8125", err => {
    if (err) {
        console.log(err);
    } else {
        console.log("Server start, listening on http://localhost:8125");
    }
});复制代码

运行,而后访问localhost:8125,页面正常显示:

如今我把charset改为gbk:

<meta charset="gbk">复制代码

而后从新刷新页面,就不正常了:
可是若是我在Node.js的server里面设置Content-Type的响应头的charset为utf-8的话:

res.setHeader("Content-Type", "text/html; charset=utf-8");复制代码

从浏览器控制台能够看到这个响应头:

无论meta标签里面设置的是什么编码,页面都能正常显示。

因此根据咱们的观察meta标签只有在http的响应头里没有设置charset的时候才会起做用。


本篇讨论了utf/utf-8/utf-16的关系,utf是国际标准,规定了每一个字符的编码,而utf-8/utf-16决定了utf该如何存储与读取,utf-8的优势是对于英文比较有利,比较节省空间,utf-16对于中文比较有利。可是若是西方国家使用utf-8,而后东方国家使用utf-16,那么互联网可能就乱了,因此从统一标准的角度咱们仍是使用utf-8。还讨论了GBK编码和乱码的问题,若是一个字符存的时候是用的一种编码,可是读的时候却用的另外一种编码,那就会对不上原先的字符,就会出现乱码的状况。另外,因为utf-16编码长度比较固定,因此JS和Java使用了utf-16作为它们在内存里字符串的编码。根据实验,meta的charset标签在没有设置响应头的charset时能够起做用。

总之字符编码是一个很大的话题,本篇主要讨论了和web关系比较大的部分,还列了平时会遇到的几个问题。相信看完本文,你对文字编码应该有了一个比较好的理解。

相关文章
相关标签/搜索