专栏首页Java进阶指南我都服了,为啥上游接口返回的汉字总是乱码?

我都服了,为啥上游接口返回的汉字总是乱码?

前言

想必大家编写代码时肯定和我一样,也遇到过汉字乱码的问题。特别是,有时候和上下游对接接口,不能统一编码格式的话,一堆乱码问题,让人头皮发麻。

那么为什么会有这么多的乱码问题?

什么是字符编码?什么是字符集?他们之间有什么区别和联系?

什么是 Unicode ?Unicode 和我们常说的 UTF-8 又有什么关系?

字符编码和解码

要想搞清楚上面的问题,首先我们要知道,在计算机中,不管是一段文字、一张图片还是一段视频,最终都是以二进制的方式来存储。也就是最终都会转化为 0001 1011 0010 0110 这样的格式。

换句话说,计算机只认识 0 和 1 这样的数字,并不能直接存储字符。所以我们需要告诉它什么样的字符对应的是什么数字。

例如,我们的业务中有记录客户端的客户行为日志,然后导出文件来分析,字段间会以 ESC 来分隔。

我在编写代码的时候,就需要定义一下这个ESC 字符应该对应什么数字,这样计算机才能识别并存储。

比如我把它定为 0001 1011,这样计算机就把 ESC 这个字符存了下来。等我下次需要查看的时候,根据对应关系把它解出来就可以了。

上边的两个过程就对应字符的编码和解码过程。

字符编码就是把字符按一定的规则,转换成数字。字符解码是编码的逆过程,即把数字按规则转换成字符。

这样看来,貌似没有什么问题。

但是,这是我自己定义的编码规则,我同桌阿霄就不乐意了。他非要认为 ESC 应该定义为 1101 1000,好家伙正好和我定义的二进制数字顺序相反。

那结果肯定不用说了,我把 0001 1011 这串数字给他之后,按照他的编码规则来解,肯定是 &$#!这样的东西。

所以,乱码问题说到底,就是编码和解码的规则对应不上导致的。

ASCII 码

为了避免我和阿霄因为编码问题打起来,美国国家标准学会(AMERICAN NATIONAL STANDARDS INSTITUTE) ANSI 组织发话了。

停、停、停。不就是个编码问题吗,这种小事犯不着动手,我定义一个统一的规则,大家都按照我的规则来编码和解码不就好了嘛。

于是,ASCII 码出现了,它定义了一个常用字符集,用来表示字符和数字的对应关系,如下表。

ASCII 码全称:美国信息交换标准代码 (American Standard Code for Information Interchange)

图片来自百度图片

我一查表,ESC 字符不就对应 27 吗,对应的二进制就是 0001 1011 。我去,没想到我定义的规则竟和 ANSI 不谋而合。

同桌阿霄把抡在空中的拳头收了起来,默默地回去敲代码了。

ASCII 码扩展码

在使用英语的国家,ASCII 码就足够用了。但是,在其他欧洲发达国家比如法国,使用的语言是法语,有类似于这样的 á 符号,ASCII 码就不能表示了。那怎么办呢?

我们看上表就会发现,ASCII 码表的表示范围是十进制 0~127,也就是二进制 0000 00000111 1111 。其实只是用了后边的 7 位,第一位都是 0 。

而计算机二进制中一个字节是 8 个位,现在只用了 7 位。不行啊太浪费了,要充分利用第一个高位,扩展一下,这样多了一位,能表示的字符范围就多了一倍。(2的8次方=256)

这样一些欧洲其他国家,也能在计算机中表示自己的文字了。

后来,随着计算机的普及,中国的用户也多了起来。却发现,一个字节只能表示 256 个字符,远远不能满足我们的要求。

于是,就出现了 GB2312 编码,它使用了两个字节来表示一个汉字。但是,并没有把所有的位都用完,前面一个字节范围 0xA1 ~ 0xF7 (即 10110001 ~ 11110111),后面一个字节范围 0xA1 ~ 0xFE (即 10110001 ~ 11111110) 。这样就能表示简体汉字 6763 个。

GB2312 是国家标准总局发布的《信息交换用汉字编码字符集》,也可以说是简体中文的字符集。

但是,台湾和香港等使用繁体字的地区怎么办。于是,就有了大五码 Big5 编码来存储繁体。高字节(第一个字节)表示范围 0x81~0xFE,低字节(第二个字节)表示范围 0x40 ~ 0x7E,以及0xA1 ~ 0xFE 。

需要注意的是,GB2312 是简体中文,Big5 是繁体中文。如果用其中一种编码文字去读另外一种编码文字就会乱码。所以,就出来了 GBK 编码,把简体中文和繁体中文,以及一些 GB2312 不支持的人名(如历代总理有的名字用 GB2312 打不出来),还有一些我们不认识的古汉语都包含进去,共 2 万多个字符。

再然后,我们发现少数民族像藏文,蒙古文这些少数民族的语言,GBK 也支持不了,就再进行扩展,出现了 GB18030 。又多了几千个少数民族的文字。

所以,我们使用的 GB 国标系列文字都是在 ASCII 码之上扩展的,它们是依次向下兼容的。表示文字范围从小到大为 GB2312 = Big5 < GBK < GB18030 。

Unicode 字符集

我们在打开一个文档之前,就必须要知道它的编码格式,否则用错误的方式解码就会出现乱码情况。

设想,如果一个文本中,有多种类型文字,包括中文,韩语,德语,日语,应该用哪种编码方式?貌似怎么处理都会有乱码问题,那怎么办呢?

ISO(国际标准化组织)说:这好办啊,我把地球上,只要是人们使用的,所有语言和符号都囊括其中,为每个字符都指定一个唯一的字符码,这样就没有乱码问题了。于是 Unicode 出现了,又叫统一码,万国码。

如上图表,汉字“一”对应的 unicode 码是 \u4e00。我们通常在字符码前加个 \u代表这是 unicode 码。4e00 是十六进制表示。

也有很多在线转码工具供我们使用,如:http://tool.chinaz.com/tools/unicode.aspx

Unicode 编码方案

首先强调一下以下几个概念的区别:

  1. 字符:就是我们看到的一个字母或一个汉字、一个标点符号都叫字符。如上边的汉字“一”就是一个字符。
  2. 字符码:在指定的字符集中,一个字符对应唯一一个数字,这个数字就叫字符码。如上边的字符“一”,在 Unicode 字符集中,对应的字符码为 \u4e00
  3. 字符集:规定了字符和字符码之间的对应关系。
  4. 字符编码:规定了一个字符码在计算机中如何存储。

需要注意的是,Unicode 只是一个字符集,它规定了每个字符对应的唯一字符码,却没有规定这个字符码在计算机中怎样存储(也就是它的字符编码格式)。

例如,上边的汉字“一”,它的 Unicode 字符码为 \u4e00,转换成二进制就是 100 1110 0000 0000 。可以看到,它有 15 位二进制数,至少需要两个字节来存储。

这只是简单的汉字,如果其他复杂的字符有可能会需要 三、四 个字节或者更多字节来存储。

那么到底应该用几个字节来存储呢?

于是 UTF-32 编码 制定了标准,一个字符就用四个字节来表示。这样编码和解码都方便,固定取 32 位二进制就行了。

但是这样又引来一个问题。比如 A 字符其实只需要一个字节就可以存储了。如果必须要用四个字节来存储,那么前边三个字节都要补 0 ,这样势必会造成空间的浪费。

于是 UTF-16 编码(一个字符用两个字节或者四个字节)和我们熟悉的 UTF-8 编码格式就出现了。

这里我们重点介绍 UTF-8 。它使用一种变长的编码方式,可以使用 1~4 个字节来表示一个字符。根据不同的字符变换长度。

变长听起来很美好,但是它的不固定性,就让计算机懵逼了。比如,计算机怎么知道这四个字节代表的是一个字符,还是四个字符,亦或是两个字符呢?

于是,UTF-8 规定了以下编码规则,来避免以上问题。

  • 对于单字节的符号,第一位设为0,后边 7 位对应这个字符的ASCII码值。因此,像“A"这样的英文字母,UTF-8 编码和 ASCII 编码是相同的。
  • 对于大于一个字节的符号,假设为 n 字节,那么第一个字节的前 n 位都设为 1,这样有几个 1 就说明有几个字节。然后,第 n+1 位设为0 。后边的字节,前两位都设为10 ,剩余的其他二进制位都用这个字符的 Unicode 码填充(从后向前填充,不够补0)。

字节个数

Unicode符号范围(16进制)

UTF-8 编码格式(二进制)

1(单字节)

0000 0000 ~ 0000 007F

0xxxxxx

2

0000 0080 ~ 0000 07FF

110xxxxx 10xxxxxx

3

0000 0800 ~ 0000 FFFF

1110xxxx 10xxxxxx 10xxxxxx

4

0001 0000 ~ 0010 FFFF

11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

刚开始看上表,可能比较懵逼。其实,Unicode 符号表示的范围最大为四个字节,因此二进制为 4*8=32 位。我们知道,二进制转换十六进制时,以四位为一个单位转换,因此,对应的十六进制为 32/4=8 位。

上表中的 Unicode 符号范围是以 16 进制表示,可以看到就是 8 位的。

我们还是以汉字 “一” 为例,16进制表示为 4e00,补全所有位,其实就是 0000 4E00 (不区分大小写)。因此,查上表发现,它处在三个字节的 Unicode 范围内(0000 0800 < 0000 4e00 < 0000 FFFF)。

所以,它用 UTF-8 来编码,就是三个字节的,即格式是这样的 1110xxxx 10xxxxxx 10xxxxxx

4e00 转换为二进制为 100 1110 0000 0000,二进制位从后向前依次填充到上述格式中的x位置(也是从后向前填充)。

于是,就得出汉字 “一” 的 UTF-8 编码后的二进制表示为:1110 0100 1011 1000 1000 0000

其实,可以发现,汉字的二进制为 15 位,前边补零一位即为 16 位 0100 1110 0000 0000。而三个字节的 UTF-8 编码格式中的 x 个数也为 3*8 - (4+2+2) = 16 位,正好一一对应。

那么,我们这一通推算,是否正确呢。可以在程序中打印这个字符的二进制格式,以及UTF-8编码后的二进制。程序如下,

public class Test {
    public static void main(String[] args) throws UnsupportedEncodingException {
        System.out.println("字符'一'的二进制为:" + Integer.toBinaryString('一'));
        System.out.println("========");
        String str = "一";
        System.out.println("转换为UTF-8编码格式的二进制为:"+ toBinary(str,"utf-8"));
    }

    public static String toBinary(String str, String encode) throws UnsupportedEncodingException {
        StringBuilder sb = new StringBuilder();
        byte[] bytes = str.getBytes(encode);
        for (int i = 0; i < bytes.length; i++) {
            byte b = bytes[i];
            sb.append(Integer.toBinaryString(b & 0xFF));
        }
        return sb.toString();
    }

}

打印结果为:

字符'一'的二进制为:100111000000000
========
转换为UTF-8编码格式的二进制为:111001001011100010000000

PS:通常的,我们发现常用的汉字以 16 进制表示都在 0000 0800 ~ 0000 FFFF 范围内。因此,汉字在 UTF-8 编码下通常占用三个字节。

细心的同学可能发现了,我上边转换的汉字可以用 char 类型来存储,这是为什么呢?

这是因为,在 Java 中,默认使用的字符集就是 Unicode,可以容纳 100 多万个字符,其中就包括汉字。

我们使用的绝对大多数汉字,都在0000 0800 ~ 0000 FFFF 这个范围内,可以看出来前边的四位十六进制都用不到(都是0000),因此,只需要后边的四位十六进制位,转换为二进制就是 4*4=16 位,只占用了两个字节(16/8=2)。而 char 在 Java 中占用两个字节,完全可以用来存储汉字。

总结

最后,来解答下文章开头的问题。

乱码的问题,究其根本原因,其实是编码和解码时的规则不一样导致的。

字符编码和字符集是两个不同的概念。一句话表示:字符集定义了字符到数字的映射关系,字符编码定义了这个数字如何在计算机中表达(存储)。

对于 ASCII 和 GB 系列,他们既是字符集也是字符编码。GB 兼容 ASCII 码。

而对于 Unicode 来说,字符集是 Unicode,而字符编码可以是 UTF-8,UTF-16 和 UTF-32 。所以,我们平时常用的 UTF-8 编码其实只是 Unicode 的一种编码实现方式而已。

本期内容你学会了吗,把学会打在评论区。。

本文分享自微信公众号 - 烟雨星空(mistyskys),作者:烟雨星空

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-08-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 前端生僻字显示

    异名在一个游戏项目中遇到一个比较有意思的问题,在游戏的玩法设定中,当怪物在消失的时候会爆出一个中文字,这个效果在部分机型上会出现乱码符号

    异名
  • 彻底搞懂 python 中文乱码问题

    我们来说说 Python 中是如何存储字符的,先来看一个乱码的例子。新建一个 demo.py 文件,文件存储格式为utf-8文件中内容如下。

    py3study
  • 【云+社区年度征文】新的开始,新的挑战

    嘿,你什么时候回去啊,买到票了吗?没有哦。回家的车票一秒钟就没有了,连站票都没抢到。真的烦,话说你呢?我也是啊,没抢到票哦,都不知道咋回去。到时候看看,能不能搭...

    不一样的科技宅
  • Apache环境下页面乱码的几种可能总结

    采用典型的LAMP架构开发的时候,环境中多处涉及到编码的指定,有一个地方忽略,都有可能造成页面汉字乱码的产生,本文将总结这些乱码产生的可能的原因,方便我们排查。

    大江小浪
  • 彻底搞懂 python 中文乱码问题(深入分析)

    前言 曾几何时 Python 中文乱码的问题困扰了我很多很多年,每次出现中文乱码都要去网上搜索答案,虽然解决了当时遇到的问题但下次出现乱码的时候又会懵逼,究其...

    砸漏
  • 北漂生活第二十弹-只想在舒适区躺平【后端开发实习】

    不得不说,最近的互联网公司 风雨飘摇 各种政策缩紧 加上 疫情和市场的影响 特别是在线教育 现在风雨飘摇,但是我所在的公司倒是岁月静好。

    韩旭051
  • 不想再被鄙视?那就看进来!一文搞懂 Python 2 字符编码

    来源:xybaby自荐投稿 www.cnblogs.com/xybaby/p/7814299.html 程序员都自视清高,觉得自己是创造者,经常鄙视不太懂技术的...

    企鹅号小编
  • 一台主机最多能创建多少个 TCP 连接?

    我一看这个表,这不就是经典的 socket 四元组嘛。我只有一块网卡,其 IP 地址是 123.126.45.68,我想要与 110.242.68.3 的 80...

    崔庆才
  • 国王们的海盗

    最近Magic Leap终于发布了第一款产品,按照这个公众号的尿性,一般是等热点快过的时候再去讨论。 『卧槽,现在这个产品概念图刚出,不是炒作的最好时间段吗?这...

    沙因Sign
  • 常见乱码问题分析

    在字符集这一篇文章中,我们基本了解了字符集的一些概念,也知道了什么是编码,什么是编码什么是解码。那么接下来我们就聊聊乱码。

    用户5475193
  • 数字文旅周报54期 | 腾讯携手山西文旅集团共同打造“一部手机游山西”

    ? ? 1.腾讯与山西文旅集团战略合作,共同打造“一部手机游山西” 8月27日,腾讯与山西省文化旅游投资控股集团有限公司(以下简称山西文旅集团)签署战略合作框...

    腾讯文旅
  • 【职场】112家IT公司薪水一览表

    作者是西电通院2013届毕业硕士,根据今年找工作的情况以及身边同学的汇总,总结各大公司的待遇如下,吐血奉献,公司比较全。以下绝对是各大公司2013届校招的数据,...

    小莹莹
  • 万字长文总结JAVA几种常见的编码格式和乱码原因分析

    编码问题一直困扰着开发人员,尤其在 Java 中更加明显,因为 Java 是跨平台语言,不同平台之间编码之间的切换较多。本文将向你详细介绍 Java 中编码问题...

    业余草
  • 深入分析 Java 中的中文编码问题

    不知道大家有没有想过一个问题,那就是为什么要编码?我们能不能不编码?要回答这个问题必须要回到计算机是如何表示我们人类能够理解的符号的,这些符号也就是我们人类使用...

    九州暮云
  • 【专业技术】Android webkit处理汉字编码问题

    在XX项目中解决android webkit处理汉字编码问题的总结 1.问题: 服务器通过302重定向方式发送给客户端重定向地址,地址中的汉字采用原数据方式发送...

    程序员互动联盟
  • python基础之字符编码

    计算机由美国人发明,最早的字符编码为ASCII,只规定了英文字母数字和一些特殊字符与数字的对应关系。最多只能用 8 位来表示(一个字节),即:2**8 = 25...

    py3study
  • unicode、utf-8、ansi、gbk、gb2312编码详解

    在开始本文前,我需要大家思考一个问题:你知道联通为什么干不过移动吗? 我们来看看微软站在哪边吧,用记事本写下联通两个字:

    tnt阿信
  • 定位bug的思路

    做测试这一行,总有一道绕不过去的坎就是定位bug,这其实是非常花费时间的。也许有很多人不以为然,觉得无非就是发现bug后提交bug管理系统,描述操作步骤,预期结...

    Meccer
  • Python爬虫的一次提问,引发的“乱码”问题

    近日,有位小伙伴向我请教,在爬取某网站时,网页的源代码出现了中文乱码问题。之前关于爬虫乱码有很多粉丝的各式各样的问题,今天恋习Python与大家一起总结下关于网...

    一墨编程学习

扫码关注云+社区

领取腾讯云代金券