在字符集这一篇文章中,我们基本了解了字符集的一些概念,也知道了什么是编码,什么是编码什么是解码。那么接下来我们就聊聊乱码。
复习:
我们知道计算机是不能直接存储字母,数字,图片,符号等,计算机能处理和工作的唯一单位是"比特位(bit)",一个比特位通常只有 0 和 1,是(yes)和否(no),真(true)或者假(false)等等我们喜欢的称呼。利用比特位序列来代表字母,数字,图片,符号等,我们就需要一个存储规则,不同的比特序列代表不同的字符,这就是所谓的"编码"。反之,将存储在计算机中的比特位序列(或者叫二进制序列)解析显示出来成对应的字母,数字,图片和符号,称为"解码"。
Panda丹(My wife):老公,那我知道什么是乱码了!!就是对一些字符以错误的规则去解码,然后解出来了我们看不懂的字符,这些字符就是乱码了!
Panda诚:说的对,当然,我们正常所说的乱码就是我们以肉眼可见的那些混乱字符,我们见过最多的可能就是问号(?)了,就我们中国人来说,汉字乱码是最常见的问题了。你刚才说的那种乱码方式就是最常见的一种原因,比较标准的说法就是编码解码采用了不同的标准,乱码产生的根源一般情况下可以归结为三方面即:编码引起的乱码、解码引起的乱码以及缺少某种字体库引起的乱码(这种情况需要用户安装对应的字体库),其中大部分乱码问题是由不合适的解码方式造成的。
Panda丹:但很奇怪的是,为啥乱码里那些英文字母都是正常的,而汉字却是乱码的?
Panda诚:还记得上一篇字符集里我们说的ASCLL嘛?英文字母都在这里,而一般上,我们用到的字符集都是对ASCLL的扩展,所以那些英文字母确实是正常的。
举个例子,汉字编码的第一个标准--GB2312:
在汉字编码的第一个标准里我们规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(称之为高字节)从0xA1(10100001)用到 0xF7(11110111),后面一个字节(低字节)从0xA1到0xFE(11111110),这样我们组合出了6763个简体汉字。说个题外话:在这个编码中,我们还同时还把ASCII里有的字符重新用两字节编码了一次,我们称之为全角,而原来在127号以下的那些就叫半角。例如ABCD这些是半角,ABCD这些则是全角。
Panda丹:哦,原来全角就是字母和数字等与汉字占等宽位置的字,用了这么多年的输入法,我还真是没有在意。
。。。
Panda诚(其实我也是最近看文章才看到的):那是当然,所谓一日不学习,老大徒伤悲...人生漫长,仍需多加努力吧。说到这里,我们就以一个开发的角度,分析一下常见的乱码原因。
常见乱码问题分析:
从编程角度来看,出现乱码的场景主要是有文本处理的时候,比如文件的新建和读取、复制和粘贴,导入和导出,打开和保存,数据存储和检索,显示,打印,分词处理,字符转换,规范化,搜索,整理和发送数据等,文本数据的示例包括平面文件,流文件,数据区域,目录名称,资源名称,用户标识等。以下 是出现乱码的一个常见场景分类。
I/O 操作包括读(输入)写(输出)两方面,而所谓的输入和输出是以程序为中心的,数据流向程序即输入流,数据从程序中流出即输出流。读数据比如将文件中的内容显示出来,即字节-->字符的转换,也就是解码;写数据比如创建一个新文件,即字符-->字节转换,也就是编码;在分析 I/O 操作中出现乱码原因之前,先简要概述一下 Java I/O 操作接口。如下图所示:
当我们想创建一个文件并且将对应的字符写入文件时将用到字节流 FileOutputStream和字符流 Writer,其流程为下图所示:
Java 中与 I/O 操作相关的 API 一般都有是否指定字符集的重载形式,选择不指定字符集形式的函数时将使用默认字符集。如 String.getBytes()就有两种形式:String.getBytes() 和 String.getBytes(String charsetName)。下面是 String.getBytes()方法的详解。
代码语言:javascript复制String.getBytes():
Encodes this String into a sequence of bytes using the
platform's default charset, storing the result into a
new byte array.
这个是 Java 帮助文档提供的解释,这里需要强调一下"The platform's default charset"即 Charset.defaultCharset(),defaultCharset 由系统属性 file.encoding 决定,如果用户没有设置 JVM 的这个属性,其值依赖于启动该 JVM 的环境编码:比如是由操作系统命令行启动 JVM,则有操作系统的运行时的区域语言设置决定的编码;比如是在 Eclipse 里面启动 JVM,可以设置 JVM 的这个属性,默认情况下 file.encoding 属性由通用设置页面的编码决定。
下面我们来看几个String.getBytes()简单的示例:
这里我用GBK编码保存了一个文本文件
在单元测试类中执行如下代码去以默认编码去读取这个文件内容:
代码语言:javascript复制 @Test
public void testRead() throws IOException {
System.out.println("默认编码是:" Charset.defaultCharset());
File file =new File(filePath);
FileInputStream inputStream = new FileInputStream(file);
byte[] content = new byte[1000];
inputStream.read(content);
System.out.println(new String(content));
}
执行结果如下,可以看到文件内容乱码了。
在单元测试类中执行如下代码去以默认编码写入文件内容:
代码语言:javascript复制 @Test
public void test() throws IOException {
String utf_8 = "nPanda诚:我是一串UTF-8字符串,这一行是我写的UTF-8编码数据";
File file =new File(filePath);
FileOutputStream outPutStream = new FileOutputStream(file, true);
//使用默认编码
outPutStream.write(utf_8.getBytes());
}
结果我们看到了,乱码了!
如下、我们再添加一行指定编码的写入方式:
代码语言:javascript复制@Test
public void test() throws IOException {
String utf_8 = "nPanda诚:我是一串UTF-8字符串,这一行是我写的UTF-8编码数据";
File file =new File(filePath);
FileOutputStream outPutStream = new FileOutputStream(file, true);
//使用默认编码
outPutStream.write(utf_8.getBytes());
//使用GBK
outPutStream.write(utf_8.getBytes("GBK"));
}
结果指定了GBK编码的方式写入就不乱码了
强调:为了避免乱码问题出现,在调用 I/O 操作相关的 API 时,最好使用带有指定字符集参数的重载形式。
Web 程序中出现的乱码情况:
在 web 应用程序中,存在用户输入以及输出显示的地方都有可能存在编码解码,下图简要概括了 HTTP web 请求响应环节。
下面是对上图的几点说明:
Web 应用程序中出现乱码的可能原因有:
- 浏览器本身没有遵循 URI 编码规范;
- 服务器端没有正确配置编码解码;
- 开发人员对 Web 程序中涉及到的编码解码理解不太深入。
HTTP Get 请求方式中的编码解码规则:
Get 请求方式中请求参数会被附加到地址栏的 URL 之后,URL 组成:
代码语言:javascript复制"域名:端口/contextPath/servletPath/pathInfo?queryString",
URL 中 pathInfo 和 queryString 如果含有中文等非 ASCII 字符,则浏览器会对它们进行 URLEncode,编码成为 16 进制,然后从右到左,取 4 位(不足 4 位直接处理),每 2 位做一位,前面加上%,编码成%XY 格式。
然而 URL 中的 PathInfo 和 QueryString 字符串的编码和解码是由浏览器和应用服务器的配置决定,在我们的程序中是不能设定的。即使同一浏览器对 pathInfo 和 queryString 的编码方式有可能不一样,因为浏览器对 URL 的编码格式是可设置的,这就对服务器的解码造成很大的困难。
应用服务器端对 Get 请求方式解码中 pathInfo 和 queryString 的设定是不同的。比如 Tomcat 服务器一般在 server.xml 中设定的,pathInfo 部分进行解码的字符集是在 connector 的 <Connector URIEncoding="UTF-8"/> 中定义的;QueryString 的解码字符集一般通过 useBodyEncodingForURI 设定,如果没有设定,Tomcat8 之前的版本默认使用的是 ISO-8859-1,但是 Tomcat 8 默认使用的是 UTF-8。
为了避免浏览器采用了我们不希望的编码,在我们的程序中最好不要在 URL 中直接使用非 ASCII 字符,而是对双字节字符进行 URI 编码后在放到 URL 中,JavaScript§提供了 encodeURI()函数,它提供的是 UTF-8 的 URI 编码,也可以通过 java.net.URLEncoder.encode(str,"字符集")进行编码。
HTTP Post 请求方式中的编码解码:
请求表单中的参数值是通过 request 包发送给服务器,此时浏览器会根据网页的 ContentType("text/html; charset=utf-8")中指定的编码进行对表单中的数据进行编码,然后发给服务器;
JSP 中 contentType 设定<%@ page language="java" contentType="text/html; charset="GB18030" pageEncoding="UTF-8"%>,JSP 页面命令中的 charset 的作用包括:
- 通知浏览器应该用什么编码方式解码显示网页;
- 提交表单时浏览器会按 charset 指定的字符集编码数据(post body)发送给服务器;
- pageEncoding 属性里指定的编码方式是储存该 jsp 文件时所用的编码,比如 eclipse 的文本编辑器可以根据该属性决定储存该文件时采用的编码方式;
- 服务器端通过 Request.setCharacterEncoding() 设置编码,然后通过 request.getParameter 获得正确的数据。
浏览器显示:通常有 JSP 和 HTML 来展示,通过实验发现,对于网页中的静态内容,不同浏览器显示网页所使用的字符集原则是不一样的,Chrome 63 和 IE11 使用 JSP 页面命令中 contentType 和 charset 设置,html 页面中的 charset 设置,然而 firefox 52 却根据自己的 text encoding 方式来显示页面。
对于 JSP:通过 JSP 页面命令<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>中的 contentType 属性和 pageEncoding 属性设置。在 JSP 标准的语法中,如果 pageEncoding 属性存在,储存该 jsp 文件时所用的编码由该属性决定,如果没有指定 pageEncoding 属性,那么存储该 jsp 文件的编码就由 contentType 属性中的 charset 决定,如果 charset 也不存在,JSP 页面的字符编码方式就采用默认的 ISO-8859-1;charset 的作用包括通知浏览器应该用什么编码方式解码显示网页,如果没有指定 charset 默认的字符集为"ISO-8859-1";提交表单时浏览器会按 charset 指定的字符集编码数据(post body)发送给服务器;Post 请求时,浏览器会根据 contentType 中 charset 指定的字符集对表单中的数据进行编码,然后提交给服务器。
对于 HTML: <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">,其中的 charset 左右和 JSP 中的 charset 作用一样。
对于动态页面内容:览器根据 http 头中的 ContentType("text/html; charset=utf-8")指定的字符集来解码服务器发送过来的字节流。在应用服务器端可以调用 HttpServletResponse.setContentType()设置 http 头的 ContentType,即服务器端编码方式。
数据库操作过程中的乱码:
在实际应用中,和数据库操作相关的乱码可能出现在数据的导入和导出操作中,在整个过程中涉及到的字符集有服务器端数据库字符集、客户端操作系统字符集、客户端环境变量 nls_lang(lang_territory.charset),这三个参数的工作流程如图 15 所示。如果这三个参数设置一样,整个数据库操作中就不会出现乱码问题,但是实际应用中客户端的情况复杂多样,很难保持三者一致,涉及到双字节字符就需要服务器端进行转码操作,而转码的桥梁就是 Unicode 字符集,这就要求数据库本身支持 UTF-8 编码方式。为了编码数据库操作过程中的乱码问题,在创建数据库的时候使用 UTF-8 编码方式,如果仅在某些列中使用多语言数据,则可以使用 SQL NCHAR 数据类型(NCHAR,NVARCHAR2 和 NCLOB)以 UTF-16 或 UTF-8 编码形式存储 Unicode 数据,避免存储空间的浪费。
接下来的文章,会对数据库的编码乱码问题进一步进行研究。
参考:
https://www.ibm.com/developerworks/cn/java/analysis-and-summary-of-common-random-code-problems/index.html
https://www.zhihu.com/question/22680300
https://baike.baidu.com/item/全角/9323113#3