常见乱码问题及分析

背景

有一个现成的古老的web项目,技术栈包括JSP JAVA MYSQLMyEclipse 10.5作为开发工具,开发环境JDK1.7 Tomcat7 Mysql5.5

现在项目能够部署成功,正常访问。

但是当提交的表单数据中包含中文时,数据库存储的相关数据显示为???。

因为这个BUG,诞生这篇小结。

朋克风

常见乱码问题分析和总结

本节内容选自IBM Developer : 常见乱码问题分析和总结

编码与解码

计算机不能直接存储字母,数字,图片,符号等,计算机能处理和工作的唯一单位是”比特位(bit)”,一个比特位通常只有 0 和 1,是(yes)和否(no),真(true)或者假(false)等等我们喜欢的称呼。利用比特位序列来代表字母,数字,图片,符号等,我们就需要一个存储规则,不同的比特序列代表不同的字符,这就是所谓的”编码”。反之,将存储在计算机中的比特位序列(或者叫二进制序列)解析显示出来成对应的字母,数字,图片和符号,称为”解码”,如同密码学中的加密和解密,下面将详细解释编码解码过程中涉及到的一些术语:

字符集合(Character set):是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等,简单理解就是一个字库,与计算机以及编码无关。

字符编码集(Coded character set):是一组字符对应的编码(即数字),为字符集合中的每一个字符给予一个数字,如 Unicode 为每一个字符分配一个唯一的码点与之一一对应。

字符编码(Character Encoding):简单理解就是一个映射关系,将字符集对应的码点映射为一个个二进制序列,从而使得计算机可以存储和处理。常见的编码方式有 ASCII 编码、ISO-8859-1(不支持中文)、GBK、GB2312(中国编码,支持中文)、UTF-8 等等,详情见表 1。

字符集(Charset):包括编码字符集合和字符编码,如 ASCII 字符集、ISO-8859-X、GB2312 字符集(简中)、BIG5 字符集(繁中)、GB18030 字符集、Shift-JIS 等,即下文中提到的字符集。

笔者注:
这里提到的字符集合与字符集是不同的概念,后者包含前者。
一般乱码现象都是因为中文数据导致的,汉字占用双字节,所以支持中文的编码方式(GBK、UTF-8等)也就是可以支持对双字节汉字进行编码。

字符解码(Character Decoding): 根据一定规则,将二进制序列映射成对应的正确字符串,即二进制序列–>字符串,个人将其理解为”翻译”。

Code Point:称作码点或码位,是组成编码空间(或代码页)的数值。例如,ASCII 码包含 128 个码点,范围是 0 到 7F(16 进制);ISO-8859-1 包含 256 个码点,范围是 0 到 FF;而 Unicode 包含 1,114,112 个码点,范围是 0 到 10FFFF。Unicode 码空间划分为 17 个 Unicode 字符平面(基本多文种平面,16 个辅助平面),每个平面有 65,536(= 216)个码点。因此 Unicode 码空间总计是 17 x 65,536 = 1,114,112。

Code Page:代码页或者内码表,是 IBM 早期称呼计算机的 BIOS 所支持的字符集编码(也称作 OEM 代码页)。Windows 系统在没有使用 UTF-16 之前,为了解决由于不同国家和地区采用的字符集不一致,很可能出现无法正常显示所有字符的问题,使用了代码页(Codepage)转换表的技术来过渡性的部分解决这一问题,即定义了一系列支持不同国家和地区所制定的字符集,被称为 Windows 代码页或 ANSI 代码页,然而代码页一般与其所对应的字符集之间并非完全相同,有时候会对字符集有所扩展,可以理解为一张字符-字节序列映射表,通过查表实现编码解码功能。操作系统中不同 Locale 设置默认使用不同的代码页。

Locale:是指特定于某个国家或地区的一组设定,包括代码页,以及数字、货币、时间和日期的格式等。在 Windows 内部,有两个 Locale 设置:系统 Locale 和用户 Locale。系统 Locale 决定代码页,用户 Locale 决定数字、货币、时间和日期的格式。

乱码产生原因

乱码产生的根源一般情况下可以归结为三方面即:编码引起的乱码、解码引起的乱码以及缺少某种字体库引起的乱码(这种情况需要用户安装对应的字体库),其中大部分乱码问题是由不合适的解码方式造成的,如图 3 所示的鱼骨图。

图 3. 乱码产生原因

乱码产生原因

从普通用户角度分别阐述这几种原因导致的乱码表象和解决办法

图 5. 编码方式引起的乱码剖析

图 5. 编码方式引起的乱码剖析

图 6. 不正确的解码方式引起的乱码

图 6. 不正确的解码方式引起的乱码

笔者注:
乱码有很多形式。
"??"通常是由于编码引起的乱码,出现?说明原始字符的信息已经丢失,这种情况下形成的乱码是不可逆的,也就是说无论用什么解码方式都不能正确显示字符。
"ÄãºÃ£¬Öйú"一般由解码方式不对引起的,乱码可逆,即可以通过乱码恢复获取原始的字符信息。

从编程角度分析出现乱码的场景和解决办法

从编程角度来看,出现乱码的场景主要是有文本处理的时候,比如文件的新建和读取、复制和粘贴,导入和导出,打开和保存,数据存储和检索,显示,打印,分词处理,字符转换,规范化,搜索,整理和发送数据等,文本数据的示例包括平面文件,流文件,数据区域,目录名称,资源名称,用户标识等。图 9 是出现乱码的一个常见场景分类。

图 9. 出现乱码的场景

出现乱码的场景

Web 程序中出现的乱码情况

在 web 应用程序中,存在用户输入以及输出显示的地方都有可能存在编码解码,图 12 简要概括了 HTTP web 请求响应环节。

图 12. Web 请求响应环节中的编码解码

请求响应环节中的编码解码

数据库操作过程中的乱码

在实际应用中,和数据库操作相关的乱码可能出现在数据的导入和导出操作中,在整个过程中涉及到的字符集有服务器端数据库字符集、客户端操作系统字符集、客户端环境变量 nls_lang(lang_territory.charset),这三个参数的工作流程如图 15 所示。如果这三个参数设置一样,整个数据库操作中就不会出现乱码问题,但是实际应用中客户端的情况复杂多样,很难保持三者一致,涉及到双字节字符就需要服务器端进行转码操作,而转码的桥梁就是 Unicode 字符集,这就要求数据库本身支持 UTF-8 编码方式。为了编码数据库操作过程中的乱码问题,在创建数据库的时候使用 UTF-8 编码方式,如果仅在某些列中使用多语言数据,则可以使用 SQL NCHAR 数据类型(NCHAR,NVARCHAR2 和 NCLOB)以 UTF-16 或 UTF-8 编码形式存储 Unicode 数据,避免存储空间的浪费。

图 15. Oracle 数据库字符集

Oracle 数据库字符集

修改Windows的默认编码

在Windows设置-时间与语言-语言-管理语言设置-更改系统区域设置中进行修改。

联想到一个现象:通过执行startup.bat文件开启tomcat服务器时,打印的启动日志出现乱码。

通过查询cmd的编码格式,并修改为utf-8编码格式,理论上能够解决上述乱码问题。实际上,执行chcp 65001,只是将当前cmd窗口的编码设置为utf-8。执行startup.bat时,会另外启动一个cmd进程,所以打印服务器启动日志的那个cmd窗口还是会有乱码。(除非从操作系统层面更改编码格式)

//查看当前cmd的编码格式
//character code page
chcp

//换成utf-8代码页
chcp 65001

//换成默认的gbk
chcp 936

小结

现在回过头来想背景中的问题。??的出现有可能是字符的编码方式不对,导致通过任何解码方式都读不出正确的字符。那么在哪个环节出现了这个问题呢?通过测试,表单数据提交到服务端,服务端获取数据并打印,显示正常。说明在浏览器到web服务端的编码解码方式是OK的。那么再来,就是java程序代码中将正常的字符数据存入数据库时发生了问题,导致数据库中数据显示为??。于是考虑中文存入数据库的编码方式出了问题。事实也应该就是这样的。

【问题解决】java连接数据库的代码中,DBURL设置为jdbc:mysql://localhost:3306/XXX?useUnicode=true&characterEncoding=UTF-8。重点就是后边的characterEncoding参数。

字符数据在二进制与可识读形式之间来回切换跳转,编码解码的步骤都是循环重复的。博大精深的中国汉字,到底在代码的世界里也很独特。

尽管问题解决,原因大概分析了,但真实的情形又是怎样的呢?谁又真正的可知?如果能够化身数据流,以数据本身的角度来看浏览器到数据库的往返之路,这该是很奇妙的想象与体验吧。

hacker