以前碰到过文件编码问题,但都没太在意。最近在win7下写一个C++程序时,转移到linux下表现怪异,调试个半天发现竟然是文件编码问题!于是想花点时间好好总结一下关于文件编码格式的基本概念。这东西长时间没搞就容易忘,这也方便以后再来查询。
首先,关于字符编码的基本概念可以参考百度百科:http://baike.baidu.com/view/1204863.htm?fr=aladdin,当然我不清楚有多少正确性,不过我还是稍微总结了一些,最后再添加一个我自己的示例,若有误请见谅!
ASCII
ASCII码于1961年提出,用于在不同计算机硬件和软件系统中实现数据传输标准化,在大多数的小型机和全部的个人计算机都使用此码。ASCII码划分为两个集合:128个字符的标准ASCII码和附加的128个字符的扩展ASCII码。标准ASCII码为7位,扩展为8位。扩展的8位码官方标准叫:ISO-8859-1,也叫Latin-1编码
ANSI(MBCS)
为了扩充ASCII编码,以用于显示本国的语言,不同的国家和地区制定了不同的标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准。这些使用2个字节来代表一个字符的各种汉字延伸编码方式,称为ANSI编码,又称为"MBCS(Multi-Bytes Character Set,多字节字符集)"。在简体中文系统下,ANSI编码代表GB2312编码,在日文操作系统下,ANSI 编码代表 JIS 编码,所以在中文 windows下要转码成gb2312,gbk只需要把文本保存为ANSI编码即可。不同ANSI编码之间互不兼容,导致了unicode码的诞生。
我国的编码
GB2312,1980年颁布,有区码、位码、存储码之分
GBK,兼容GB2312,添加了繁体字
GB18030,增加了更多字符
BIG5,台湾、香港地区使用,也叫大五码
UNICODE
统一了全世界的所有字符,可分为UCS-2(对应UTF-16)和UCS-4(对应UTF-32),有大小端之分,如"A"的Unicode编码为6500,而BigEndian Unicode编码为0065,编码效率不高
UTF
为了提高UNICODE编码效率,就出现了UTF码,变长字节。UTF-8兼容ASCII,UTF-16不兼容ASCII。
UCS-2与UTF-8字节流之间的对应
UCS-2编码(16进制) UTF-8 字节流(二进制)
0000 - 007F 0xxxxxxx
0080 - 07FF 110xxxxx10xxxxxx
0800 - FFFF 1110xxxx10xxxxxx 10xxxxxx
例如“汉”字的Unicode编码是6C49。6C49在0800-FFFF之间,所以肯定要用3字节模板了:1110xxxx10xxxxxx 10xxxxxx。将6C49写成二进制是:0110 110001 001001,用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。可见UTF-8是变长的,将Unicode编码为0000-0007F的字符,用单个字节来表示; 0080-007FF的字符用两个字节表示;0800-FFFF的字符用3字节表示。因为目前为止Unicode-16规范没有指定FFFF以上的字符,所以UTF-8最多是使用3个字节来表示一个字符。
字节序
UTF-8以字节为编码单元,没有字节序的问题。UTF-16以两个字节为编码单元,在解释一个UTF-16文本前,首先要弄清楚每个编码单元的字节序。例如收到一个“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”?
Unicode规范中推荐的标记字节顺序的方法是BOM。BOM不是“Bill Of Material”的BOM表,而是Byte Order Mark。BOM是一个有点小聪明的想法:
在UCS编码中有一个叫做"ZEROWIDTH NO-BREAK SPACE"的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符"ZERO WIDTH NO-BREAK SPACE"。
这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符"ZEROWIDTH NO-BREAK SPACE"又被称作BOM。
UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8编码是EF BB BF。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了不同的系统对BOM的支持
因为一些系统或程序不支持BOM,因此带有BOM的Unicode文件有时会带来一些问题。
1、JDK1.5以及之前的Reader都不能处理带有BOM的UTF-8编码的文件,解析这种格式的xml文件时,会抛出异常:Content is not allowed in prolog。
2、Linux/UNIX 并没有使用 BOM,因为它会破坏现有的 ASCII 文件的语法约定。
3、不同的编辑工具对BOM的处理也各不相同。使用Windows自带的记事本将文件保存为UTF-8编码的时候,记事本会自动在文件开头插入BOM(虽然BOM对UTF-8来说并不是必须的)。而其它很多编辑器用不用BOM是可以选择的。UTF-8、UTF-16都是如此。
软件如何决定文本的字符集与编码
(1)对于Unicode文本最标准的途径是检测文本最开头的几个字节。如:
开头字节 Charset/encoding
EF BB BF UTF-8
FE FF UTF-16/UCS-2,big endian(UTF-16BE)
FF FE UTF-16/UCS-2,little endian(UTF-16LE)
FF FE 00 00 UTF-32/UCS-4,little endian.
00 00 FE FF UTF-32/UCS-4,big-endian
(2)弹出一个对话框来请示用户。
然而MBCS文本(ANSI)没有这些位于开头的字符集标记,现在很多软件保存文本为Unicode时,可以选择是否保存这些位于开头的字符集标记。因此,软件不应该依赖于这种途径。这时,软件可以采取一种比较安全的方式来决定字符集及其编码,那就是弹出一个对话框来请示用户。
(3)采取自己“猜”的方法。
如果软件不想麻烦用户,或者它不方便向用户请示,那它只能采取自己“猜”的方法,软件可以根据整个文本的特征来猜测它可能属于哪个charset,这就很可能不准了。使用记事本打开那个“联通”文件就属于这种情况。(把原本属于ANSI编码的文件当成UTF-8处理,详细说明见:http://blog.csdn.net/omohe/archive/2007/05/29/1630186.aspx)
win7平台测试示例
win7下创建一个文件tmp.txt,内容为:
a<CR>
汉
即一个ascii字符,一换行,再加一中文字符。
我们知道在中文windows系统中,默认ANSI即为GB系列的字符集,不妨将tmp.txt更名为tmp-ansi.txt。那怎么查呢?可用Emeditor打开它,在窗口右下角即可见所属字符集,如下图:
很明显是GB2312字符集。那怎样看具体的编码呢?有几种方式:
1、进入网站http://bianma.supfree.net/,输入汉字即可得,如下图:
2、单击Emeditor菜单View→Character Code Value,即可显示,同时还显示了unicode编码,如下图:
3、用ultraedit打开,快捷键ctrl+h,即可显示文件内容编码为:61 0D 0A BA BA,如下图:
同理,当我们将tmp-ansi.txt用记事本另存为其它格式时(当然用Emeditor另存为的可选格式更多),也可用上述方式得到验证其unicode编码为:6C49,UTF-8编码为:E6 B1 89,如下图验证tmp-utf-8.txt(由记事本生成,默认带有BOM):
即带有签名的BOM:EF BB BF; 'a':61; CRLF:0D 0A; '汉':E6 B1 89
再来看另存为unicode big endian的情况:
果然字节数多了!其BOM:FE FF; 接下来的每个字符为2个字节。当然若是小端的话,其BOM就是FF FE了
Linux平台测试示例
同理在Linux测试也差不多,只不过软件没了,用命令可看。创建文件tmp-utf-8,同样输入:a+换行+汉,执行命令:
od -x tmp-utf-8