很久没有写blog了﹐今天下午工作刚好告一段落﹐有点时间﹐就把上周花了很多时间总结出来的一些计算机字符相关的心得写出来﹐希望能够帮助当初和我一样迷茫的人能够容易理解﹐也希望能够引出玉来(这么多废话﹐还不快开始...)
由于公司使用的是繁体操作系统﹐而我有时习惯在自己家里的简体计算机上写一些程序﹐但是当我用U盘把代码在两者之间copy时﹐经常发现文件中文的 地方成了乱码﹐所以就花了些时间到网上查了一下﹐发现有很多关于乱码问题的讨论﹐按自己的方法总结了一下(有不对的地方﹐还希望各位指出):
1.文件分为文本文件和二进制文件﹐不过本质都一样﹐都是些01。
2.计算机存储设备存储的0或1﹐称为计算机的一个二进制位(bit)。
3.二进制文件的0和1有专门的应用程序来读﹐所以它们没有什么乱不乱码的问题﹐只要该程序认得就行。(像doc,xls,exe,dll等)
4.文本文件就不一样了﹐notepad要认识它﹐vs.net要认识它,UE也要认识它...所以它们就要有一个标准。这个标准的原理其实很简单﹐就是把所有的字符都给它一个序号﹐然后根据这个序号来找字符就可以了。这个东东就是编码表,也叫字符集(charset)。
5.文本文件存的都是字符﹐如﹕A,?,@,x。很明显一个bit不能表示﹐刚好计算机的存储单位--字节(byte)就是多个字节(1个byte=8个bit),因此用byte来表示字符就理所当然了。
6.第一个编码表--ASCII码很快产生﹐很简单﹐就是用一个byte来表示一个字符(最高位置0),总共能存储128(2^8)个字符。如A用 65表示﹐存在计算机中就是01000001(65)﹐为了书写方便﹐我们一般记作0x41(16进制),97则表示小写的a,存在计算机中就是 01100001(97)﹐记作0x61。?用63表示,记作0x3F。
7.英语国家的大小写字母加起来才52个字符﹐再加上数字﹐符号和一些特殊字符﹐已经足够使用。所以ASCII刚开始非常流行(谁叫计算机不是咱中国发明的... )
8.随着计算机的普及﹐当非英语系的国家开始使用时﹐ASCII已经明显不能满足了(总不成天天使用xiao sheng来表示"小生"吧),所以这些国家(地区)就开始制订自己的标准。
9.中国大陆制订了简体汉字的字符集(GB2312)。和英语国家不同﹐我们的汉字远远不止128个﹐所以一个byte肯定不能表示完﹐那就多加个 byte,16位(65536)总可以了吧。不过这样虽解决了位数不够的问题﹐但是原来的英文文件怎么办?总不成又全部拿出来改成双字节吧。幸好﹐居然发 现原来的ASCII的第一位居然是0﹐那我们把第1位改成1不就OK了吗?以后凡看到0开头的就读1个字节﹐1开头的就读2个字节。(而且128*128 表示所有的简体字也足够了)
10.因此在GB2312标准中,"小"的序号是0xD0A1,表示成11010000 10100001,而A还是表示成01000001,这就是为什么简体操作系统读ASCII文件不会乱码﹐而反之则不然的原因。
11.目前来说﹐情况还比较好﹐中国大陆的计算机运行正常。
12.看到中国大陆制订了一个标准﹐其它国家和地区也不甘示弱﹐纷纷亮出自己的字符集,于是乎什么BIG5(中国台湾),shift_jis(日本),ks_c_5601-1987(韩国)都闪亮登场﹐一时间百鸟争鸣,百花齐放。
13.每个国家都想与ASCII保持兼容﹐理所当然﹐后面的字符就完全不一样了﹐因此﹐同样的0xD0A1,在GB2312中是"小"字﹐而在BIG5中却是"苤"字。你想想﹐这样不乱才怪。
14.到了这时候﹐总有人会想到﹐再这样继续下去是肯定不行的﹐于是它们就想到了﹐如果有一个标准﹐能包括所有字符那不就OK了吗?
15.于是"大哥大"标准就出来了﹐这就是unicode,为了能够足够表示世界上的所有字符这样光荣而又伟大的任务﹐这家伙用了四个字节来表示 (2的32次方到底是多少﹐我也懒得算了),这下好了﹐天下太平了﹐再也不会有麻烦了﹐耳根清静了...(打住﹐你小子这么这么罗嗦呀)
15.不过unicode好是好﹐但是毕竟四个字节表示一个字符"浪费"太大了(我那破猫上网容易吗﹐电信黑呀﹐说好是2M﹐就给我 200K...)﹐而且大家"惊奇"地发现﹐居然世界上一些"较强大"的国家的字符刚好集中在前65536位前﹐呵呵﹐结果unicode也分成了 unicode-16和unicode-32了﹐自然﹐前者只用两个字节表示(所以只能表示前65536位喽,欧亚国家大部分字符都OK了﹐什么﹐你们那 个@$Y$%字符没有﹐呵呵﹐不管我什么事,找标准协会﹐都是那帮家伙弄的...)
16.虽然标准出来了﹐可是好歹ASCII也用了这么久﹐那些英语国家也在那里嚷嚷﹐这倒好﹐搞个什么破标准﹐我们又没有得到什么好处﹐反而让我们 原来的程序都运行不了了(为什么呀﹐你想想﹐原来我们的程序字符都是一个字节一个字节认﹐现在倒好﹐全改成2个一起认﹐这还怎么跑呀?)﹐况且我们凭白无 故了用了这么多0﹐真别扭(unicode中的前128位还是ASCII标准﹐只不过在前面加了8个0)﹐由于那些国家"势力"比较大﹐所以这个问题不容 忽视
17.这个世界上的牛人总是这么多﹐这个问题很容易就被小意思地解决了。
18.想想GB2312怎么解决与ASCII兼容的问题的(1开头的就读2个字节﹐0开头的就读1个字节)﹐同样﹐UTF也这样﹐0开头的读1个字 节(ASCII码)﹐110开头的读2个字节﹐1110开头的读3个字节﹐这就是伟大的UTF-8(当然还有UTF-16,原理一样﹐xx开头的读4个字 节﹐xx开头的读5个字节﹐xx开头的读6个字节)
19.当然UTF-8没GB2312这么简单﹐读完之后不能直接查编码表﹐多加一个步骤﹐按照模板提取一下字符再查就OK了
以下就是UTF-8的模板
0x0000 - 0x007F用一个字节表示 0xxxxxxx
0x0080 - 0x07FF用两个字节表示 110xxxxx 10xxxxxx
0x0800 - 0xFFFF用三个字节表示 1110xxxx 10xxxxxx 10xxxxxx
举个例子吧,
如 果你遇到了11100110 10110001 10001001 01000001 这样的字节流﹐首先你看第一个字节以1110开头﹐即读3个字节并按模板提取得到 0110 110001 001001(去除模板标志﹐再四字节四字节读即0x6c49),查unicode编码表就是"汉"字,而最后一个以0开头就一定是一个字节了 ﹐0x0041,也就是"A"。
20.好了﹐上面是原理﹐再来谈谈简繁体操作系统转换时的乱码问题吧
21.按照我的想法﹐windows操作系统应该有一个默认的系统字符集﹐如简体操作系统应该是GB码﹐繁体操作系统则是BIG5,英文操作系统是ASCII。系统内的软件(notepad)默认都是使用这个字符集。
22.所以我在繁体操作系统默认存储的文本文件就是BIG5了﹐当这个文件到了简体系统里﹐它的notepad程序则使用自己的默认编码(GB)来读取﹐这样就乱了。
23.因此如果在保存时就使用utf-8来保存﹐应该在两系统切换时就不会有问题了。
24.而要解决这个问题其实也很简单﹐只要知道这个文本文件原来的编码就可以了﹐使用它读出来﹐再转成unicode即可。
上面的东东都是我用自己的理解来解释的﹐当然有些东西我避开了﹐主要是想让大家更容易理解原理﹐想要更正式的内容大家到网上随便一搜就出来了。
在证明那些东东之前﹐首先把.net中关于处理encoding,二进制,16进制,byte等相关类别和方法罗列一下。
1.byte与string(那些255以内的整数)的相互转换(各种进制之间的相互转换)
使用System.Convert类别
string to byte
Convert.ToByte(string,base)
base:2表示二进制,8表示八进制,10表示十进制,16表示十六进制(你要输入33,呵呵﹐异常)
这样可以把字符串的(0--255)转成一个byte
Convert.ToByte("01000001",2)转成 65
Convert.ToByte("255",10)转成255
Convert.ToByte("42",16)转成66
同理﹐byte to string也是Convert类
Convert.ToString(byte,base)
同样可以转成相应的进制表示的字符串
通过这两个方法﹐我们要进行2,8,10,16进制的相互转换就容易了
2.char,int,long,boolean等与byte[]之间的相互转换(这些数据在内存中的存储状况)
使用System.BitConverter类别
我们都知道char,int,long等基本类型是以字节形式存在内存中的﹐所以要查看其内存存储方式则直接使用BitConverter.GetBytes()就可以了
然后再使用BitConverter.ToString(byte[])就可以以string方式查看了(如:f9-03表示2个字节)
string是由char组成的﹐只要foreach(char in string)就可以看到string的存储方式了(实验表明﹐string在内存中是以unicode编码存在的,下有示例)
3.各种Encoding之间的转换
使用System.Text中的Encoding相关的类别就可以了
包括Encoding,ASCIIEncoding,UTF8Encoding等,当然也可以通过Encoding.GetEncoding()来获取不同的编码。
然后再通过GetBytes(string)方法﹐就可以获取string的不同编码的byte数组了
通过GetString(byte[])方法﹐就可以把某种编码的byte数组转成字符串.
如"I am 小生,hello world!"的各种bytes编码测试。
using System;
using System.Collections;
using System.Text;
public class MyClass
{
public static void Main()
{
string tmp = "I am 小生,hello world!";
WL("内存中存储的字节数组﹕");
foreach(char c in tmp)
{
byte[] b = BitConverter.GetBytes(c);
Console.Write(BitConverter.ToString(b) + "-");
}
WL("");
WL("unicode字节数组﹕");
byte[] bs1 = Encoding.Unicode.GetBytes(tmp);
WL(BitConverter.ToString(bs1));
WL("utf8字节数组﹕");
byte[] bs2 = Encoding.UTF8.GetBytes(tmp);
WL(BitConverter.ToString(bs2));
WL("default字节数组﹕");
byte[] bs3 = Encoding.Default.GetBytes(tmp);
WL(BitConverter.ToString(bs3));
WL("big5字节数组﹕");
byte[] bs4 = Encoding.GetEncoding(950).GetBytes(tmp);
WL(BitConverter.ToString(bs4));
RL();
}
private static void WL(string text, params object[] args)
{
Console.WriteLine(text, args);
}
private static void RL()
{
Console.ReadLine();
}
private static void Break()
{
System.Diagnostics.Debugger.Break();
}
}
在下面开始之前﹐先摘录一段关于BOM的知识
-----------------------------------------------------------------
UTF的字节序和BOM
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编码中有一个叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输 字符"ZERO WIDTH NO-BREAK SPACE"。
这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符"ZERO WIDTH NO-BREAK SPACE"又被称作BOM。
UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8编码是EF BB BF。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。
Windows就是使用BOM来标记文本文件的编码方式的。
----------------------------------------------------------
好了﹐这些问题解决后﹐我们就来做单纯的文本文件的编码识别﹐读取与写入测试吧。
以windows的notepad为例(其它的文本文件读取软件的原理应该也差不多﹐只是会多一些特殊的判断算法而已)。
notepad默认有四种编码来存储和读取文本文件。分别是﹕
ANSI,Unicode,Unicode-big-endian和UTF-8。
首先来讲ANSI吧﹐这个是windows操作系统在区域与语言块设置的编码(也就是系统默认的编码)﹐因此像繁体操作系统就是big5,而简体操作系统则是GBK。
而Unicode和UTF-8这两种格式相信大家已经有所了解(当然前者是unicode-16)
而Unicode-big-endian是什么意思呢﹐它与Unicode几乎一样﹐只是它把高位放在前面(而后者则刚好相反)
上面的摘录已经有所说明﹐这里再解释一下﹕
如同样是字符"A"﹐在以下几种格式中的存储形式分别是﹕
UTF-16 big-endian : 00 41
UTF-16 little-endian : 41 00
UTF-32 big-endian : 00 00 00 41
UTF-32 little-endian : 41 00 00 00
好了﹐大家想一想﹐文本文件在硬盘中是以字节形式存储的﹐如果不知道文本文件的编码﹐那是无论如何也不能正确读出文本文件显示给用户看的(乱码了只有人才知道﹐程序则认为一切正常)
根据BOM的规则﹐因此在一段字节流开始时﹐如果接收到以下字节﹐则分别表明了该文本文件的编码。
UTF-8: EF BB BF
UTF-16 : FF FE
UTF-16 big-endian: FE FF
UTF-32 little-endian: FF FE 00 00
UTF-32 big-endian: 00 00 FE FF
而如果不是以这个开头﹐那程序则会以ANSI,也就是系统默认编码读取。
所以现在我们来做个测试就可以很清楚地对以上的东东进行验证了。
1.用notepad输入"汉A"这2个字符﹐然后分别保存成ANSI,Unicode,Unicode-big-endian和UTF-8,名字分别取为ansi.txt,unicode.txt,unicode_b.txt,utf8.txt,并且放在c盘根目录下
2.用以下程序进行验证
using System;
using System.Collections;
using System.IO;
public class MyClass
{
private static void writefile(string path)
{
FileStream fs = null;
try{
fs = new FileStream(path,FileMode.Open);
byte[] bs = new byte[fs.Length];
fs.Read(bs,0,bs.Length);
WL(BitConverter.ToString(bs));
SixTTwo(BitConverter.ToString(bs));
}
catch(Exception ex)
{
WL(ex.ToString());
}
finally
{
if(fs!=null)
fs.Close();
}
}
public static void Main()
{
string path;
WL("ANSI文件格式的字节流﹕");
path = "c:\\ansi.txt";
writefile(path);
WL("Unicode文件格式的字节流﹕");
path = "c:\\unicode.txt";
writefile(path);
WL("Unicode-big-endian文件格式的字节流﹕");
path = "c:\\unicode_b.txt";
writefile(path);
WL("utf-8文件格式的字节流﹕");
path = "c:\\utf8.txt";
writefile(path);
RL();
}
public static void SixTTwo(string sixstr)
{
string[] tmp = sixstr.Split(new char[]{'-'});
foreach(string s in tmp)
{
Console.Write(Convert.ToString(Convert.ToByte(s,16),2).PadLeft(8,'0')+ "
");
}
WL("");
}
private static void WL(string text, params object[] args)
{
Console.WriteLine(text, args);
}
private static void RL()
{
Console.ReadLine();
}
private static void Break()
{
System.Diagnostics.Debugger.Break();
}
}
3.以下是输出格式﹕
ANSI文件格式的字节流﹕
BA-BA-41
10111010 10111010 01000001
Unicode文件格式的字节流﹕
FF-FE-49-6C-41-00
11111111 11111110 01001001 01101100 01000001 00000000
Unicode-big-endian文件格式的字节流﹕
FE-FF-6C-49-00-41
11111110 11111111 01101100 01001001 00000000 01000001
utf-8文件格式的字节流﹕
EF-BB-BF-E6-B1-89-41
11101111 10111011 10111111 11100110 10110001 10001001 01000001
从以上结果可以很容易的看到BABA正是"汉"字的gb2312编码﹐当然我的操作系统是繁体的﹐如果我直接双击打开﹐则可以看到"荦A"﹐这是乱码﹐因为我的系统baba查的是big5﹐而baba的big5码正是"荦"
然而还有其它很多程序﹐像IE呀,它可以使用meta标签来识别文件的编码,xml也是可以通过encoding属性来说明文件的编码的﹐所以这些程序的识别方法和普通的又有些不同罢了。
同样﹐写一个文本文件时﹐先写入这些标记符﹐则也会帮助notepad识别这些文件的编码(当然.net专门提供了一些类别﹐如StreamWriter﹐可以直接存成某种编码的格式)。
至于各种encoding之间的转换﹐我想也不必多说了﹐通过Encoding类的Convert,GetBytes和GetString方法是很容易进行转换的。