字符编码
简介
起初再考虑写不写这篇文章, 感觉这篇文章比较枯燥乏味, 而且自己感觉也没理解的太透彻, 就把理解的记录下来, 所以这是纪念版的
Go hard or go home 要么全力以赴, 要么走人
No person has the right to rain on your dreams,you do it yourself.
没有人有权利给你的梦想泼冷水, 只有你自己给自己的梦想泼冷水
看到这样的文字是不是很励志? 那换一种方式你还会这样想吗? 16 进制版:
476f2068617264206f7220676f20686f6d652089814e485168529b4ee58d742c89814e488d704eba20a4e6f20706572736f6e20206861732074686520726967687420746f207261696e206f6e20796f757220647265616d732c796f7520646f20697420796f757273656c662e206ca167094eba6709674352297ed94f60768468a660f36cfc51b76c342c53ea67094f6081ea5df17ed981ea5df1768468a660f36cfc51b76c34
然而他的字符编码是 GB2312 的, 叫我转化成易懂的字符串, 当时我就懵 b 了因为当时我对字符编码一窍不通, 然后就网上, 查啊查, 最后终于想到了解决方案
几个值的深思的问题
什么是字符?
字符是各种文字和符号的总称, 包括各个国家文字标点符号图形符号数字等
什么是字符集?
字符集是多个字符的集合, 字符集种类较多, 每个字符集包含的字符个数不同, 常见字符集有: ASCII 字符集 ISO 8859 字符集 GB2312 字符集 BIG5 字符集 GB18030 字符集 Unicode 字符集等
什么是字符编码?
1 计算机要准确的处理各种字符集文字, 需要进行字符编码, 以便计算机能够识别和存储各种文字
2 字符编码 (encoding) 和字符集不同字符集只是字符的集合, 不一定适合作网络传送处理, 有时须经编码 (encode) 后才能应用如 Unicode 可依不同需要以 UTF-8UTF-16UTF-32 等方式编码
3 字符编码就是以二进制的数字来对应字符集的字符 因此, 对字符进行编码, 是信息交流的技术基础
概括
1 使用哪些字符也就是说哪些汉字, 字母和符号会被收入标准中所包含字符的集合就叫做字符集
2 规定每个字符分别用一个字节还是多个字节存储, 用哪些字节来存储, 这个规定就叫做编码
3 各个国家和地区在制定编码标准的时候, 字符的集合和编码一般都是同时制定的因此, 平常我们所说的字符集, 比如: GB2312, GBK, JIS 等, 除了有字符的集合这层含义外, 同时也包含了编码的含义
4 注意: Unicode 字符集有多种编码方式, 如 UTF-8UTF-16 等; ASCII 只有一种; 大多数 MBCS(包括 GB2312,GBK)也只有一种
有趣的例子
1 在显示器上看见的文字图片等信息在电脑里面, 其实并不是我们看见的样子, 即使你知道所有信息都存储在硬盘里, 把它拆开也看不见里面有任何东西, 只有些盘片假设, 你用显微镜把盘片放大, 会看见盘片表面凹凸不平, 凸起的地方被磁化, 凹的地方是没有被磁化; 凸起的地方代表数字 1, 凹的地方代表数字 0 硬盘只能用 0 和 1 来表示所有文字图片等信息
2 那么字母 A 在硬盘上是如何存储的呢? 可能小张计算机存储字母 A 是 1100001, 而小王存储字母 A 是 11000010, 这样双方交换信息时就会误解比如小张把 1100001 发送给小王, 小王并不认为 1100001 是字母 A, 可能认为这是字母 X, 于是小王在用记事本访问存储在硬盘上的 1100001 时, 在屏幕上显示的就是字母 X 也就是说, 小张和小王使用了不同的编码表小张用的编码表是 ASCII,ASCII 编码表把 26 个字母都一一的对应到 2 进制 1 和 0 上; 小王用的编码表可能是 EBCDIC, 只不过 EBCDIC 编码与 ASCII 编码中的字母和 01 的对应关系不同一般地说, 开放的操作系统 (LINUX WINDOWS 等) 采用 ASCII 编码, 而大型主机系统 (MVS OS/390 等) 采用 EBCDIC 编码在发送数据给对方前, 需要事先告知对方自己所使用的编码, 或者通过转码, 使不同编码方案的两个系统可沟通自如
这个例子说明了三点
1 不管是任何文字图片等, 最后都会以二进制的形式储存到电脑的磁盘中(比如记事本 A.txt, 内容为 "ABC" 文件, 在此磁盘中表现的就是 01 01 这种二进制形式)
盘片表面凹凸不平, 凸起的地方被磁化, 凹的地方是没有被磁化, 凸起的地方代表数字 1, 凹的地方代表数字 0 硬盘只能用 0 和 1 来表示所有文字图片等信息是的 很强势
2 任何文件要储存到电脑中, 都会事先进行编码, 然后储存到电脑的磁盘中, 比如 A.txt 文件, 默认编码为 ANSI 编码, 也可以编码为 UTF-8, 然而不同的编码方式 对应着计算机用一个字节还是多个字节存储, 用哪些字节来存储
3 在双方数据进行通讯时, 要么就保证发送方和接受方的数据编码是相同, 要么就是其中一方需要转码
什么是字节和位?
字节 byte 和位 bit 是电脑里的数据量单位
1. 按计算机中的规定, 一个英文的字符占用一个字节, 而一个汉字以及汉字的标点符号字符都占用两个字节
2.1 个字节等于 8 位 1byte=8bit
3.1bit 在磁盘中以二进制 01 的形式保存 凸起的地方代表数字 1, 凹的地方代表数字 0
ASCII
ASCII 码是西欧编码的方式, 采取 7 位编码, 所以是 2^7=128, 共可以表示 128 个字符, 包括 34 个字符,(如换行 LF, 回车 CR 等), 其余 94 位为英文字母和标点符号及运算符号等
ASCII 对照表
重点:
字符集: 从符号 (NUL="/0"= 空操作字符) 到 Z 再到 DEL 符号
字符编码范围: 二进制: 0000000001111111 十进制: 0-127
占用字节: 1 字节 8bit 盘片储存方式: 凹凹凹凹凹凹凹凹凸凸凸凸凸凸凸凸
注: NUL:\0'是一个 ASCII 码为 0 的字符, 从 ASCII 码表中可以看到 ASCII 码为 0 的字符是空操作字符, 它不引起任何控制动作, 也不是一个可显示的字符
但我们发现 ASCII 码是没有中文编码的, 显然在天朝是不够用的, 于是 GB2312 诞生了
GB2321
GB2312 是对 ASCII 的中文扩展兼容 ASCII
编码规定:
编码小于 127 的字符与 ASCII 编码相同,
特性: 两个大于 127 的字符连在一起时, 就表示一个汉字, 前面的一个字节 (称之为高字节) 从 0xA1 用到 0xF7, 后面一个字节 (低字节) 从 0xA1 到 0xFE, 这样我们就可以组合出大约 7000 多个简体汉字了
字符集: 从符号 (NUL="/0"= 空操作字符) 到 Z 到齄 "(简体中文)
字符编码范围: 16 进制: 0x0000-(中间有一部分是未使用的)-0xF7FE
占用字节: 英文 1 字节 8bit 盘片储存方式: 凹凹凹凹凹凹凹凹凸凸凸凸凸凸凸凸
中文 2 字节 16bit 凹凹凹凹凹凹凹凹凹凹凹凹凹凹凹凹...
GBK
GBK 兼容 ASCLL 兼容 GB2312 是 GB2312 的扩展
但是中国的汉字太多了, 我们很快就就发现有许多人的人名没有办法在这里打出来, 不得不继续把 GB2312 没有用到的码位找出来用上后来还是不够用, 于是干脆不再要求低字节一定是 127 号之后的内码, 只要第一个字节是大于 127 就固定表示这是一个汉字的开始, 不管后面跟的是不是扩展字符集里的内容结果扩展之后的编码方案被称为 GBK 标准, GBK 包括了 GB2312 的所有内容, 同时又增加了近 20000 个新的汉字 (包括繁体字) 和符号
Unicode
Unicode 是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案
目前的 Unicode 字符分为 17 组编排, 0x0000 至 0x10FFFF, 每组称为平面(Plane), 而每平面拥有 65536 个码位, 共 1114112 个然而目前只用了少数平面 UTF-8UTF-16UTF-32 都是将数字转换到程序数据的编码方案
UTF-8
UTF-8 以字节为单位对 Unicode 进行编码从 Unicode 到 UTF-8 的编码方式如下:
UTF-8 的特点是对不同范围的字符使用不同长度的编码对于 0x00-0x7F 之间的字符, UTF-8 编码与 ASCII 编码完全相同 UTF-8 编码的最大长度是 6 个字节从上表可以看出, 6 字节模板有 31 个 x, 即可以容纳 31 位二进制数字 Unicode 的最大码位 0x7FFFFFFF 也只有 31 位
例 1: 汉字的 Unicode 编码是 0x6C490x6C49 在 0x0800-0xFFFF 之间, 使用用 3 字节模板了: 1110xxxx 10xxxxxx 10xxxxxx 将 0x6C49 写成二进制是: 0110 1100 0100 1001, 用这个比特流依次代替模板中的 x, 得到: 11100110 10110001 10001001, 即 E6 B1 89
举一个例子: It's 知乎日报
你看到的 unicode 字符集是这样的编码表:
- I 0049
- t 0074
- ' 0027
- s 0073
- 0020
知 77e5
乎 4e4e
日 65e5
报 62a5
每一个字符对应一个十六进制数字
计算机只懂二进制, 因此, 严格按照 unicode 的方式(UCS-2), 应该这样存储:
- I 00000000 01001001
- t 00000000 01110100
- ' 00000000 00100111
- s 00000000 01110011
- 00000000 00100000
知 01110111 11100101
乎 01001110 01001110
日 01100101 11100101
报 01100010 10100101
这个字符串总共占用了 18 个字节, 但是对比中英文的二进制码, 可以发现, 英文前 9 位都是 0! 浪费啊, 浪费硬盘, 浪费流量
怎么办?
UTF
UTF-8 是这样做的:
单字节的字符, 字节的第一位设为 0, 对于英语文本, UTF-8 码只占用一个字节, 和 ASCII 码完全相同;
n 个字节的字符(n>1), 第一字节的前 n 位设为 1, 第 n+1 位设为 0, 后面字节的前两位都设为 10, 这 n 个字节的其余空位填充该字符 unicode 码, 高位用 0 补足
高位字节 | 低位字节 | 低位字节 | 低位字节 | 低位字节 | 低位字节 |
---|---|---|---|---|---|
0xxxxxxx | |||||
110xxxxx | 10xxxxxx | ||||
1110xxxx | 10xxxxxx | 10xxxxxx | |||
11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | ||
111110xx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | |
1111110x | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
... .... |
这样就形成了如下的 UTF-8 标记位:
高位字节 | 低位字节 | 低位字节 | 低位字节 | 低位字节 | 低位字节 |
---|---|---|---|---|---|
0xxxxxxx | |||||
110xxxxx | 10xxxxxx | ||||
1110xxxx | 10xxxxxx | 10xxxxxx | |||
11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | ||
111110xx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | |
1111110x | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
... .... |
比如 "知" 字 在 Unicode 中占用两个字节, 那么第一字节 (我叫它高位字节) 的前两位设位 1, 第三位设为 10, 后面低位字节设为前两位设为 10, "知" 11100111 10011111 10100101
怎么知道知字占用两个字节的? 首先要知道 Unicode 字符集中, 知字的编码为 77e5, 然后转化为二进制流 01110111 11100101 的 bit, 每 8bit 等于 1byte 所以就占两个字节
于是, It's 知乎日报就变成了:
- I 01001001
- t 01110100
- ' 00100111
- s 01110011
- 00100000
知 11100111 10011111 10100101
乎 11100100 10111001 10001110
日 11100110 10010111 10100101
报 11100110 10001010 10100101
和上边的方案对比一下, 英文短了, 每个中文字符却多用了一个字节但是整个字符串只用了 17 个字节, 比上边的 18 个短了一点点
剧透: 一切都是为了节省你的硬盘和流量
一图解忧愁
汉字对照表
1. 从这个可以看出, 同样的字符集, 但 unicode 编码和 gbk 编码是不同的, 所以 unicode 字符集不兼容 gbk 字符集
2. 只要知道 unicode 字符集的编码表, 就可以用 UTF8 编码规则找到 UTF-8 对应的汉字编码
解决问题
从上面的内容了解了字符编码以后, 以后遇到相关的字符编码问题的时候至少有解决的思路, 而不是一头雾水
分析
NodeJS 服务端环境下
476f2068617264206f7220676f20686f6d652089814e485168529b4ee58d742c89814e488d704eba20a4e6f20706572736f6e20206861732074686520726967687420746f207261696e206f6e20796f757220647265616d732c796f7520646f20697420796f757273656c662e206ca167094eba6709674352297ed94f60768468a660f36cfc51b76c342c53ea67094f6081ea5df17ed981ea5df1768468a660f36cfc51b76c34
容易产生误区:
这个问题的情况并不是字符乱码问题, 而只是怎样解析 16 进制 gb2312 字符, 只是利用了字符编码的原理
1. 我接受的是 gb2312 格式的数据, 但是这里并没有乱码, 因为服务器发过来的是数字和英文, gb2312 是兼容 ASCII 的
2. 我设置了(接受响应数据编码格式)response.setEncoding('gb2312'); 即使我不设置响应格式, nodejs 默认识 utf-8 的, utf-8 和 gbk 都是兼容 ASCII, 也是就是支持英文和数字
- var http=require('http');
- var Iconv = require('iconv-lite');// 转码数据
- var GetHttp=function(options,callback){
- var AllData="";
- try{
- var GetReq = http.request(options, function (res) {
- console.log('STATUS:' + res.statusCode);
- res.setEncoding('gb2312');
- if(res.statusCode==200){
- res.on('data', function (chunk) { AllData+=chunk;})
- .on('end',function(){callback(200,AllData);})
- }else{
- callback(500,'error');
- }
- console.log(AllData);
- });
- GetReq.on('error',function(err){callback(500,err)});
- GetReq.end();
- }catch(error){
- callback(500,error);
- }
- }
- exports.GetHttp=GetHttp;
开始问题分析:
1. 字符集分析: gb2312 支持数字和英文和 6000 + 汉字
2. 编码分析: 英文占一个字节, 中文占两个字节(这就是问题)
- //1.fromCharCode() 可接受一个指定的 Unicode 值, 然后返回一个字符串但我们的数据是 gb2312 的编码数据, 然而 gbk 和 unicode 的编码方式又不一样, 所以解析出来的数据会乱码
- //2. 利用下面的代码, 中文也会乱码, 因为英文占 1 个字节, 中文占 2 个字节, 1 个字节是 8 个二进制流的 bit=2 个 16 进制流的 bit, 而中文 = 4 个 16 进制流的 bit, 下面的代码相当于把 1 个 16 进制的数转为字符
- function HexTostring(s) {
- var r = "";
- for (var i = 0; i < s.length; i += 2) { var sxx = parseInt(s.substring(i, i + 2), 16); r += String.fromCharCode(sxx); }
- return r;
- }
这时就要想到, 中文汉子对照表:
汉字对照表
解决方案
首先把汉子编码对照表存入以存入数据库(mongodb)
获取, 并以 key=gbk16 进制编码 value = 汉子的形式存下来
- var dicUniCodeCN=new Array();
- DBTool.FindData('mongodb:// 数据库地址 / 数据库名','unicodeCN',{},function(Docs){
- if (Docs.length>0) {
- for (var i = 0; i < Docs.length; i++) {
- dicUniCodeCN[Docs[i].gbk16.toString().toUpperCase()]=Docs[i].CN;
- };
- }
- });
3. 特性: gb2312 的高位字节如果大于 127(ASCII), 就为中文, 只有 gb2312 具有这个特性
- var simpleCNStr="";
- for (var j = 0; j < hexData.length; j += 2){
- // 高位字节 > 127 为中文
- var strHex=hexData.substring(j,j+2);
- console.log(parseInt("0x"+strHex,16));
- if (parseInt("0x"+strHex,16)>127) {
- strHex=hexData.substring(j,j+4);
- j+=2;
- simpleCNStr+=dicUniCodeCN[strHex];
- }else{
- simpleCNStr+=String.fromCharCode(parseInt(strHex,16));
- }
- }
4. 如果想兼容 utf-8 和 unicode 和 gbk, 那么可以 4 位 16 进制的字符截取, 如果大于 127, 那么默认为中文, 否则就是英文或字符或数字
- var simpleCNStr="";
- for (var j = 0; j < hexData.length; j += 4){
- //4 位截取, 大于 127 的为中文
- var strHex=hexData.substring(j,j+4);
- console.log(parseInt("0x"+strHex,16));
- if (parseInt("0x"+strHex,16)>127) {
- // 不想写了
- }else{
- // 待续 你们写吧...
- }
- }
题外话 - 关于 parseInt(string, radix)
- parseInt("10"); // 返回 10
- parseInt("19",10); // 返回 19 (10+9)
- parseInt("11",2); // 返回 3 (2+1)
- parseInt("17",8); // 返回 15 (8+7)
- parseInt("1f",16); // 返回 31 (16+15)
- parseInt("010"); // 未定: 返回 10 或 8
这个函数是把数字或进制字符都转为 10 进制的数字, 第二个参数 radix 表示的是第一个参数 string 的类型(10 进制, 2 进制, 8 进制, 16 进制), 我之前很白菜的理解为我想把第一个参数 string 转化成 16 进制哎, 我还是太年轻啊
来源: http://www.jianshu.com/p/91e53946c75b