• 字符集与字符编码的强化理解与操作实践


    字符集与字符编码的强化理解与操作实践

    踩坑

    最近在工作中遇到了一个说大不大说小不小的问题,就是当我解析一个xml文件的时候,抛出了一个”Invalid byte 2 of 2-byte UTF-8 sequence”的异常,这个异常会导致解析直接退出,显然不能容忍。查阅相关资料稍微定位了一下,大概知道是字符集的问题,仔细一看,xml文件中的确有中文字符,而且当我把这些中文字符删了之后的确又能解析成功。不过我还是不能理解这当中的缘由,不过由于时间原因,当时只是把中文字符删了就草草完工。现在回头想想这个坑还是不能留,顺便趁机补下字符集相关的知识。

    字符集和字符编码

    字符集

    字符集的概念是一个非常容易让人混淆的概念,很多情况下我们都会把他跟字符编码当成是同一个概念,但是事实上这两个概念其实是完全不一样的。
    所谓字符集,其实是对所有字符映射到唯一ID的一个映射表,或者叫hash表,比如我就可以定义一个字符集,这个字符集里只有四个字符—-“我”,”是”,”帅”,”哥”。那么我就可以把这四个分别映射为0,1,2,3,二者一一对应:

    1
    2
    3
    4
    我-0
    是-1
    帅-2
    哥-3

    字符编码

    但是字符集只是规定了字符与数字之间映射关系,并没有规定如何在二进制文件中进行表示(编码)。我可以定义很多中字符编码方法,比如我可以认为所有的字符都占两个bit位,这样当读取文件流的时候,我就可以两个bit两个bit的去读,并按照下面的规则进行解析:

    1
    2
    3
    4
    00-我
    01-是
    10-帅
    11-哥

    看上去没问题,但是有人可能会说,这种编码不好,为啥呢,因为这样子每个字符都占用了2个bit,可能在某些情况下”我”这个字符出现的次数非常多,其他的字符出现的非常少,那么使用上面的编码方法可能就会浪费空间。我们可以用类似Huffman编码的策略修改一下编码方法:

    1
    2
    3
    4
    0-我
    10-是
    110-帅
    111-哥

    这其实就是构造了一个二叉树,每一个内部节点就是0或1,每一个叶子节点就是一个字符。当我们解析的时候就顺着这棵树去找相应的字符就行了。
    这种编码能保证当“我”出现次数很多的时候,文件的大小能够变小。当然我们需要注意每个字符的编码都不能是其他字符的前缀,否则就会出现解析混乱。

    其实所谓字符集和字符编码的关系就是这么简单。只是由于历史原因导致当前的字符集和字符编码比较杂乱,没有绝对的统一,因此才会出现各种”乱码”现象。

    Unicode字符集与UTF-8编码

    为什么要单独拿Unicode字符集跟UTF-8编码来说是呢,一方便是因为这两个东西被用的最广,尤其是Java语言的原生支持;另一方面正是因为用到广,因此这两个东西被人误解的最多。
    我在一开始了解这两个东西的时候也很蒙,有的文章说Unicode是一种编码,有的文章说Unicode不是编码而是字符集,有的文章说UTF-8是一种Unicode编码,有的文章说UTF-8不是Unicode编码。。。现在回想起来,其实他们说的都对,又都不全对。

    Unicode是一种字符集

    没错,Unicode当然是一种字符集,他又被称为”万国码”,能够表示很多很多的字符,具体的个数还在持续增加,目前根据WIKI上的说法,截至2017年6月已经增加到了13万个字符了。
    所谓字符集,当然是想要多少有多少了,因此没有“Unicode能表示的最多字符数”这个概念。当需要增加新字符的时候,大不了把表格增加几行,然后对外发布个声明罢了。

    Unicode有一个默认的编码叫UCS-2

    这个概念是非常坑的,正式因为Unicode有一个默认的编码UCS-2(Universal Character Set),因此才导致了概念的混乱。我们可以在很多地方看见所谓“Unicode编码”这个概念,其实他们说的不是Unicode字符集,而是UCS-2编码。这种编码方式就像我之前举的第一个例子类似,是一种定长的编码方式,每一个字符都用两个字节来表示。这就导致了他最多只能表示2^16个字符。因此很多地方提到说”Unicode编码最多能表示65536个字符”,其实指的是UCS-2编码。
    显然,这种编码方式并不具备较好的扩展性。我们前面提到Unicode已经有13万个字符了,显然UCS-2编码搞不定了。因此当前很多系统都不会默认用UCS-2编码,而是用扩展性更好的UTF-8编码,不过在windows中还是经常会用到Unicode(UCS-2)编码。

    UTF-8是Unicode字符集上的编码

    其实UTF-8跟UCS-2一样,都是Unicode字符集上的编码,不过UTF-8使用的方式更像我上面举的第二个例子。采用UTF-8编码的字符有可能占用1个字节,比如ACSII码,也可能占用2-3字节,比如中文,也有可能占用4个以上字节,比如中日韩的一些超大字符集里的文字。正是由于UTF-8采用的变长编码,因此他能够更有扩展性,被用的也最广。
    具体的编码方式这里就不多说了,网上资料有很多。

    Java的字符支持

    支持方式

    既然知道了字符集的相关知识,就有必要了解一下在具体的编程工作中的注意点了。我们知道Java是原生支持Unicode的,他默认采用的就是UTF-8编码来处理文件以及存储字节码。一个最具体的表现就是,在java中,我们可以将一个中文赋值给一个char,而在C中,这样的操作是会报warning,并且中文会乱码的。
    我们知道Java有个InputStreamReader,他的作用就是将从文件读取的字节流转化为字符流。他读取InputStream中的字节流,并且对他进行字符解码。我们可以通过在这里指定编码方式从而对编码流程进行控制。比如这样:

    1
    2
    InputStream inputStream = new FileInputStream("/home/myths/examples.desktop");
    InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");

    通过InputStreamReader,所有的字节都被转化成了Unicode字符集保存在了内存里。
    在默认情况下,Java采用的就是UTF-8的编码解码方式,当我们需要用指定的解码方式去解析文件的时候,我们就可以在这里进行指定。

    Java支持的字符集

    我们在指定字符集的时候需要注意,这些字符集一定要被Java支持,否则就会抛出UnsupportedEncodingException”。事实上对与这些字符集的名字,Java能够做到不区分大小写,忽略横线之类的辅助字符,不过我们最好还是写成标准形式。他的标准形式可以如下获取:

    1
    Charset.availableCharsets().keySet().forEach(System.out::println);

    编码方式预测

    很多情况下,当我们拿到一堆乱码的文件时,我们非常想知道这玩意的编码方式到底是啥。。。其实原则上来说,这种事情目前是无法精确办到的。一个最极端的例子就是纯ASCII码文件,绝大多数的编码方式都支持ASCII码单字节保存,那么给你一个纯ASCII码文件,你可以说他是ASCII码编码,也可以说他是UTF-8编码。不过windows的团队耍了一个小聪明,当我们用他的记事本去保存文件的时候,他会在文件开头加上三个字节的标记,告诉windows说这是啥编码方式。这三个字节就叫万恶的BOM。在windows下BOM这个东西还是很管用的,可是到了其他环境下,就会发现多出三个空白字符,很多命令都会解析失败,这就非常讨厌了。。。
    事实上,虽然没有一个绝对准确的编码方式的预测方法,但是还是会有一些统计规律的,有了这些规律,我们就有了一些工具。

    file命令
    file命令是Linux自带的文件信息查看工具,我们可以用这个命令来简单查看文件的编码方式:

    1
    2
    myths@pc:~$ file -bi test.txt
    text/plain; charset=utf-8

    uchardet
    uchardet是一个开源的工具,据说比file的更准

    1
    2
    myths@pc:~$ uchardet test.txt
    UTF-8

    编码转换工具

    有时候我们可能希望将文件的编码方式进行转换。需要注意的是,所谓的转换文件的编码,其实包括下面几个步骤:

    1. 读取二进制流,
    2. 按照旧的编码规则进行解码成统一的字符集
    3. 根据字符集,按照新的编码规则进行编码成新的二进制流
    4. 将二进制流写入文件

    因此在进行编码格式转化的时候实际上就修改了文件本身,这一点需要注意。
    转换编码最简单的方法其实可以通过iconv这个命令来进行处理:

    1
    myths@pc:~$ iconv -f UTF-8 -t GBK sourcefile > outputfile

    通过-f指定旧的解码方式,通过-t指定新的编码方式,并将结果输出到新的文件中。

    综合实践

    下面做一个小实验。我们现在有如下的乱码数据,问这些数据是用什么编码的,他的正确编码方式应该是什么。
    由于乱码的字符复制粘贴会影响二进制表示,因此我们通过指定二进制的方式来生成测试文件。

    1
    echo -e "xb5xf3xbcxd2xbaxc3xa3xacxcexd2xcaxc7xcbxa7xb8xe7xa3xacxbbxb6xd3xadxb4xf3xbcxd2xbaxcdxcexd2xd7xf6xc5xf3xd3xd1xa1xa3"> guess

    那么这个guess里装的到底是啥呢?


    答案与解析
    如果你电脑的默认字符集是GBK,那么或许你已经看到了答案了。
    如果你电脑的默认字符集是UTF-8之类的,那你大概就要稍微折腾一番了。
    1. 通过file -bi guess命令来猜测文件的字符集,发现是ISO-8859
    2. 尝试iconv命令,发现并不能正常解码,放弃。
    3. 通过uchardet guess命令来猜测文件的字符集,可以看到字符集是gb18030
    4. 通过iconv -f GB18030 -t UTF-8 guess命令可以将字符集从GBK转换为UTF-8
    5. 答案:“大家好,我是帅哥,欢迎大家和我做朋友。”
    以下链接是对uchardet这个工具的介绍:
    http://blog.sina.com.cn/s/blog_5258e1360102vikh.html  工具安装不需要看改文章,直接看github的安装方式即可
    总结:此外,由于该工具是开源的,所有对于大对数的程序员来说,可以把该代码集成到直接对应的sdk上。个人目前测试了这个判断方式,感觉这个方法准确率还是很高的。非常感谢文章的
    作者:myths
  • 相关阅读:
    struts2 显示表格
    设置eclipse默认编码为UTF-8 Set default encoding to utf-8 in eclipse
    java hibernate +mysql demo
    Java项目引入第三方Jar包
    mysql 常用sql
    C# snaps
    sql server 与mysql差异(innodb)
    系统数据监控
    Twitter Bootstrap Grid System
    设计模式之访问者模式
  • 原文地址:https://www.cnblogs.com/yejianyong/p/7413127.html
Copyright © 2020-2023  润新知