转载:https://zhuanlan.zhihu.com/p/73917931

开始

先考虑下边的问题。

let s = "js"
console.log(s.length)
s= "亮"
console.log(s.length)
s = " "
console.log(s.length)

我们知道 length 就是字符串的字符数,所以输出的依次是 2,1,1,对吗?

探索一

我们知道,计算机里只能存 0 和 1,换言之,只能存数字,而我们现在在屏幕上看到的文字只是将数字对应到图形而已。

早期的 ASCII 码就是典型的例子,如下图,为了书写方便我在数字前边加了 0x 代表是 16 进制。

我们用 106 代表 ' j ',115 代表 ' s '。然后如果用 ASCII 码表示 "js" 的话,其实就是 0110101001110011 ,然后每 8 位也就是一个字节组成一个数字,根据对应关系电脑把本来的数字转换成了字符 "js" 展示到了我们面前。

有一个缺点就是 ASCII 码是 8 位,那么只能表示  个数字,也就是 256 个数字,这对于英文字母已经足够了。但是对于汉字的话,还远远不够。

探索二

所以我们加 1 个字节,用两个字节的数字去对应汉字,  $也就是 65536,肯定足够了。

当然,每个国家都会这样想,然后都制定了自己的语言相应的对应规则,这当然不方便大家在互联网上互通有无,如果本机不知道对应国家的编码对应关系,从而会造成乱码。所以后来有了 Unicode。

我们用 0x000000 - 0x10FFFF 这么多的数字去对应全世界所有的语言、公式、符号。然后把这些数字分成 17 部分,把常用的放到 0x0000 - 0xFFFF,也就是 2 个字节,叫做基本平面 (BMP)。从 0x010000 - 0x10FFFF 再划分为其他平面。

和 ASCII 码一样,我们可以把每个符号对应于一个数字,这个数字我们也把它叫做码点值

有了对应关系,我们可以像 ASCII 码那样去存了。当然这里的话因为每个字符都对应 24 比特位的数字,所以我们就用 3 个字节去存它吧。但是考虑到 CPU 的寄存器都是 8 位,16 位,32 位。。。翻倍来的,所以即使用 24 位,最终还得转到 32 位,所以我们直接用 32 位吧。

是的,这就是传说中的 UTF - 32 编码,简单明了,码点值是多少,内存中就存多少。

探索三

UTF - 32 缺点很明显了,字母 A 原本只需要 1 个字节去存储,而现在却用了 4 个字节去存,大部分位置都是 0。

我们为什么要多存那么多零呢?能不能 A 只存 0x41只存 0x4eae。如果 A亮这个字符串放到内存中就是 0x414eae。问题来了,计算机怎么知道,几个字节代表一个字符呢?是 0x41呢?还是 0x414e 呢?还是 0x414eae

于是,就有了 UTF - 8,将码点值进行一定的转换再去存储。

把阮一峰老师的讲解搬过来。

根据上表,解读 UTF-8 编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
下面,还是以汉字为例,演示如何实现 UTF-8 编码。
的 Unicode 是4E25100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800 - 0000 FFFF),因此的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx。然后,从的最右边二进制位开始,依次从右往左填入上边格式中的x,多出的位补0。这样就得到了,的 UTF-8 编码是11100100 10111000 10100101,转换成十六进制就是E4 B8 A5

让我们再看下「亮」,码点值是0x4eae,二进制就是 100111010101110,同样符合第三行,即格式是1110xxxx 10xxxxxx 10xxxxxx。从的最右边二进制位开始,依次从右往左填入上边格式中的x,多出的位补0。这样就得到了,的 UTF-8 编码是 1110(0100) 10(111010) 10(101110),16 进制就是 e4 ba ae

所以现在的对应关系变成了下边的样子。

和 UTF - 32 不同之处在于,我们不再用 4 个字节存储码点值,而是通过规则转换后再存储,这样的好处就是之前的A的话就只需 1 个字节就够了,而其他的可能是 2 个或 3 个,4 个字节,所以 UTF - 8 也叫变字长编码。

由于 UTF - 8 的变字长,而对于大部分常用字符都是 1 或 2 个字节,所以对于 html、邮件的传输多用 UTF-8 进行编码后传输。

探索四

UTF - 8 有什么缺点吗?

对于一个字符串abc天气不错,如果我们知道它的总共大小是 19 字节,但是我们很难算出它有多少个字符。因为有的字符是 1 个字节,有的是 2 个字节,有的是 3 个。所以为了知道字符数,我们还需要遍历一遍所有字节,从而确定有多少个字符。此外如果我们想取第 3 个字符,我们还是得从第 0 个字节开始遍历,因为我们不知道每个字符有多少字节。

如果每个字符都用固定长度编码就好了,这不又回到 UTF - 32 了吗?不不不,我们折中一下。

对于 Unicode 字符集,基本平面是我们常用的一些字符,用两个字节就可以编码。所以对于字的话,码点值是0x4eae,那么我们内部就用 0x4eae 去存。而 ASCII 码只需要一个字节,那么我们把通过高位补零扩充至两个字节去存。例如A的码点值是 65,16 进制对应 0x41,用 U+41 表示。那么内部的话就用 0x0041 去存。

那么基本面以外的字符呢?比如 这个字就属于基本面以外,它的 Unicode 码点值是 178178,也就是 0x2b802 ,显然用两个字节是存不下的,那怎么办呢?

用四个字节存呗,像 UTF - 32 那样直接存码点值,然后高位补零吗?显然不行了,因为第一平面我们是用的两个字节,如果第一平面外的直接用四个字节去存码点值的话,可能会导致前两个字节和基本面的两个字节重复,导致我们无法区分当前字符是两个字节还是四个字节。

UTF - 8 中,我们根据二进制开头的 1 的个数来表示当前字符是几个字节。这里的话,幸运的是在第一平面 U+D800..U+DFFF 的值不对应于任何字符。所以我们可以根据一些算法,把码点值转换为 4 个字节,前两个字节就用 U+D800..U+DFFF 中的值,这样如果前两个字节是 U+D800..U+DFFF 范围内的数,那就意味着该字符是 4 个字节编码的。否则就是两个字节。

这就是 UTF - 16 的编码方式了(具体的算法大家可以网上找一下),相对于 UTF - 8 的优势就是固定字节数,大部分字符都是两个字节。所以如果对于一个字符串abc天气不错如果采用 UTF - 16 编码,我们知道了它的总大小是 14 字节,那么字符数就很好知道了,它的大小除以 2 就是它的字符数了。而取第 4 个字符,如果知道了字符串开头的地址,也只需要加 2 * 4 就可以了(下标从 0 开始)。对于字符串的切割合并也都很好操作了。

所以对于一些语言 java,javascript 里的字符串也都用了 UTF - 16 编码。所以回到最开始的问题。

let s = "js"
console.log(s.length)
s= "亮"
console.log(s.length)
s = " "
console.log(s.length)

那么就取决于这些字符是不是在第一平面内了,如果是的话,那么结果就会就是 2 1 1。遗憾的时 " " 并不在基本平面,所以它内部是用四个字节编码,而 js 为了方便简单,它简单粗暴的认为两个字节就是一个字符,所以输出的就是 2 了。

此外关于,Unicdoe 所有的字符的码点值可以在 这个 网站找到。

实验验证

接下来说一下文件的存储。

我们打开一个 .txt,看到很多文字、符号,而内部其实也是用 0、1 存储的。既然要存储,就需要把 Unicode 的码点值进行编码。

如果是 UTF - 8 编码,那么一个码点值会生成 1 个或多个字节,然后把这些字节按顺序存就可以了。

如果是 UTF - 16 编码呢?

我们知道一个 Unicode 的码点值会对应一个数字,对于基本平面的字符,我们直接把这个数字存到内存中。那么问题来了,我们知道的码点值是 20142,换成 16 进制就是 0x4eae,内存中是按字节进行编址的。所以我们是先存4e呢?还是ae?先存4e吧,这样就符合我们人类阅读顺序,先读4e,所以先存4e呗。所以在内存中就是下边的样子。

内存地址       内存值
0x00000000    01001110 (4e)
0x00000001    10101110 (ae)

那么问题又来了,计算机处理的话先读取的是低地址,也就是4e,而4e对应数字0x4eae的高位(如果是 10 进制,个十百千,千就叫做高位)。有时候我们希望从低位读(也就是十进制中的个位)数字,所以我们希望这样去存。

内存地址       内存值
0x00000000    01001110 (ae)
0x00000001    10101110 (4e)

这就是多个字节存储的时候的字节序问题,把数字的高位存到低地址,低位存到高地址,叫做大端序(big endian),存储顺序符合我们人类习惯。反之就叫小端序(little endian)。

如果把字存到一个 .txt 中。

如果用 UTF-8 编码,那么前边算过的,就是e4 ba ae

如果用 UTF-16 编码,大端序的话就是4eae

如果用 UTF-16 编码,小端序的话就是ae4e

我们可以验证一下,可以用 notepad++,安装一个 HEXEditor 插件即可。或者其他的可以查看内部编码的也行。

写一个到 text.txt

以 UTF - 8 编码。

如果用 UTF - 16 编码,大端序

如果用 UTF - 16 编码,小端序

可以看到 UTF - 16 编码的时候,除了本身的字节,最开头还多了两个字节,fffe。原因很直接了,就是为了区分大端序和小端序。feff代表大端序,fffe代表小端序。

fefffffe也叫做 BOM,它可以区分不同编码。我们也听过 UTF - 8 无 BOM 或者 UTF - 8 BOM。UTF - 8 的 BOM 是 EF BB BF,windows 记事本编写的 .txt ,如果以 UTF - 8 编码保存,它默认就是有 BOM 的,所以如果看他的内存存储就是下边的样子。

而 UTF - 8 并不存在字节序的问题,因为它的最小编码单位就是字节,而 UTF - 16 编码最小单位是两个字节,所以有字节序的问题,从而加了 BOM 来区分是大端序还是小端序。但是 UTF - 8 并不需要区分大端序还是小端序,所以可以不需要 BOM。如果加了 BOM,对于一些读取操作,它可能会把读取到的 BOM 认为是字符,从而造成一些错误。所以我们保存 UTF - 8 编码的文件时,最好选择无 BOM。

我们也可以在浏览器的控制台上直接验证,因为 js 允许我们直接给字符串赋 Unicode 的码点值。格式是 \u 加上 16 进制的码点值即可。对于超过 2 个字节的码点值,用大括号括起来。

我们所熟知的 emoji 表情其实在 Unicode 字符集上也有对应的码点值。

比如最常用的笑哭脸的码点值是 U+1F602,当然 Unicode 只规定了码点值,并没有规定怎么实现,不同平台对于笑哭的表情展现也是不一样的。

同样我们也可以在浏览器上进行验证。

更多好玩

知道了上边的编码原则,我们就可以做些有趣的事情了,还记得「神奇字体」小程序吗?可以生成不同样式的字体,在微信、知乎发送。

其实上边的每一个字母并不是对应 ASCII 码值,而是对应基本平面外的 Unicode 码点值。所以我们如果输出上边的 I 字母," ".length,输出的就是 2,因为它是基本平面外的字符,用了 4 个字节编码。

大家可以回顾下,我之前写的探索过程,就会明白「神奇字体」的原理了。

「神奇字体」小程序的从零到一

此外,Unicode 还有一些组合字符、控制字符,实现不同字符的组合,比如删除线、下划线和字符的组合,实现字符的逆序输出等等,大家可以自己去探索下,蛮有意思的。

结束

作者:邱昊宇
链接:https://www.zhihu.com/question/23374078/answer/24385963

  • Unicode 是「字符集」
  • UTF-8 是「编码规则」

其中:

  • 字符集:为每一个「字符」分配一个唯一的 ID(学名为码位 / 码点 / Code Point)
  • 编码规则:将「码位」转换为字节序列的规则(编码/解码 可以理解为 加密/解密 的过程)

广义的 Unicode 是一个标准,定义了一个字符集以及一系列的编码规则,即 Unicode 字符集和 UTF-8、UTF-16、UTF-32 等等编码……

Unicode 字符集为每一个字符分配一个码位,例如「知」的码位是 30693,记作 U+77E5(30693 的十六进制为 0x77E5)。

UTF-8 顾名思义,是一套以 8 位为一个编码单位的可变长编码。会将一个码位编码为 1 到 4 个字节:

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

根据上表中的编码规则,之前的「知」字的码位 U+77E5 属于第三行的范围:

       7    7    E    5    0111 0111 1110 0101    二进制的 77E5
--------------------------0111   011111   100101 二进制的 77E5
1110XXXX 10XXXXXX 10XXXXXX 模版(上表第三行)
11100111 10011111 10100101 代入模版E   7    9   F    A   5

这就是将 U+77E5 按照 UTF-8 编码为字节序列 E79FA5 的过程。反之亦然。

题目

UTF-8 中的一个字符可能的长度为 1 到 4 字节,遵循以下的规则:

  • 对于 1 字节的字符,字节的第一位设为0,后面7位为这个符号的unicode码。
  • 对于 n 字节的字符 (n > 1),第一个字节的前 n 位都设为1,第 n+1 位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。

这是 UTF-8 编码的工作方式:

   Char. number range  |        UTF-8 octet sequence(hexadecimal)    |              (binary)--------------------+---------------------------------------------0000 0000-0000 007F | 0xxxxxxx0000 0080-0000 07FF | 110xxxxx 10xxxxxx0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

给定一个表示数据的整数数组,返回它是否为有效的 utf-8 编码。

注意:输入是整数数组。只有每个整数的最低 8 个有效位用来存储数据。这意味着每个整数只表示 1 字节的数据。

示例 1:data = [197, 130, 1], 表示 8 位的序列: 11000101 10000010 00000001.返回 true 。
这是有效的 utf-8 编码,为一个2字节字符,跟着一个1字节字符。class Solution {public boolean validUtf8(int[] data) {int totalByteCount = 0;for (int item : data) {if (totalByteCount == 0) {totalByteCount = totalByteCount(item);if (totalByteCount == -1) {return false;}totalByteCount--;continue;}//10xxxxxx检查if ((item & 0xC0) != 0x80) {return false;}totalByteCount--;}return totalByteCount == 0;}private int totalByteCount(int i) {if ((i & 0x80) == 0) {return 1;}if ((i & 0xE0) == 0xC0) {return 2;}if ((i & 0xF0) == 0xE0) {return 3;}if ((i & 0xF8) == 0xF0) {return 4;}return -1;}
}

追本溯源:字符串及编码相关推荐

  1. java 读取流的字符编码格式_如何使用Java代码获取文件、文件流或字符串的编码方式...

    标签: 今天通过网络资源研究了一下如何使用Java代码获取文件.文件流或字符串的编码方式,现将代码与大家分享: package com.ghj.packageoftool; import info.m ...

  2. 【廖雪峰Python学习笔记】字符串与编码

    字符串与编码 三种字符编码 ASCII编码 :计算机由美国人发明,最早只有127个字符编码-- 大小写英文字母.数字和符号 Unicode:把中文.日文.韩文等所有语言统一到一套编码中,2-4byte ...

  3. 如何判断一个字符串的编码类型?

    最近遇到这样一个问题: 预从一个txt文件中读取文本,但不清楚这个文件的编码方式,可能是ASCII的.UNICODE.UTF8等等,这样就造成对字符处理的不利啦.如何知道一个字符串的编码类型呢? 转载 ...

  4. php对字符串进行编码,PHP如何使用convert_uuencode()函数对字符串进行编码?

    convert_uuencode()函数是PHP中的一个内置函数,它使用uuencode算法对字符串进行编码.下面本篇文章就来给大家介绍一些convert_uuencode()函数的使用方法,希望对大 ...

  5. PHP json_decode 对 JSON 格式的字符串进行编码并获取对应的值

    关于PHP中对JSON 格式的字符串进行编码并解析,同时可使用正则来获取内容,看示例: 字符串: {"resp": {"userid": 0, "re ...

  6. Python字符串的编码与解码(encode与decode)

    首先要搞清楚,字符串在Python内部的表示是unicode编码,因此,在做编码转换时,通常需要以unicode作为中间编码,即先将其他编码的字符串解码(decode)成unicode,再从unico ...

  7. gb2312编码在线转换_python基础学习—04字符串与编码

    点击上方蓝字关注我们不迷路! 字符串与编码 一.了解计算机编码 1.1  编码 定义:将信息从一种形式转换为另外一种形式的过程叫做编码,即信息转换过程 举例:信息加密解密.语言翻译 1.2  计算机编 ...

  8. LeetCode 271. 字符串的编码与解码(4位16进制字符+字符串)

    文章目录 1. 题目 2. 解题 1. 题目 请你设计一个算法,可以将一个 字符串列表 编码成为一个 字符串. 这个编码后的字符串是可以通过网络进行高效传送的,并且可以在接收端被解码回原来的字符串列表 ...

  9. java 字符串指定编码输出_java对字符的编码处理

    在java应用软件中,会有多处涉及到字符集编码,有些地方需要进行正确的设置,有些地方需要进行一定程度的处理. 1. getBytes(charset) 这是java字符串处理的一个标准函数,其作用是将 ...

  10. python中对字符串进行编码_Python 中的字符串编码

    对Python字符编码一直没搞明白,今天看<Python参考手册>再次遇到这个问题,重新整理下 Python中字符串字面量用于指定一个字符序列,其定义方法是把文本放入单引号('),双引号( ...

最新文章

  1. 深度学习论文阅读路线图
  2. Android应用开发—TextView的动态创建
  3. LNK1169 找到一个或多个多重定义的符号
  4. 如何写好一份竞品运营分析报告?
  5. javaone_JavaOne 2012:JavaOne技术主题演讲
  6. Linux开发相关书籍
  7. linux使用rsync增量保存文件与无交互自动传输
  8. 微型计算机有缺点,PT开口安装微机消谐的优缺点?
  9. c语言条件语序心得,C语言之精华总结.doc
  10. Android 7.0动态权限大总结
  11. python调用darknet
  12. C++游戏开发入门项目精选:制作经典游戏拳皇97
  13. FAT32与NTFS区别
  14. [CC2642r1] ble5 stacks 蓝牙协议栈 介绍和理解 TI协议栈下载
  15. Linux下线程池源码实现
  16. 6.11 通过文件描述符来获取信号
  17. 精选优美英文短文1——Dear Basketball(亲爱的篮球)
  18. 打开excel 自动启动宏_Excel启动时自动打开不需要的文件
  19. Unity RayCast容易忽视的地方
  20. com.mysql.jdbc.Driver飘红,已解决

热门文章

  1. Linux(ubuntu)下切换root用户
  2. memsql 多节点部署
  3. spring boot 2.0之安全
  4. CodeVS 1031 质数环(DP)
  5. 使用oracle数据库和MySQL数据库时hibernate的映射文件.hbm.xml的不同
  6. 基于Raspbian(树莓派)搭建web安全练习环境(一)
  7. [LeetCode] Missing Ranges 缺失区间
  8. [Swust OJ 166]--方程的解数(hash法)
  9. JS 统计函数执行时间
  10. Linux 命令(52)—— ipcrm 命令