锟斤拷?UTF-8与GBK互转,为什么会乱码?

2023-11-06

作为一名程序员,肯定有被乱码困扰的时候,真到了百思不得其解的时候,就会觉得:英文程序员真幸福。

但其实只要明白编码之间的转换规律,其实乱码so easy~

 

我们知道,计算机存储数据都是2进制,就是0和1,那么这么多的字符就都需要有自己对应的0和1组成的序列,计算机将需要存储的字符转换成它们对应的01序列,然后就可以储存在电脑里了。

 

比如我们可以定义用8位2进制表示一个字符,“00000000”表示小写字母“a”,“00000001”表示小写字母“b”,那么计算机要存储“ab”的时候,其实在计算机里的存储的是“0000000000000001”,读取的时候先读取前8位,根据对应关系,可以解码出“a”,再读取后8位,又可以解码出“b”,这样就读出了当时写入的“ab”了。而我们定义的这种字符和二进制序列的对应关系,就可以称之为编码。我们如果需要将“ab”发送给别人,因为网络也是基于二进制,所以只要先约定好编码规则,就可以发送“0000000000000001”,然后对方根据约定的编码解码,就可以得到“ab”。现在是互联网的时代,我们经常需要和其他的计算机进行交互,一套编码系统还是比较复杂的,所以大家就需要约定通用的编码,这样的编码是大家都约定好的,通信时就不用再去约定编码规则了~然而,为了满足各种不同的需求,人们还是制定了很多种编码,没有哪一种能全面替代其他编码,所以现在多种编码并存。通常这些编码都被大家所接受和熟知,所以现在不用再通信前商量编码的对应规则和细节,只需要告诉对方,我采用的是什么通用编码,彼此就能愉快地通信了。

 

那么其实乱码的本质就是:读取二进制的时候采用的编码和最初将字符转换成二进制时的编码不一致。

 

ps:编码有动词含义也有名词含义,名词含义就是一套字符和二进制序列之间的转换规则,动词含义是使用这种规则将字符转换成二进制序列。

 

好了,废话不多,直接上一段代码:

 

import java.io.UnsupportedEncodingException;

public class EncodingTest {
	public static void main(String[] args) throws UnsupportedEncodingException {
		String srcString = "我们是中国人";
		String utf2GbkString = new String(srcString.getBytes("UTF-8"),"GBK");
		System.out.println("UTF-8转换成GBK:"+utf2GbkString);
		String utf2Gbk2UtfString = new String(utf2GbkString.getBytes("GBK"),"UTF-8");
		System.out.println("UTF-8转换成GBK再转成UTF-8:"+utf2Gbk2UtfString);
	}
}

 

因为UTF-8和GBK是两套中文支持较好的编码,所以经常会进行它们之间的转换,这里就以它们举例。

以上代码运行打印出以下内容:

 

UTF-8转换成GBK:鎴戜滑鏄腑鍥戒汉
UTF-8转换成GBK再转成UTF-8:我们是中国人

 

我们看到,将"我们是中国人"以UTF-8编码转换成byte数组(byte数组其实就相当于二进制序列了,此过程即编码),再以GBK编码和byte数组创建新的字符串(此过程即以GBK编码去解码byte数组,得到字符串),就产生乱码了。

因为编码采用的UTF-8和解码采用的GBK不是同一种编码,所以最后结果乱码了

之后再对乱码使用GBK编码,还原到解码前的byte数组,再使用和最初编码时使用的一致的编码UTF-8进行解码,就可得到最初的“我们是中国人”。

这种多余的转换有时候还是很有用的,比如ftp协议只支持ISO-8859-1编码,这个时候如果要传中文,只能先换成ISO-8859-1的乱码,ftp完成后,再转回UTF-8就又可以得到正常的中文了。

 

怎么样?编码转换是不是so easy?那该来点正经的了:

 

import java.io.UnsupportedEncodingException;

public class EncodingTest {
	public static void main(String[] args) throws UnsupportedEncodingException {
		String srcString = "我们是中国人";
		String gbk2UtfString = new String(srcString.getBytes("GBK"), "UTF-8");
		System.out.println("GBK转换成UTF-8:" + gbk2UtfString);
		String gbk2Utf2GbkString = new String(gbk2UtfString.getBytes("UTF-8"), "GBK");
		System.out.println("GBK转换成UTF-8再转成GBK:" + gbk2Utf2GbkString);
	}
}

这次我们反过来,先将字符串以GBK编码再以UTF-8解码,再以UTF-8编码,再以GBK解码。

 

这次的运行结果是:

 

GBK转换成UTF-8:��������й��
GBK转换成UTF-8再转成GBK:锟斤拷锟斤拷锟斤拷锟叫癸拷锟斤拷

 

WTF??万恶的“锟斤拷”,相信不少人都见过。这里GBK转成UTF-8乱码好理解,但是再转回来怎么变成了“锟斤拷锟斤拷锟斤拷锟叫癸拷锟斤拷”,这似乎不科学。

这其实和UTF-8独特的编码方式有关,由于UTF-8需要对unicode字符进行编码,unicode字符集是一个几乎支持所有字符的字符集,为了表示这么庞大的字符集,UTF-8可能需要更多的二进制位来表示一个字符,同时为了不致使UTF-8编码太占存储空间,根据二八定律,UTF-8采用了一种可变长的编码方式,即将常用的字符编码成较短的序列,而不常用的字符用较长的序列表示,这样让编码占用更少存储空间的同时也保证了对庞大字符集的支持。

正是由于UTF-8采用的这种特别的变长编码方式,这一点和其他的编码很不一样。比如GBK固定用两个字节来表示汉字,一个字节来表示英文和其他符号。

来测试一下:

import java.io.UnsupportedEncodingException;

public class EncodingTest {
	public static void main(String[] args) throws UnsupportedEncodingException {
		String srcString = "我们是中国人";
		byte[] GbkBytes = srcString.getBytes("GBK");
		System.out.println("GbkBytes.length:" + GbkBytes.length);
		byte[] UtfBytes = srcString.getBytes("UTF-8");
		System.out.println("UtfBytes.length:" + UtfBytes.length);
		String s;
		for (int i = 0; i < srcString.length(); i++) {
			s = Character.valueOf(srcString.charAt(i)).toString();
			System.out.println(s + ":" + s.getBytes().length);
		}
	}
}

运行结果为:

 

GbkBytes.length:12
UtfBytes.length:18
我:3
们:3
是:3
中:3
国:3
人:3

 

可以看到使用GBK进行编码,“我们是中国人”6个汉字占12个字节,而是用UTF-8进行编码则占了18个字节,其中每个汉字占3个字节(由于是常用汉字,只占3个字节,有的稀有汉字会占四个字节。)

UTF-8编码的读取方式也比较不同,需要先读取第一个字节,然后根据这个字节的值才能判断这个字节之后还有几个字节共同参与一个字符的表示。

对于某一个字符的UTF-8编码,如果只有一个字节则其最高二进制位为0;如果是多字节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的位数,其余各字节均以10开头。UTF-8最多可用到6个字节。 

如表: 
1字节 0xxxxxxx 
2字节 110xxxxx 10xxxxxx 
3字节 1110xxxx 10xxxxxx 10xxxxxx 
4字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 
5字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 
6字节 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 
因此UTF-8中可以用来表示字符编码的实际位数最多有31位,即上表中x所表示的位。除去那些控制位(每字节开头的10等),这些x表示的位与UNICODE编码是一一对应的,位高低顺序也相同。 
实际将UNICODE转换为UTF-8编码时应先去除高位0,然后根据所剩编码的位数决定所需最小的UTF-8编码位数。 
因此那些基本ASCII字符集中的字符(UNICODE兼容ASCII)只需要一个字节的UTF-8编码(7个二进制位)便可以表示。 

上面一随便看看就好,只要知道“由于UTF-8的特殊编码方式,所以有些序列是不可能出现在UTF-8编码中的”就可以了。

 

所以当我们将由GBK编码的12个字节试图用UTF-8解码时会出现错误,由于GBK编码出了不可能出现在UTF-8编码中出现的序列,所以当我们试图用UTF-8去解码时,经常会遇到这种不可能序列,对于这种不可能序列,UTF-8把它们转换成某种不可言喻的字符“�”,当这种不可言喻的字符再次以UTF-8进行编码时,他们已经无法回到最初的样子了,因为那些是UTF-8编码不可能编出的序列。然后这个神秘字符再转换成GBK编码时就变成了“锟斤拷”。当然,还有很多其他的巧合,可能正好碰到UTF-8中存在的序列,甚至原本不是一个字符的字节,可能是某个字的第二个字节和下一个字的两个字节,正好被识别成一个UTF-8序列,于是解码出一个汉字,当然这些在我们看来都是乱码了,只不过不是“锟斤拷”的样子。因为不可能序列更普遍存在,所以GBK转UTF-8再转GBK时,最常见的便是“锟斤拷”!

� \xef\xbf\xbd

锟 \xef\xbf

斤 \xbd\xef 

拷 \xbf\xbd

可以看到UTF-8编码下的两个“��”和GBK编码下的“锟斤拷”的字节编码相同,都是 \xef\xbf\xbd\xef\xbf\xbd

所以:以非UTF-8编码编码出的字节数组,一旦以UTF-8进行解码,通常这是一条不归路,再尝试将解码出的字符以UTF-8进行编码,也无法还原之前的字节数组。

相反地,其他的固定长度编码几乎都可以顺利还原。

乱码解决方式:

出现“锟斤拷”说明在字节和字符的转换(编码和解码)过程中使用了不同的编码,找出编解码的代码,修改成使用同一种编码方式即可。

=====================补充==========================

上文中其实有一个东西一直在回避,就是既然所有字符在保存时都需要转换成二进制,那么java是使用什么编码来保存字符的呢?这个问题其实我们可以不必深究,因为这对我们是透明的,我们只要假设java使用某种编码可以表示所有字符。得益于这种透明,我们可以当作java是直接保存字符本身的,就如上文所做的这样。

java虚拟机中使用unicode字符集。

java虚拟机中使用UTF-16编码方式。

 

 

 

 

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

锟斤拷?UTF-8与GBK互转,为什么会乱码? 的相关文章

随机推荐

  • 智能合约相关设计

    1 运行环境 以太坊采用以太坊虚拟机作为智能合约的运行环境 以太坊虚拟机是一个隔离的轻量级虚拟机环境 运行在其中的智能合约代码无法访问本地网络 文件系统或其他进程 对同一个智能合约 查看什么是智能合约 来说 往往需要在多个以太坊虚拟机中同时
  • 详细实现最短路径(迪杰斯特拉算法)

    最短路径 说白了 就是图里从一个顶点到另一个顶点的最小权值之和 今天 小编带大家一起用迪杰斯特拉 Dijkstra 算法实现它吧 目录 一 实现原理 二 代码实现 一 思路 二 代码 一 实现原理 其实 在小编看来 迪杰斯特拉算法与普里姆算
  • ws协议与http协议的异同

    http协议 识别数据内容 与webSocket协议 同 建立在TCP之上 同http一样通过TCP来传输数据 不同 HTTP协议为单向协议 即浏览器只能向服务器请求资源 服务器才能将数据传送给浏览器 而服务器不能主动向浏览器传递数据 分为
  • Selenium及chromedriver安装教程

    文章目录 安装Python环境及Selenium工具包 使用命令行安装 使用Pycharm安装 安装chromedriver驱动 验证 安装Python环境及Selenium工具包 首先 我们需要安装Python环境 安装好了之后需要安装S
  • keras IMDB数据集 LSTM分类

    在keras提供的IMDB数据集中 word被映射为一个大于0的整数 表示该单词出现频率的排名 这样处理的目的是为了方便按照词频过滤单词 其中0用于表示unknown word 载入数据 x train shape 25000 是一个250
  • 如果IBM再给我一次实习机会

    2014年 我拿到了IBM斯图加特R D的实习机会 在连续被索尼和博世拒掉之后 这个实习对我来说弥足珍贵 我学的是通信专业 在这之前与编程相关的活动只有一学期的安卓Lab 还是靠抱队友大腿才及格 在申请时 我的编程能力可以说几乎为0 连我自
  • java: 找不到符号 符号: 类 ResourceVO 位置: 类 com.

    一 java找不到符号 如果你的代码里没有任何问题 但是java报错找不到符号 如下 解决方法
  • 《Pytorch深度学习和图神经网络(卷 2)》学习笔记——第二章

    基于图片内容的处理任务 主要包括目标检测 图片分割两大任务 目标检测 精度相对较高 主要是以检测框的方式 找出图片中目标物体所在坐标 模型运算量相对较小 相对较快 图片分割 精度相对较低 主要是以像素点的集合方式 找出图片中目标物体边缘的具
  • Prometheus 监控之 kafka

    初探 默认情况下 Kafka metrics 所有的 metric 都可以通过 JMX 获取 暴露kafka metrics 支持两种方式 1 在 Kafka Broker 外部 作为一个独立进程 通过 JMX 的 RMI 接口读取数据 这
  • linux:需要注意docker和aws的rds的mysql默认是UTC而不是中国时区

    问题 如题 解决办法 docker参考 mysql时间不对 修改时区 set global time zone 无效 小书生 的博客 CSDN博客 aws参考 https www youtube com watch v B NaqV A1B
  • 数字IC手撕代码--联发科(总线访问仲裁)

    题目描述 当A B两组的信号请求访问某个模块时 为了保证正确的访问 需要对这些信号进行仲裁 请用Verilog实现一个仲裁器 对两组请求信号进行仲后 要求 协议如图所示 请求方发送req request 信号1表示有请求给仲裁器 仲裁器响应
  • es6扩展运算符 (...)

    es6的扩展运算符就是取出参数对象中的所有可遍历属性 拷贝到当前对象之中 let bar a 1 b 2 let baz bar a 1 b 2 实际上是通过Object assign方法实现的 let baz Object assign
  • 【React】路由懒加载 React.lazy()

    React路由懒加载lazy 文章目录 React路由懒加载lazy React lazy 懒加载概念 React lazy 使用 React lazy 懒加载概念 我们在使用网站时 如果不对路由使用懒加载 则会导致刚打开网站就加载全部路由
  • spring security oauth2源码解析

    spring security oauth2源码解析 EnableResourceServer 启用资源服务配置 注入配置 ResourceServerConfiguration ResourceServerConfiguration 资源
  • 利用ThreadPoolTaskExecutor创建线程池,并实现数据透传

    1 线程池配置 Configuration public class ThreadPoolConfig cpu内核 暂时默认8核 private static final int CORE SIZE 8 核心线程数 暂定为I O密集型 pr
  • java 取出list 中一列_java8新特性 获取list某一列的操作

    提取某一列 以name为例 从对象列表中提取一列 以name为例 List nameList studentList stream map StudentInfo getName collect Collectors toList 提取后输
  • 大数据毕设项目 大数据电影数据分析与可视化系统 - python Django

    文章目录 0 前言 1 课题背景 2 效果实现 3 爬虫及实现 4 Flask框架 5 Ajax技术 6 Echarts 7 最后 0 前言 这两年开始毕业设计和毕业答辩的要求和难度不断提升 传统的毕设题目缺少创新和亮点 往往达不到毕业答辩
  • 字符串版本号比较(Java)

    APP的版本升级更新 会用到版本号的对比 根据版本号去解析埋点上报得信息 正则匹配方式解析版本号中字符和数字做对比 默认字符大于数字 版本号1 是否大于等于 版本号2 详见以下代码 Slf4j public class CompareUti
  • java修饰符权限

    java修饰符有public protected private和default 默认 四种访问级别 四类修饰符都用于类 类属属性及方法 1 访问权限 访问权限 类 包 子类 其他包 备注 public 可 可 可 可 包内及包外的任何类均
  • 锟斤拷?UTF-8与GBK互转,为什么会乱码?

    作为一名程序员 肯定有被乱码困扰的时候 真到了百思不得其解的时候 就会觉得 英文程序员真幸福 但其实只要明白编码之间的转换规律 其实乱码so easy 我们知道 计算机存储数据都是2进制 就是0和1 那么这么多的字符就都需要有自己对应的0和