zoukankan      html  css  js  c++  java
  • 字符编码总结

    最近被字符编码问题搞的很头疼,很多编码方式可谓“耳熟不能详”,GB2312、ANSI、UTF-8、Unicode…。于是静下心来,好好学习一番。

    参考资料:

    http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

    http://www.regexlab.com/zh/encoding.htm

    字符与编码的发展

    系统内码

    说明

    阶段一

    ASCII

    计算机刚开始只支持英语,其它语言不能够在计算机上存储和显示。ASCII码一共规定了128个字符的编码,比如空格“SPACE”是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。

    阶段二

    ANSI编码(本地化)

    为使计算机支持更多语言,通常使用 0x80~0xFF 范围的 2 个字节来表示 1 个字符。比如:汉字 '中' 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储。不同的国家和地区制定了不同的标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准。这些使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。

    阶段三

    UNICODE(国际化)

    为了使国际间信息交流更加方便,国际组织制定了 UNICODE 字符集,为各种语言中的每一个字符设定了统一并且唯一的数字编号,以满足跨语言、跨平台进行文本转换、处理的要求。Unicode当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字“严”。需要注意的是,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。

    在接下去讨论之前,首先要明确几个概念:

    1、字符是我们可见和可以理解的符号,字节是计算机存储字符的形式。

    2、计算机在把字符存储到字节的时候需要使用某种编码方式,以便在重现字符的时候使用这种已知的编码方式解码。因此保存和打开都存在选择编码方式的问题。

    由上表可以看出,类似GB2312这样的编码属于ANSI规范中的一种。在windows的记事本中我们可以选择ANSI编码方式保存文本(默认用这种编码方式),而在不同语言版本的windows系统中ANSI编码方式是不同的;在简体中文系统中,记事本所指的ANSI就是GB2312。因此如果在英文系统下,使用默认的保存方式(ANSI)保存含有中文字符的文本,记事本将会给出提示,如果此时不予理会的话,中文信息将会丢失。当使用记事本打开一个文件时,记事本将自动检测当前文件的编码方式,并使用对应的编码方式解码,以重现文字符,当然默认使用当前系统语言环境下的ANSI编码。

    Unicode的实现---UTF-8

    互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。

    UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

    UTF-8的编码规则很简单,只有二条:

    1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。

    2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。(这里有人可能有疑问,n不是最多2吗。其实Unicode字符集码不一定是上面举例的两个字节,可能多于2的。)

    下表总结了编码规则,字母x表示可用编码的位。

    Unicode符号范围 | UTF-8编码方式
    (十六进制) | (二进制)
    --------------------+---------------------------------------------
    0000 0000-0000 007F | 0xxxxxxx
    0000 0080-0000 07FF | 110xxxxx 10xxxxxx
    0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
    0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

    下面,还是以汉字“严”为例,演示如何实现UTF-8编码。

    已知“严”的unicode是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此“严”的UTF-8编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是“11100100 10111000 10100101”,转换成十六进制就是E4B8A5。

    windows记事本程序可选的编码方式有:ANSI,Unicode,Unicode big endian 和 UTF-8。

    1)ANSI是默认的编码方式。对于英文文件是ASCII编码,对于简体中文文件是GB2312编码(只针对Windows简体中文版,如果是繁体中文版会采用Big5码)。

    2)Unicode编码指的是UCS-2编码方式,即直接用两个字节存入字符的Unicode码(对于大于2个字节的字符无法存储。UTF-16扩充了Unicode,包括了一些稀有字符,想我们国家的满文,藏文等等,两者基本上等价)。这个选项用的little endian格式。

    3)Unicode big endian编码与上一个选项相对应。下一节会解释little endian和big endian的涵义。

    4)UTF-8编码,也就是上面谈到的编码方法。

    Little endian和Big endian

    上一节已经提到,Unicode码可以采用UCS-2格式直接存储。以汉字”严“为例,Unicode码是4E25,需要用两个字节存储,一个字节是4E,另一个字节是25。存储的时候,4E在前,25在后,就是Big endian方式;25在前,4E在后,就是Little endian方式。

    自动检测编码方式---BOM

    Unicode规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格“(ZERO WIDTH NO-BREAK SPACE),用FE FF表示。这正好是两个字节,而且FF比FE大1。如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。

    在windows中Unicode编码中表示字节排列顺序的这个个文件头,也叫做BOM(byte-order mark),FFFE和FEFF就是不同的BOM。

    例如:

    1)Unicode:FF FE 表明是小头方式存储。

    2)Unicode big endian:FE FF 表明是大头方式存储。

    3)UTF-8:EF BB BF 表示这是UTF-8编码。

    Windows Codepage

    Codepage实际上是一系列表示不同编码规范的数值,常见的windows codepage有:

  • 874Thai
  • 932Japanese
  • 936Chinese (simplified) (PRC, Singapore)
  • 949Korean
  • 950Chinese (traditional) (Taiwan, Hong Kong)
  • 1200Unicode (BMP of ISO 10646, UTF-16LE)
  • 1201Unicode (BMP of ISO 10646, UTF-16BE)
  • 1250Latin (Central European languages)
  • 1251Cyrillic
  • 1252Latin (Western European languages)
  • 1253Greek
  • 1254Turkish
  • 1255Hebrew
  • 1256Arabic
  • 1257Latin (Baltic languages)
  • 1258Vietnamese
  • 65000Unicode (BMP of ISO 10646, UTF-7)
  • 65001Unicode (BMP of ISO 10646, UTF-8)

    windows内核使用的是Unicode。对于非Unicode的windows应用程序,windows在呈现GUI界面的时候,需要知道使用哪种codepage(即编码)来呈现字符串。这个codepage是可以通过控制面板设置的,所以在英文系统下安装某些中文软件会显示乱码,可能是因为没有把codepage设置成936。

  • 一些思考和体会

    1、UTF-8之所以被广泛接受,可能是因为它的统一性和卓越的“压缩”能力。首先,它是Unicode的一种实现,即可以实现国际化;其次,对于英文的存储它只用一个字节即可,而UTF-32不管什么都用4个字节、Unicode(UTF-16)都用两个字节等之辈都不如它省空间。而对于中文比较多的文档使用UTF-8未必有多好的效果,很多中文常用字都用3个字节存储。

    2、可以利用Unicode作为中间编码方式,实现各种编码之间的转化。详见下面所述的问题2。

    .NET下与字符编码相关的编程

    Encoding类用于处理编码的相关问题。

    EncodingInfo[] Encoding.GetEncodings()

    这是个静态方法。返回当前系统包含的所有编码方式,每种编码方式有名字、显示名字、对应的CodePage值。

    Encoding.GetEncoding(string name)

    这是个静态方法。这个方法还有多个重载,根据编码的名称返回编码对象。

    Encoding.Default\Encoding.Unicode...

    静态属性。提供一种便捷的方式返回一些常用的编码对象,其中Encoding.Default返回当前系统设置的本地化编码方式,简体中文的window系统下应该等同于GB2312。

    byte[] Encoding.GetBytes(string s)

    这是个实例方法。假如实例对象是对应UTF-8编码的对象,那么这个方法表示将字符串s,以UTF-8的编码方式编码成字节数组。(由于window内核使用了Unicode字符集,因此string在内存中是一Unicode方式存储的,这个方法实际上做的事情便是将Unicode转化成UTF-8)

    string Encoding.GetString(byte[] bytes)

    这是个实例方法。假如实例对象是对应UTF-8编码的对象,那么这个方式表示将字符数组bytes,以UTF-8的编码方式解码成字符串。(也就是UTF-8转化成Unicode)这个方法经常用于将流按照某种编码转化成字符串。

    问题1:判断字符是否是中文

    看到有同仁是这样做的:
    static public bool IsChina(char chr)
    {

      if (Convert.ToInt32(chr) < Convert.ToInt32(Convert.ToChar(128)))

        return false;

      else

        return true;
    }

    这里显然是错的,字符不是只有中文、英文和数字。我查阅了Unicode编码表,得出的结论是在Unicode字符集中中文的范围应该在0x4E00-0x9FC3之间,所以光判断大于128显然不正确。还有强调一下,之所以查阅Unicode而不是GB2312或是UTF-8编码表,因为char和string在windows管理的内存中都是Unicode方式存储的。

    问题2:如何将http请求返回的包含网页代码的流正确解码

    这个问题主要源于编码方式不统一。在互联网不发达的时候,本地化的编码层出不穷,导致当互联网发展起来后,出现了编码不统一的问题,很多网站只考虑本地化的编码,国内包括百度在内的网页的charset都是gb2312。一些国际化的网站一般使用UTF-8,比如微软的,谷歌的。(如果http请求www.google.com会发现charset是big5的,但是用IE打开是charset变成UTF-8,不解)

    因此,如果我们编程请求一个网页,那么返回的是流,如何才能用合适编码来解码这个流呢?思路其实很简单:对于一般的网页代码都有meta、都有charset,而英文字符在各种编码方式中是兼容的,我们可以用任何一种编码方式先把流解码出来编程string,然后用正则表达式匹配charset,得到网页的编码方式,然后再将string用之前的编码方式编码回字节流,再用匹配出来的编码方式解码这个字节流就可以了。

    这种方法对于没有charset和网页无能为力!

    关键代码:

    ...

    responseStream = webRequest.GetResponse().GetResponseStream();
    string ResultStringContent;
    string PreStringContent;
    using (StreamReader sr = new StreamReader(responseStream, encoding))//encoding可以是任何一种编码方式,一般可以是Encoding.Default
    {
        PreStringContent = sr.ReadToEnd();
        byte[] byteContent = encoding.GetBytes(PreStringContent);//用这种编码编码这个string,得到原始byte[]
        ResultStringContent = TryToGetEncoding(PreStringContent, encoding).GetString(byteContent);//TryToGetEncoding见下面
    }

    private static Encoding TryToGetEncoding(string content, Encoding tryEncoding)
    {
        /*try to parse html*/
        MatchCollection mc = Regex.Matches(content, "<meta [^>]*>", RegexOptions.IgnoreCase);
        foreach (Match m in mc)
        {
            if (m != null && m.Value != string.Empty)
            {
                Match mm = Regex.Match(m.Value, @"charset[ \t]*=[ \t]*[\w-_]+");
                if (mm != null && mm.Value != string.Empty)
                {
                    string s = mm.Value;
                    int start = s.IndexOf('=') + 1;
                    Encoding retEncoding = null;
                    try
                    {
                        retEncoding = Encoding.GetEncoding(s.Substring(start, s.Length - start).Trim());
                    }
                    catch
                    {
                    }
                    if (retEncoding != null)
                        return retEncoding;
                }
            }
        }
        return tryEncoding;
    }

    问题3:StreamReader读取流或文件乱码

    StreamReader实际上是一个带有编解码功能的流读取类,所以你可以看到它有ReadToEnd()方法可以直接返回string、它的Read方法返回的不是Byte[],而是char[]。

    StreamReader读取乱码用一句话归纳那肯定是:编码问题!但这个答案太概括了。其实本质是编码问题,但实际上是我们没有注意到使用StreamReader时的细节。

    在对StreamReader做了一些测试实验后,我总结如下:

    StreamReader的构造函数

    注意到StreamReader的构造函数有多达10个重载。其中有4个关键的构造参数,还有一个参数跟编码无关这里不讨论:

    1)Stream stream:传递一个流对象给StreamReader

    2)string path:传递一个文件路径给StreamReader

    3)bool detectEncodingFromByteOrderMarks:指定是否自动检测BOM,只针对Unicode(L\B)、UTF-8、UTF-16(L\B)、UTF-32(L\B)编码

    4)Encoding encoding:指定编码方式

    这四个参数相互的组合,构造出来多达10个的重载,这些组合使得StreamReader的工作方式令人疑惑不解,这也是出现乱码的根本原因,现在我们来一一解读:

    序号 原型 文件的编码
    UTF-8 Default(GB2312) Unicode(L)
    1 public StreamReader(Stream stream)
    默认将encoding设置为UTF-8,自动检测BOM
    正常 乱码(因为没有BOM,用UTF-8解码) 正常(有BOM)
    2 public StreamReader(string path)
    默认将encoding设置为UTF-8(这里MSDN说The default character encoding is used,实验证明默认是UTF-8),自动检测BOM
    正常 乱码(因为没有BOM,用UTF-8解码) 正常(有BOM)
    3 public StreamReader(
     Stream stream,
     bool detectEncodingFromByteOrderMarks
    )
    默认将encoding设置为UTF-8,可设置检测BOM,默认检测
    正常 乱码(因为没有BOM,用UTF-8解码) 如果设置不检测会是乱码,如果检测则正常
    4 public StreamReader(
     Stream stream,
     Encoding encoding
    )
    检测BOM,如果有BOM则忽略encoding参数,如果没有则应用encoding
    正常 在encoding设置为Encoding.Default时正常 正常
    5 public StreamReader(
     string path,
     bool detectEncodingFromByteOrderMarks
    )
    默认将encoding设置为UTF-8,可设置检测BOM,默认检测
    正常 乱码(因为没有BOM,用UTF-8解码) 如果设置不检测会是乱码,如果检测则正常
    6 public StreamReader(
     string path,
     Encoding encoding
    )
    先检测BOM,如果有BOM则忽略encoding参数,如果没有则应用encoding
    正常 在encoding设置为Encoding.Default时正常 正常
    7 public StreamReader(
     Stream stream,
     Encoding encoding,
     bool detectEncodingFromByteOrderMarks
    )
    先看detectEncodingFromByteOrderMarks是否为true,如果true那么行为同4,否则直接用encoding解码
    8 public StreamReader(
     string path,
     Encoding encoding,
     bool detectEncodingFromByteOrderMarks
    )
    先看detectEncodingFromByteOrderMarks是否为true,如果true那么行为同6,否则直接用encoding解码

    由上表的总结可以看出,自动检测BOM的优先级最高,如果取消检测或者没有BOM则应用Encoding,如果参数提供了Encoding那么应用参数的Encoding,否则就用UTF-8。

    所以最后的一个终极解决方案就是StreamReader sr = new StreamReader(fileStream,Encoding.Default)。

    另外,对于上表的第二条,我的实验结果跟MSDN的描述有出入,有待考证。

查看全文
  • 相关阅读:
    static 静态
    纽扣电池带负载能力差
    JAVA--异常(1)
    【DP专题】——洛谷P1273有线电视网
    我到现在都没有搞明白git233333
    git常见问题之git pull origin master时fatal: refusing to merge unrelated histories
    矩阵内积转化为求矩阵乘积的迹
    矩阵分解系列三:非负矩阵分解及Python实现
    矩阵分解系列三:可对角化矩阵的谱分解
    矩阵分解系列二:正交三角分解(UQ、QR分解)
  • 原文地址:https://www.cnblogs.com/P_Chou/p/1811742.html
  • Copyright © 2011-2022 走看看