结合实例学习|字符编码和解码

前言

前人踩过的坑,后人不必再踩!

编码格式,在前后端的对接中及其重要,由于一些编码格式的局限性,以及繁多的编码格式,只要是双方对接的编码格式不对,通常都会发生中文乱码问题。而作者也在实际项目中遇到了这种情况,并且进行了排查,对此学习过程进行记录。

本文首先讲下对应的基本知识点,从而讲下一些基本操作,再通过实际项目中的排查过程进行概念的进一步理解。

什么是编码和解码?

由来

计算机自己能理解的“语言”是二进制数,最小的信息标识是二进制数,8个二进制位表示一个字节;而我们人类所能理解的语言文字则是一套由英文字母、汉语汉字、标点符号字符、阿拉伯数字等等很多的字符构成的字符集。如果要让计算机来按照人类的意愿进行工作,则必须把人类所使用的这些字符集转换为计算机所能理解的二进制码,这个过程就是编码,他的逆过程称为解码。

因为计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理。所以才要有编码的存在。

Java中的相关用法

1.编码(encode) : String ---> byte[]
String中有对应的方法:
①:byte[] getBytes() : 使用平台的默认字符集将此 String 编码为 byte 序列
②:byte[] getBytes(Charset charset) : 使用指定的字符编码来编码字符串
③:byte[] getBytes(String charsetName) : 使用指定的字符编码来编码字符串
2.解码 (decode) : byte[] ---> String
String中有对应的构造方法:
①:String(byte[] bytes) : 通过使用平台的默认字符集解码指定的 byte 数组
②:String(byte[] bytes, Charset charset) : 使用指定的字符集来解码指定的byte数组
③:String(byte[] bytes, String charsetName) : 使用指定的字符集来解码指定的byte数组

Byte

字节(Byte)是存储数据的基本单位,并且是硬件所能访问的最小单位。

一个字节储存的数值范围为0-255(无符号数)。

1B=8bit。

简单理解,也就是能放8个0或者1,数值上等于一个一位的32进制数,一个两位的16进制数。

编码方式

大致讲下常见的几种编码格式:ASCII,UTF-8,GBK

ASCII

我们知道,计算机内部,所有信息最终都是一个二进制值。每一个二进制位(bit)有01两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从0000000011111111

上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。

ASCII 码一共规定了128个字符的编码,比如大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0

Unicode

Unicode让全世界都说一种语言

为了实现跨语言、跨平台的文本转换和处理需求,ISO国际标准化组织提出了Unicode的新标准,这套标准中包含了Unicode字符集和一套编码规范。Unicode字符集涵盖了世界上所有的文字和符号字符,Unicode编码方案为字符集中的每一个字符指定了统一且唯一的二进制编码,这就能彻底解决之前不同编码系统的冲突和乱码问题。

UTF-8

UTF-8 是 Unicode 的实现方式之一。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 码。

下表总结了编码规则,字母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 编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。

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

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

GBK

专门为解决汉字的编码而生成的解决方案,一个汉字用两个字节表示

​ 那么,一个汉字究竟被存储为什么,就需要:先查unicode码表,然后根据在码表的位置进行计算。例如:“电”字,在码表中是3575,计算成utf8就是E794B5,而在GB2312的码表中为B5E7。
​ GBK的中文编码是双字节来表示的,英文编码是用ASCII码表示的,既用单字节表示。但GBK编码表中也有英文字符的双字节表示形式,所以英文字母可以有2种GBK表示方式

为区分中文,将其最高位都定成1。英文单字节最高位都为0。当用GBK解码时,若高字节最高位为0,则用ASCII码表解码;若高字节最高位为1,则用GBK编码表解码。

ISO-8859-1

tomcat默认编码方式

ISO-8859-1编码是单字节编码,向下兼容ASCII,其编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。

BIG5

Big5,又称为大五码五大码,是使用繁体中文(正体中文)社区中最常用的电脑汉字字符集标准,共收录13,060个汉字。大多用于我国台湾,香港和澳门等。

字节序

字节序,简单理解,就是字节存放的顺序的意思,这一小节是对于硬件底层是怎么对字节顺序的存储的阐述,不想看的可以略过。

上一节已经提到,UCS-2 格式可以存储 Unicode 码(码点不超过0xFFFF)。以汉字为例,Unicode 码是4E25,需要用两个字节存储,一个字节是4E,另一个字节是25。存储的时候,4E在前,25在后,这就是 Big endian 方式;25在前,4E在后,这是 Little endian 方式。

这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-endian)敲开还是从小头(Little-endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。

第一个字节在前,就是"大头方式"(Big endian)(人类思维,12 34 56 78),第二个字节在前就是"小头方式"(Little endian)(颠倒思维,78 56 34 12)。

那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?

Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做"零宽度非换行空格"(zero width no-break space),用FEFF表示。这正好是两个字节,而且FFFE1

如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。

实际项目中遇到的问题

负责的是通过java调用python的数据接口,然而写python的人不靠谱,说明用UTF-8,最后我排查出来,用的编码方式不是UTF-8,浪费我一下午的时间。

说下排查过程,通过http请求的方式进行调用数据接口,java方面获取数据是从reponse的输入流中进行获取,我从输入流中获取到完整的byte数组,但是我通过UTF-8拿数据出来,会发生乱码的现象,所以我干脆写了个工具类,对byte数组进行所有字符编码方式的验证,最后也比较图方便,通过看打印出来的信息是不是乱码来判断。

    //查看byte数组是什么编码格式
    @Test
    public void byteToCheck() throws UnsupportedEncodingException {

        Map<String , Charset> map = Charset.availableCharsets();
        Set<Map.Entry<String , Charset>> set = map.entrySet();
        for(Map.Entry<String , Charset> entry : set){
            checkEncoding(String.valueOf(entry.getValue()));
            String s = String.valueOf(entry.getValue());
        }
    }

    //检查编码格式
    private void checkEncoding( String charset ) throws UnsupportedEncodingException {
        //从字节流中来的
        byte[] byteArray =  {123, 34, 99, 111, 100, 101, 34, 58, 32, 48, 44, 32, 34, 109, 101, 115, 115, 34, 58, 32, 34, -27, -68, -89, -27, -98, -126, -24,-82, -95, -25, -82, -105, -25, -88, -117, -27, -70, -113, -24, -65, -112, -24, -95, -116, -27, -121, -70, -23, -108, -103, 34, 44, 32, 34, 100, 97, 116, 97, 34, 58, 32, 123, 34, 103, 97, 109, 97, 54, 34, 58, 32, 48, 44, 32, 34, 104, 111, 114, 105, 122, 111, 110, 116, 97, 108, 115, 116, 114, 101, 115, 115, 34, 58, 32, 48, 44, 32, 34, 112, 105, 99, 116, 117, 114, 101, 95, 97, 100, 100, 114, 101, 115, 115, 34, 58, 32, 34, 104, 116, 116, 112, 58, 47, 47, 52, 55, 46, 49, 49, 51, 46, 49, 48, 52, 46, 50, 50, 57, 47, 112, 121, 116, 104, 111, 110, 47, -24, -82, -95, -25, -82, -105, -27, -121, -70, -23, -108, -103, -24, -65, -108, -27, -101, -98, -27, -101, -66, 46, 112, 110, 103, 34, 44, 32, 34, 115, 97, 103, 95, 109, 97, 120, 34, 58, 32, 48, 44, 32, 34, 97, 34, 58, 32, 48, 125, 125};
        //打印编码
        System.out.println( charset+"编码格式");
        System.out.println(new String(byteArray,charset));
    }

排查出来字符编码是CESU-8。

参考资料

(3条消息) JavaSE基础(124) IO流读写乱码问题(字符编码)_郑清的IT学习之路-CSDN博客

字符编码笔记:ASCII,Unicode 和 UTF-8 - 阮一峰的网络日志

(4条消息) 字符串编码:ASCII、GB系列、Unicode、UTF-8_yangyang的专栏-CSDN博客

(4条消息) 字符串编码:ASCII、GB系列、Unicode、UTF-8_yangyang的专栏-CSDN博客

(4条消息) 汉字编码之GBK编码(附完整码表)_郭晓东的专栏-CSDN博客

posted @ 2020-09-27 22:41  亥码  阅读(549)  评论(4编辑  收藏  举报