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的描述有出入,有待考证。

查看全文
  • 相关阅读:
    ABAP接口用法
    监听textarea数值变化
    The first step in solving any problem is recognizing there is one.
    Wrinkles should merely indicate where smiles have been.
    God made relatives.Thank God we can choose our friends.
    Home is where your heart is
    ABAP跳转屏幕
    Python 工具包 werkzeug 初探
    atom通过remote ftp同步本地文件到远程主机的方法
    Mongodb学习笔记一
  • 原文地址:https://www.cnblogs.com/P_Chou/p/1811742.html
  • Copyright © 2011-2022 走看看