为什么UTF8编码的文件,开头都是EF

2024/10/15 来源:本站原创 浏览次数:

刘军连的号怎么挂 https://disease.39.net/bjzkbdfyy/210905/9408135.html

UTF-8编码早已成为了当今互联网最为重要的编码之一,几乎在所有正儿八经的网页中,都会看到这么一段:

charset="UTF-8"

如果要讨论UTF-8,我们就需要先聊一聊与它息息相关的的Unicode。

Unicode

Unicode定义了一个字——当然绝对不仅是汉字,而是全世界任何语言中的某个字或字母——所对应的十六进制数是多少。比如"中"字,对应的数字是4E2D,在Unicode中,用U+4E2D表示,其中"U+"用来说明这串数字是Unicode编码。而4E2D这串(十六进制)数字则有其对应的术语——在Unicode中,称之为码点(codepoint)。

UTF-8

UTF-8则是码点以二进制的形式,保存成字节时,根据特定规则,以字节的方式,所呈现的16进制值。如果这个时候仔细思考一下,读者可能就会说了:你要不要听听看你在说些什么?把4E2D这个十六进制转换成计算机能理解的二进制进行保存,然后把保存在计算机中的二进制,再转换成十六进制,就得到了UTF-8编码。那我还专门搞一个UTF-8编码干嘛?不就是Unicode中定义的4E2D么?这个问题问非常好,它也就是这篇文章所要解释的重点。究其原因,不得不再重申一下Unicode的惊人优势:它把世界上所有的语言中的字或字母,都做了码点定义!甚至是emoji表情!

我国的“中华字库”工程曾经统计的汉字约有10万个。我们看到形如4E2D这样的码点,才4个16进制数,撑死16的4次方,最多只能放个字,连所有汉字都放不下,还要放全世界的字符?还要放emoji?而且码点的定义还在不断增加?这怎么能做得到呢?其实,并不是我在瞎说,也不是Unicode在说大话。而是我们常见的码点大多是这样4个16进制数,但并非码点只能是4个16进制数。比如"

"字,读作伊,它所对应的Unicode是U+C,码点总共5位16进制数。看到这里,显然,无论用哪种编码将所有的Unicode字符表示成二进制,两个字节都是远远不够的。本文的主角UTF-8实际上可以使用1到6个字节来表示1个Unicode字符。

不确定的字节个数给计算机带来的困扰

聪明的你一定开始为计算机感到发愁了。设想一台计算机正在读取一个文本文件。它怎样才能知道:“我要读取文件中的下一个字符了。等一下,UTF-8编码可以用1到6个字节来表示1个字符。我怎么才能知道这个字符用了几个字节?”UTF-8对于这个问题的解决,进行了经典的设计。

ASCII码

要完全搞懂UTF-8的编码,还需要了解一下ASCII码。简单来说,它是一个古老的字符编码,主要覆盖的字符是数字,大小写英文字母,运算符号等。总共个字符。这个字符,是标准的ASCII码,不带任何扩展,在全世界走到任何地方,都是统一的个字符,不包括任何扩展。我们知道,1个字节,对应8位,总共可以表示种不同字符。而ASCII码仅需要一半的容量即可。所以,在ASCII码对应的二进制表现形式中,所有字符的第一个位都是0。比如大写字母A对应的二进制是;小写字母z对应的是。第一个位永远都是0。Unicode中,沿用了这个规范,凡是属于ASCII码的字符,都是用1个字节表示。计算机读取字节的时候,只要读到第1位是0,那么它就知道这个字符只用1个字节表示。无需读取后面的字节,就可以将读取到的内容转换成字符了。1个字节的情况搞懂,剩下5种情况呢?

计数字节和数据字节

UTF-8编码中,有2种字节,起着不同的作用。它们分别是计数字节和数据字节。其中,计数字节就是专门用来告知计算机,一个字符用几个字节来表示的。

计数字节的特点是,以n个1加1个0作为字节的开头。其中,n表示用多少个字节来组成这个字符。因为每个bit只能是0和1,所以用0对计数的1进行收尾。而剩余的0和1作为数据的组成部分。数据字节则以10开头,剩下的内容都是数据的组成部分。

光这么说,还是有些抽象。不过在我们在举具体的例子之前,还需要再了解一个概念:字节顺序标记。

字节顺序标记:

上文提到:UTF-8可以用1至6个字节来表示一个字符。对于1个以上的多个字节,计算机在内存中存储时,不同的计算机存储的方式是不同的。不同的计算机根据自身的设计,为了更高效地存取数据,逐渐形成了2种字节顺序:大端和小端。

大端(Big-Endian):一个大端系统中,如果一个字符由多个字节构成,那么它会将最大的有效字节存放在最小的内存地址;将最小的有效字节存放在最大的内存地址。我们可以来看下图进行更好的理解:

图中一个32位整型0A0B0C0D由4个字节构成,因为1个字节等于8位嘛。那么在一个大端系统中,最靠前的,也就是最大的有效字节0A,会存放在最靠前的内存地址a中。最靠后的,也就是最小的有小字节0D,存放在最靠后的内存地址a+3中。也就是说,越靠前的字节,在内存中,也越靠前。

小端(Little-Endian):与之相反,在一个小端系统中,如果一个字符由多个字节构成的话,那么会将最大的有效字节存放在最大的内存地址中;将最小的有效字节,存放在最小的内存地址中。如图所示:

图中还是那个32位整型数0A0B0C0D。在小端系统中,最高位的0D存放在了最小的内存地址a中;最低位的0A存放在了最大的内存地址a+3中。

对于计算机读取内存数据时,是循着内存地址去一个一个读的。就0A0B0C0D来说,在大端系统中,计算机读到的就是0A0B0C0D;而在小端系统中,计算机读到的是0D0C0B0A。这就好比我们的文字,曾经有过一段时间,从右往左读;如今从左往右读一样。那么问题来了:计算机在接收到一个文件的时候,它怎么样才能知道生成这个文件的计算机,和自己的读取顺序是否一致呢?要知道,一旦顺序判断错误,整个文件的内容就完全变了。

聪明的从业者们,逐渐达成了一个共识:在文件的开头,统一存放一个特定的Unicode字符U+FEFF。如果计算机在读取文件时,最初读取的2个字节,如果是FEFF,那么表明文件与计算机的读取顺序相一致;如果是FFFE,则表示文件与计算机的读取顺序相反,程序在解释字符时,需要倒着进行。我们称U+FEFF为字节顺序标记(ByteOrderMark),简称BOM。

虽然判断顺序的方法解决了,不过又有了新的问题:U+FEFF本身也是Unicode,如果用户在文件的开头,并未使用大家所共识的文件头,而是将其作为了文件内容的一部分进行使用怎么办?会不会造成程序的误判?确实会这样。这也是Unicode的遗留问题。业内鼓励软件作者仅在文件头使用U+FEFF,避免在文件内容中使用。另外,在年的Unicode1.1版本中,将U+FEFF定义为:0宽无换行的空格。也就是说,如果将文件内容中的U+FEFF全部忽略,并不会对文件内容本身造成太大影响。

假设你有一个文本文件,里面的内容是Hello,那么在大端和小端系统中,呈现的字节如下:

小端:

大端:

以上2张截图展示了大端和小端的差异,但它们都是UCS-2编码下,底层呈现的状态。每个字母都拿16进制的码点直接存放在内存中,用不到的字节就直接补上00。这样的编码非常简单,易于理解,但问题是,对于计算机中普遍存在的英文字母,每个字母都会浪费1个字节的存储空间,同时对于仅兼容ASCII编码的文本处理文件来说,遇到空字节00,就会默认文件已经读到终点,并停止继续读取文件。这样一来,这种编码对于旧软件的兼容性是完全不具备的。那么在此基础之上,解决了空间浪费和软件兼容性的UTF-8编码,又是怎么做的呢?

UTF-8的例子

UTF-8会将能够放在ASCII编码原来的个字符都放在1个字节当中,结合之前提到的,这个字符因为只用来半个字节的空间,所以第一位都是0。这些单个字节的字符,老程序都认识,又因为都只放在1个字节当中,并不会产生00空字节,所以软件兼容性也得到了保证。那么ASCII字符以外的字符呢?

单字符多字节情况下,总排在第一个的字节是:计数字节

计数字节以开头的n个1加1个0来表示。因为仅用于多字节情况,所以开头至少是2个1。

比如:

2个字节:xxxxx

3个字节:1xxxx

4个字节:11xxx

以此类推,其中的x是具体构成某个字符的数据。

除了排在每个字符首位的计数字节以外,剩下的就都是数据字节了。

数据字节用10开头表示,比如:

10xxxxxx

把计数字节和数据字节拼在一起,如果是2个字节的话,就会是这样:

xxxxx10xxxxxx

有没有发现其中精妙的地方?如果是属于ASCII的前个字符,不会存在单个空字节,老文本处理程序都能认识和兼容;而其它字符又都保证了不管是计数字节还是数据字节,都会以1开头,无论数据是怎样的,都不会有全为0的空字节出现,这就保证老的应用软件不会只读到一半,就截断文件的所有内容。剩下的,只要交给软件开发者慢慢改善兼容性就行了。与此同时,UTF-8编码还节省了大量空字节的空间浪费。不得不说这个设计非常经典。

但是事物都有两面性。兼容性又好,又节省空间的UTF-8编码,在计算机读取时,计算量势必会有所增加。对于每个字符,它都需要确认每个字节的类型,获取字节顺序标记后,在以正确的顺序读取字节的基础上,还要将真正表示字符的位提取出来,构成码点,最终解释出哪几个字节表达的是哪个字符。

还记得字节顺序标记U+FEFF也是一个Unicode字符么?作为文件头,UTF-8也对这个文件头,做了编码,以二进制的形式进行存储。那么最后,我们就以U+FEFF为例,看看UTF-8对Unicode是怎么做编码的吧。

我们先来看一下U+FEFF分为2个字节,在二进制中的样子吧:

1

光是码点就已经分为2个字节了,那么转换成UTF-8的话,需要加上计数字节和数据字节的特征,所以在UTF-8中,一定会超过2个字节。那么就一定会有计数字节和数据字节。因为计数字节中的数据,如果位数不够,可以高位补0,数据内容保持不变。所以,我们先从低位开始,从右往左,一个一个地来填充UTF-8的字节。

数据字节1:

10xxxxxx,把U+FEFF的最右边的6个1填充进去,得到:

1011

转换成十六进制的话,得到:BF

U+FEFF还剩:

1

剩下的位数超出5个,所以再补上1个数据字节。

数据字节2:

10xxxxxx

把剩余的后6位填充进这个字节:

10111

转换位十六进制得到:BB

U+FEFF剩余:

1

最后,因为仅剩4位,正好可以放进表示3个字节的计数字节。

计数字节:

1xxxx

把U+FEFF剩下的4位填充进去,得到:

1

转换为十六进制,得到:EF

然后我们把整个转换成UTF-8的字节顺序标记整合起来,就得到了:

EFBBBF

这就是为什么,我们经常会在UTF-8文件头,看到EFBBBF的原因啦。

那么经过这篇文章,我们应该对UTF-8这一经典编码有了较为深入的理解了。希望对你有所帮助,记得点赞,

转载请注明:
http://www.wqopd.com/ynywh/13466.html
  • 上一篇文章:

  • 下一篇文章: 没有了
  • 网站首页 版权信息 发布优势 合作伙伴 隐私保护 服务条款 网站地图 网站简介
    医院地址: 健康热线:
    温馨提示:本站信息不能作为诊断和医疗依据
    版权所有 2014-2024
    今天是: