再说一说汉字的UTF8编码的问题 - 一个汉字UTF8编码占用多少个字节

  |   0 评论   |   1,371 浏览

虽然这个问题一直在接受新知识,同时不断更正自己对字符集的理解. 但是时间久了,难免自己的知识理解又模糊了. 在此把一些常用的知识点做一个简单的梳理.
首先回答一下如下问题:

  1. 汉字的GBK编码占用几个字节.
  2. 一个汉字的UTF-8编码占用几个字节.
  3. UTF-8与UNICODE或者UTF-16的关系是什么.
  4. 一个UNICODE编码怎么转换成UTF-8编码.
  5. Java的char内部到底存储的是什么.
    回答完如上几个问题基本日常的问题都解决了.

1 汉字的GBK编码占用几个字节.

答: 汉字的GBK编码占用两字节.

GBK的编码可以在这里查询: GBK 汉字内码扩展规范编码表(二)

image.png

从上表中的数据可以看出. 中字的GBK编码是: 0xD6D0,这个是我们自己做的编码. 可以看到GBK的编码的双字节中的每一个字节的16进制的每(高)4位都是大于等于0xA0 ,也就是: 1010 0000B, 这样做我想应该是为了和ASCII码能够较了的区分开且能同时混合编码使用(中文和英文混合).

2 一个汉字的UTF-8编码占用几个字节

答案: 一个常用汉字的UTF-8编码占用3个字节;

说明: 首先UTF-8的全称是:

UTF-8 is a variable-width character encoding used for electronic communication. Defined by the Unicode Standard, the name is derived from Unicode (or Universal Coded Character Set ) Transformation Format – 8-bit .^[1]^ -----wiki:

也就是说: 它是一种可变长度的编码. 是用于电子通信的. (可变长度大家都知道).
另外: UTF 的意思是 Unicode Transformation Format的意思. 然后UTF-8是按8bit长度的一个可变长的多 code unit编码方式.

我们先查一下汉字: 的unicode编码是多少. 查询地址

image.png

我们可以看到一个汉字unicode 是: \u4e2d(用十六进制表示就是 0x4e2d). 用二进制表示就是:

0100 1110  , 0010 1101
-------------  -------------

我们暂且把这个uinode的值,称为内码. 我们再来看一下 UTF-8的编码规则.

#1-byte characters have the following format: 0xxxxxxx  							: U+0000 -> U+007F
#2-byte characters have the following format: 110xxxxx 10xxxxxx					: U+0080 -> U+07FF
#3-byte characters have the following format: 1110xxxx 10xxxxxx 10xxxxxx			: U+0800 -> U+FFFF
#4-byte characters have the following format: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx	: U+10000 -> U+10FFFF

注意

  • 一字节的编码可以表示的范围是0x00 -> 0x7F 的字符. 刚好是一般的ascii码的区间. 这样如果编码中有ascii字符.那正好用一个字节就能表示.
  • 二字节编码: 高字节的前三位固定是 110 , 低字节的前两位固定是: 10, 这样有两个作用:
    • 1 可以方便的区分一个字节的内容是一个编码的开头,还是一个编码的中间或者结束字节.如果是0开头,一定是单字节的编码. 如果是10开头,那是非第一字节. 相对的, 110 开头的是一个双编码单元的的字符. (也就是这里的双字节UTF-8字符),1110开头的是三字节编码的字符.
    • 一个code point的编码被映射到 高字节的5位,和低字节的6位.总共是11位编码. 注意这个编码的起始编码是表示的是: U+0080 , 展示说: 0x0080 这个code point被编码到: 110 0 0010, 10 00 0000, 结束编码的空间为: U+07FF

其原理如图所示:

当code point能够使用一个code unit进行编码的时候. 其编码方式如下:

image.png

当需要使用两个code point来表示一个编码的时候. 其混合方式如下:

image.png

注: 从上图中可以看到双字节,或者是双编码单元的UTF8的编码不是从这个格式的最小值开始的.
而是把这除了前导的固定bit后的其它bit位合到一起作为想编码的unicode的直接值.也就是说:

  • U+0080 编码出来的是0xC280 , 而不是0xC080
  • 用下面的java代码也可以验证这个编码规则.
    • U+0080 编码结果: 0xC280
    • U+7FF 编码结果: 0xDFCF , 实际就是把数据位全部置为1.
System.out.println("\\u0080: \u0080");
        byte[] utf8bytes = "\u07ff".getBytes("UTF-8");
        System.out.println("\\u07ff's UTF-8:" + toHexString(utf8bytes, 0, utf8bytes.length));

        utf8bytes = "\u0080".getBytes("UTF-8");
        System.out.println("\\u0080's UTF-8:" + toHexString(utf8bytes, 0, utf8bytes.length));
// 输出
\u07ff's UTF-8:0xDF 0xBF
\u0080's UTF-8:0xC2 0x80 , 即: 1100 0010 , 1000 0000

注意: 两字节编码中有5位是固定掩码, 然后又有7F的编码空间会被浪费掉. (即上面所说的 U+0080不从双字节最小可编码值开始造成的).

现在我们有了这些知识后,再来看一个汉字会占用多少个字节.的utf-8编码. 我们从上面的查表可知字的编码是: \u4e2d. 上面也计算了一个其二进制表示为: 0100 1110, 0010 1011 , 显示其已经超过了11bit可以表示的范围. 因此很明显,两字节的utf8编码是放不下一个汉字了.所以一个汉字至少需要三个字节才能表示. (实际可以看到两字节的UTF-8能表示的字符相当少, 也就相当于 3+8 bit,这样也就2048个字符,同时还有128个字符是一字节,实际只表示了 2048 - 128 = 1920 个字符. 因此这两字节可表示的字符实在太少, 那汉字的表需要三个字节也是在意料之中的了)

下面是汉字"中"的UTF-8编码计算过程.:

image.png

也可以用JAVA代码计算一下:

String chs = "中";

        System.out.println("UTF16编码: \t"+toHexString(chs.getBytes("utf16"), 0, chs.getBytes("utf16").length));
        System.out.println("UTF-8编码: \t"+toHexString(chs.getBytes("UTF-8"), 0, chs.getBytes("UTF-8").length));

输出:
UTF16编码: 	0xFE 0xFF 0x4E 0x2D
UTF-8编码: 	0xE4 0xB8 0xAD

3 UTF-8与UNICODE或者UTF-16的关系是什么.

首先说一个unicode, 先看一个FQA,这是从unicode的官网摘抄下来的. -> FAQ: Is Unicode a 16-bit encoding?

Q: Is Unicode a 16-bit encoding?

A: No. The first version of Unicode was a 16-bit encoding, from 1991 to 1995, but starting with Unicode 2.0 (July, 1996), it has not been a 16-bit encoding. The Unicode Standard encodes characters in the range U+0000..U+10FFFF, which amounts to a 21-bit code space. Depending on the encoding form you choose (UTF-8, UTF-16, or UTF-32), each character will then be represented either as a sequence of one to four 8-bit bytes, one or two 16-bit code units, or a single 32-bit code unit.
翻译一下: (直接有道翻译了)
不。Unicode的第一个版本是16位编码,从1991年到1995年,但是从Unicode 2.0(1996年7月)开始,它就不是16位编码了。Unicode标准对字符的编码范围是U+0000..U+10FFFF,相当于一个21位的代码空间。根据您选择的编码形式(UTF-8、UTF-16或UTF-32),每个字符将被表示为一个到四个8位字节的序列,一个或两个16位代码单元,或一个32位代码单元。

从上面的定义可以看出最开始的版本的UNICODE的确是双字节的. 这个仅限于第一版本. 时间是91年到95年.而从2.0版本开始,一个UNI

再看一下维基百科的定义:

Unicode , formally the Unicode Standard , is an information technology standard for the consistent encoding, representation, and handling of text "Character (computing)") expressed in most of the world's writing systems. The standard, which is maintained by the Unicode Consortium, defines 144,697 characters^[1]^ ^[2]^ covering 159 modern and historic scripts "Script (Unicode)"), as well as symbols, emoji, and non-visual control and formatting codes.

翻译: Unicode,正式的Unicode标准,是一种信息技术标准,用于对世界上大多数书写系统中表示的文本进行一致的编码、表示和处理。该标准由Unicode联盟维护,定义了144,697个字符[1][2],涵盖159个现代和历史文字,以及符号、表情符号、非视觉控制和格式代码。

看一下历史:

根据1980年以来使用施乐字符编码标准(XCCS)的经验,Unicode的起源可以追溯到1987年(35年前),当时施乐公司的Joe Becker、苹果公司的Lee Collins和Mark Davis开始研究创建通用字符集的可行性在Peter Fenwick和Dave Opstad的帮助下,[13]Joe Becker于1988年8月发表了一份关于“国际/多语言文本字符编码系统”的草案,暂称“unicode”。他解释说,“Unicode”这个名字意在暗示一种独特的、统一的、通用的编码方式”在这份名为Unicode 88的文档中,Becker概述了一个16位字符模型:[13]Unicode旨在解决对可行、可靠的世界文本编码的需求。Unicode可以大致描述为“宽体ascii”;它已经被扩展到16位元,包含了世界上所有现存语言的字符。在适当设计的设计中,每个字符16位已经足够了。他最初的16位设计是基于只有那些现代使用的脚本和字符需要编码的假设:[13]Unicode优先考虑的是确保未来的实用性,而不是保护过去的文物。Unicode最初的目标是在现代文本中出版的字符(例如1988年世界上印刷的所有报纸和杂志的联合),其数量无疑远低于214 = 16,384。除了这些现代使用的字符之外,所有其他字符都可能被定义为过时或稀有;比起挤进通用unicode的公共列表,这些是私人使用注册更好的候选者。

可以看出第一版本的UNICODE 的确是16bit的.同时在设计的时候主要是考虑现代的语言文字的记录与存储使用. 而没有更多的考虑比如古文字的编码.

后面的进程:

1989年初,Unicode工作组扩大到包括Metaphor的Ken Whistler和Mike Kernaghan, RLG的Karen Smith-Yoshimura和Joan Aliprand,以及Sun Microsystems的Glenn Wright。1990年,微软的Michel Suignard和Asmus Freytag以及NeXT的Rick McGowan加入了该工作组。到1990年年底,映射现有字符编码标准的大部分工作已经完成,Unicode最后审查草案也已编制完成。统一码联盟于1991年1月3日在加利福尼亚成立,1991年10月,统一码标准第一卷出版。1992年6月出版了第二卷,内容涉及汉代表意文字。1996年,在Unicode 2.0中实现了代理字符机制,因此Unicode不再局限于16位。这使得Unicode编码速度增加到超过100万个编码点,从而允许对许多历史文字(例如埃及象形文字)和数千个很少使用或废弃的字符进行编码。在最初不是为Unicode设计的字符中,很少使用的汉字或汉字,其中许多是人名和地名的一部分,这使得它们很少被使用,但比Unicode.[16]的原始架构中设想的要重要得多1992年起的Microsoft TrueType规范1.0版本在命名表中使用了“Apple Unicode”而不是“Unicode”作为平台ID的名称。

划重点:

  • 2.0扩展了字符数到百万级别.
  • 2.0使用了字符代理技术.

代码空间与代码点( Codespace and Code Points ):

Unicode标准定义了一种编码间隔,[59]是一组从0到0x10FFFF16的数值,[60]称为码位(CodePoint)[61],并表示为U+0000到U+10FFFF(“U+”[62]后面是十六进制的码位值,前导零到最少四个数字;例如,除号为U+00F7,埃及象形文字Hiero O4.png的除号为U+13254(不是U+013254)[63])。在这2^16 + 2^20个定义的码位中,用于在UTF-16编码代理项对的从U+D800到U+DFFF的码位由Unicode标准保留,不得用于编码有效字符,导致净总2^16−2^11 + 220 = 1112064分配代码点 ( Code Point )。

BMP ( 基本平面 )中的所有编码点( Code Point )都是作为UTF-16编码的单个编码单元访问的,并且可以用UTF-8编码为一个、两个或三个字节。平面1到16(补充平面)中的代码点在UTF-16中作为代理对进行访问,并在UTF-8中以四个字节进行编码。 在每个平面内,字符被分配到相关字符的命名块中。尽管块的大小是任意的,但它们总是16个代码点(Code Point )的倍数,通常是128个代码点( Code Point )的倍数。给定脚本所需的字符可能分布在几个不同的块上。

关系概述

  1. UNICODE 最开始16BIT的编码. 后来扩展到了21BIT.
  2. UNICODE 使用码点(即 CodePoint) 来表示一个字符的编码值.
  3. unicode在存储,传输时可以选择具体的传输编码. 即UTF , 常见的有: UTF-8,UTF-16,UTF-32.
  4. UTF-16 的传输/存储编码形式,这种既代表存储传输又在一定程度上代表UNICODE本身的编码格式,是我们通常意义上所说的UNICODE.
    1. 而在windows平台所用的编码实际就是已经过时的UCS-2(有争议, 这也是windows平台的程序员最后对这些概念模棱两可的原因吧. 参考:
      1. Windows OS,VC使用的Unicode编码分别是什么?UTF-16与UCS-2的区别在哪里?,
      2. VC中的Unicode编码方式究竟是UTF-16 or UCS-2?,
      3. 确定Windows XP到底是UCS-2的还是UTF-16的.
      4. Linux 和 Windows 平台不同的 UCS-2 编码
    2. Unicode可以通过不同的字符编码来实现。Unicode标准定义了Unicode转换格式(UTF): UTF-8、UTF-16和UTF-32,以及其他几种编码。最常用的编码是UTF-8、UTF-16和过时的UCS-2 (UTF-16的前身,不完全支持Unicode);GB18030虽然不是官方的统一码标准,但在中国是标准化的,完全实现了统一码。

UTF-16编码

WIKI: https://en.wikipedia.org/wiki/UTF-16

  • UTF-16(16位Unicode转换格式)是一种字符编码,能够对Unicode的所有1,112,064个有效字符编码点进行编码(事实上,这个编码点的数量是由UTF-16的设计决定的)。编码是可变长度的,因为编码点用一个或两个16位编码单元进行编码。UTF-16源于一种早期过时的固定宽度16位编码,现在称为UCS-2(2字节通用字符集),因为很明显需要超过2^16(65,536)个编码点
  • UTF-16被Microsoft Windows API、Java编程语言和JavaScript/ECMAScript等系统使用。它有时也用于Microsoft Windows上的纯文本和文字处理数据文件。它很少用于类unix系统上的文件。自2019年5月以来,微软已经开始支持UTF-8(以及UTF-16)并鼓励使用.
  • UTF-16是唯一与ASCII码不兼容的web编码,在web上从未流行过,只有不到0.002%(千分之一多一点)的网页使用它相比之下,UTF-8占所有网页的98%Web超文本应用技术工作组(WHATWG)认为UTF-8是“所有[文本]的强制编码”,出于安全原因,浏览器应用程序不应该使用UTF-16
  • 它被SMS所使用(即可变长度UTF-16需要支持所有的表情符号字符,SMS标准指定了它的前一个固定宽度UCS-2,它不支持大多数)。[引文需要]

历史:

在20世纪80年代后期,为“通用字符集”(UCS)开发统一编码的工作开始了,它将用一个协调的系统取代早期特定于语言的编码。其目标是包括世界上大多数语言的所有必需字符,以及科学、数学和音乐等技术领域的符号。最初的想法是将需要每个字符1字节的典型256字符编码替换为使用65,536(216)个值的编码,这将需要每个字符2字节(16位)。

有两个小组并行进行这项工作:ISO/IEC JTC 1/SC 2和Unicode联盟,后者主要代表计算设备制造商。这两组尝试同步他们的字符分配,以便开发的编码可以相互兼容。早期的2字节编码最初被称为“Unicode”,但现在被称为“UCS-2”

当人们越来越清楚2^16个字符不够用时,[1]IEEE引入了更大的31位空间和每个字符需要4个字节的编码(UCS-4)。这遭到了Unicode联盟的抵制,一方面是因为每个字符4个字节浪费了大量内存和磁盘空间,另一方面是因为一些制造商已经在每个字符2个字节的技术上投入了大量资金。UTF-16编码方案是作为一种折衷方案而发展起来的,并于1996年7月与Unicode标准的2.0版本一起引入它在2000年由IETF发布的rfc2781中有完整的规定.

曾经我也有这个疑问. 中文这么多汉字.unicdoe (现在叫ucs-2)就怎么在2字节中存储下了所有的编码的呢. 但是由于对汉字的研究较少. 找来找去又很难找到不在这个编码字符集中的汉字. 这个概念就一直这样不清不楚到现在.

工作原理

在UTF-16编码中,小于2^16的编码点使用一个16位编码单元编码,该编码单元等于编码点的数值,就像在旧的UCS-2中一样。大于或等于2^16的较新的代码点由使用两个16位代码单元的复合值编码。这两个16位代码单元是从UTF-16代理项范围0xD800-0xDFFF中选择的,该范围以前没有分配给字符。这个范围内的值不用作字符,并且UTF-16没有提供将它们编码为单个代码点的合法方法。因此,UTF-16流由单个16位码点组成.

UTF-16在国际标准ISO/ iec10646及Unicode标准的最新版本中均有指定。“UCS-2现在应该被认为过时了。它不再指10646或Unicode标准的编码形式。"[11] UTF-16将永远不会被扩展以支持更大数量的码位或支持被代理取代的码位,因为这将违反Unicode稳定策略关于一般类别或代理码位的规定(任何保持自同步代码的方案都需要分配至少一个BMP代码点.

每个Unicode码位被编码为一个或两个16位码单元。这些16位代码如何以字节形式存储取决于文本文件或通信协议的“字节顺序”。 一个“字符”可能需要从最少两个字节到14个[13]字节甚至更多的字节来记录。例如,一个表情符号标志字符需要8个字节,因为它是“由一对Unicode标量值构造的”14

编码规则
U+0000 to U+D7FF and U+E000 to U+FFFF

U+D800 to U+DFFF have a special purpose, see below.

UTF-16和UCS-2都将这个范围内的码位编码为单个16位码单元,在数字上与相应的码位相等。基本多语言平面(BMP)中的这些代码点是唯一可以在UCS-2中表示的代码点。在Unicode 9.0版本中,一些现代的非拉丁亚洲、中东和非洲文字以及大多数表情符号都不在这个范围内。

Code points from U+010000 to U+10FFFF

来自其他平面(称为补充平面)的代码点(codePoint)被编码为两个称为代理对的16位代码单元,采用以下方案:

  • 从编码点(U)中减去0x10000,在十六进制数范围0x00000-0xFFFFF中留下一个20位的数字(U')。注意,对于这些目的,U被定义为不大于0x10FFFF (注: 这是最后一个平面,最后一个编码点)。
  • 高10位(在0x000-0x3FF范围内)被添加到0xD800以给出第一个16位代码单元或高代理(W1),它将在0xD800 - 0xdbff范围内。
    • 注: 由于10bit = 2bit+8bit. 因此, 后8bit直接写到低字节的8位. 高两位的值最大为3, 3+8=11 (十六进制: 0xB) , 因此最大为 0xDBFF;
  • 低10位(也在0x000-0x3FF范围内)被添加到0xDC00以提供第二个16位代码单元或低代理(W2),它将在0xDC00 - 0xdfff范围内。

上面的编码的巧妙之处在于:

  • 基本平面的 U+D800U+DFF 此部分正好是一个保留字符集. 此部分在unicode旧版本中没有来得及用 (即UCS-2中).正好用来代理扩展平面的码点 (CodePoint)
  • 扩展后的代理编码是两个10BIT 的双字节编码单元( Code Unit ).
  • 代理编码的值, 分别是W1,W2. 巧的是, W1与W2的值刚好不重合.同时又与基本平面的所有有用的非代理编码的值不同.这样我们在拿到一个code point的时候, 可以非常方便的识别这是一个代理编码单元,还是基本编码单元. 如果是代理编码单元,也又可以非常方便的区分当前的双字节是高编码单元,还是低编码单元.
U+D800 to U+DFFF之间的编码怎么办

Unicode标准为高代理和低代理的UTF-16编码永久保留这些代码点值,并且永远不会为它们分配字符,因此应该没有理由对它们进行编码。官方Unicode标准表示,没有UTF格式,包括UTF-16,可以对这些编码点进行编码。[引文需要]

然而,UCS-2、UTF-8和UTF-32可以用简单而明显的方式对这些编码点进行编码,而且大量的软件也这样做了,[需要引用],即使标准规定这样的安排应该被视为编码错误

通过使用与代码点(code point )相等的代码单元,可以以UTF-16格式明确地对未配对代理项(高代理项代码点后面没有低代理项代码点,或者低代理项代码点前面没有高代理项代码点)进行编码。结果是无效的UTF-16,但大多数UTF-16编码器和解码器实现在编码之间进行转换时都是这样做的。Windows允许在文件名和其他地方不配对的代理,[引用需要],这通常意味着它们必须得到软件的支持,尽管它们被Unicode标准排除在外.

注: 上面一段话的意思是, 虽然这种在代理码点间编码在使用中是禁止的(认为是错误) , 但是所有的编码与解码软件几乎都对此错误编码进行了兼容. 只要不是成对出现的代码点. 就"翻译"成一个UCS-2 的兼容字符.

4 一个UNICODE编码怎么转换成UTF-8编码.

这个问题已经在上面进行了说明, 不再单独阐述.

5. Java的char内部到底存储的是什么.

答: UTF-16,可以看这一个说明: orlacle链接%20and%20sequences%20of%20bytes.)

image.png
原文如下

image.png

参考文献

  1. Unicode FAQ
  2. WIKI - UTF16
  3. WIKI - UNICODE
  4. WIKI - UTF-8
  5. Windows OS,VC使用的Unicode编码分别是什么?UTF-16与UCS-2的区别在哪里?
  6. VC中的Unicode编码方式究竟是UTF-16 or UCS-2?
  7. 确定Windows XP到底是UCS-2的还是UTF-16的
  8. Linux 和 Windows 平台不同的 UCS-2 编码
  9. [GBK 汉字内码扩展规范]编码表(二)
  10. Unicode、UCS、BMP、UTF-8、UTF-16、UTF-32 (站在UCS编码标准的角度的一个阐述,权当是理解的补充吧. unicode和UTF16才是正统,UCS-2按里已经进入了历史)
  11. 彻底弄懂Java中的Unicode和UTF-8编码
  12. GBK编码查询

评论

发表评论


取消