zoukankan      html  css  js  c++  java
  • 字符集与字符编码的强化理解与操作实践

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

    踩坑

    最近在工作中遇到了一个说大不大说小不小的问题,就是当我解析一个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
  • 相关阅读:
    小波变换的引入,通俗易懂
    Leetcode 437. Path Sum III
    Leetcode 113. Path Sum II
    Leetcode 112 Path Sum
    Leetcode 520 Detect Capital
    Leetcode 443 String Compression
    Leetcode 38 Count and Say
    python中的生成器(generator)总结
    python的random模块及加权随机算法的python实现
    leetcode 24. Swap Nodes in Pairs(链表)
  • 原文地址:https://www.cnblogs.com/yejianyong/p/7413127.html
Copyright © 2011-2022 走看看