为什么UTF8编码的文件,开头都是EF
2024/11/1 来源:本站原创 浏览次数:次北京看白癜风哪个医院专业 http://www.znlvye.com/m/
UTF-8编码早已成为了当今互联网最为重要的编码之一,几乎在所有正儿八经的网页中,都会看到这么一段:
charset="UTF-8"
如果要讨论UTF-8,我们就需要先聊一聊与它息息相关的的Unicode。
UnicodeUnicode定义了一个字——当然绝对不仅是汉字,而是全世界任何语言中的某个字或字母——所对应的十六进制数是多少。比如"中"字,对应的数字是4E2D,在Unicode中,用U+4E2D表示,其中"U+"用来说明这串数字是Unicode编码。而4E2D这串(十六进制)数字则有其对应的术语——在Unicode中,称之为码点(codepoint)。
UTF-8UTF-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这一经典编码有了较为深入的理解了。希望对你有所帮助,记得点赞,