第3章 Oracle的字符集(3.7-3.8)
Oracle全球支持(即Globalization Support)允许我们使用本地语言和格式来存储和检索数据。通过全球支持,Oracle可以支持多种语言及字符集,得以展示数据库的强大魅力。这篇介绍第三章的3.7-3.8节,使用csscan辅助字符集转换、乱码的产生、“靠”字的诱惑。
由于不同语言及字符集的共同存储存在设置上具有一定的复杂性,字符集一度成为普遍困扰大家的一个主要问题。
本章就字符集一些常见问题进行讨论,并对字符集转化等本质内容进行探索。
3.7 使用csscan辅助字符集转换
如果要确保数据的完整性,应该使用csscan扫描数据库,找出所有不兼容的字符,然后通过编写相应的脚本及代码,在转换之后进行更新,确保数据的正确性。
简单看一下csscan的使用。要使用csscan之前,需要以sys用户身份创建相应数据字典对象:
E:\nls2>sqlplus "/ as sysdba" Connected to: Oracle9i Enterprise Edition Release 9.2.0.4.0 - Production With the Partitioning, Oracle Label Security, OLAP and Oracle Data Mining options JServer Release 9.2.0.4.0 - Production SQL> select instance_name from v$instance; INSTANCE_NAME ---------------- eygle SQL> @?/rdbms/admin/csminst.sql User created. Grant succeeded. …………
这个脚本创建相应用户(csmig)及数据字典对象,扫描信息会记录在相应的数据字典表里。可以在命令行调用这个工具对数据库进行扫描:
E:\nls2>csscan FULL=Y FROMCHAR=ZHS16GBK TOCHAR=US7ASCII LOG=US7check.log CAPTURE=Y ARRAY=1000000 PROCESS=2 Character Set Scanner v1.1 : Release 9.2.0.1.0 - Production on Sun Nov 2 20:24:45 2003 Copyright (c) 1982, 2002, Oracle Corporation. All rights reserved. Username: eygle/eygle Connected to: Oracle9i Enterprise Edition Release 9.2.0.4.0 - Production With the Partitioning, Oracle Label Security, OLAP and Oracle Data Mining options JServer Release 9.2.0.4.0 - Production Enumerating tables to scan... . process 1 scanning SYS.SOURCE$[AAAABHAABAAAAIRAAA] . process 2 scanning SYS.ATTRIBUTE$[AAAAEoAABAAAAhZAAA] . process 2 scanning SYS.PARAMETER$[AAAAEoAABAAAAhZAAA] . process 2 scanning SYS.METHOD$[AAAAEoAABAAAAhZAAA] …….. . process 2 scanning SYSTEM.DEF$_AQERROR[AAAA8fAABAAACWJAAA] . process 1 scanning WMSYS.WM$ENV_VARS[AAABeWAABAAAFMZAAA] …………………. . process 2 scanning SYS.UGROUP$[AAAAA5AABAAAAGpAAA] . process 2 scanning SYS.CON$[AAAAAcAABAAAACpAAA] . process 1 scanning SYS.FILE$[AAAAARAABAAAABxAAA] Creating Database Scan Summary Report... Creating Individual Exception Report... Scanner terminated successfully. 然后可以检查输出的日志来查看数据库扫描情况: Database Scan Individual Exception Report [Database Scan Parameters] Parameter Value ------------------------------ ------------------------------------------------ Scan type Full database Scan CHAR data? YES Current database character set ZHS16GBK New database character set US7ASCII Scan NCHAR data? NO Array fetch buffer size 1000000 Number of processes 2 Capture convertible data? YES ------------------------------ ------------------------------------------------ [Data Dictionary individual exceptions] [Application data individual exceptions] User : EYGLE Table : TEST Column: NAME Type : VARCHAR2(10) Number of Exceptions : 1 Max Post Conversion Data Size: 4 ROWID Exception Type Size Cell Data(first 30 bytes) ------------------ ------------------ ----- ------------------------------ AAABpIAADAAAAAMAAA lossy conversion 测试 ------------------ ------------------ ----- ------------------------------
不能转换的数据将会被记录下来,然后可以根据这些信息在转换之后,对数据进行相应的更新,确保转换无误。
3.7 乱码的产生
最后我们来讨论一下乱码的产生。通常在现实环境中,存在3个字符集设置:
· 客户端应用字符集(Client Application Character Set);
· 客户端NLS_LANG参数设置;
· 服务器端,数据库字符集(Character Set)设置。
由于一个字符在客户端应用(如SQLPLUS、CMD、NOTEPAD等)中以怎样的字符显示取决于客户端操作系统,客户端能够显示怎样的字符,我们就可以在应用中录入这些字符。至于这些字符能否在数据库中正常存储,就和另外的两个字符集设置紧密相关了(通常我们可以忽略应用程序的字符集,这个字符集在应用程序安装时,已经被内在的决定,并且会依据操作系统的相关设置进行选择)。
在传输过程中,客户端NLS_LANG主要用于进行转换判断。如果NLS_LANG等于数据库字符集,则不进行任何转换直接把字符插入数据库;如果不同则进行转换,转换主要有两个任务:
· 如果存在对应关系,则把相应二进制编码经过映射后(这一步映射以后,所代表的字符可能发生转换)传递给数据库。
· 如果不存在对应关系,则传递一个替换字符(不同平台的替换字符各不相同,最常见的替换字符是“?”)。
数据库字符集,在和客户端NLS_LANG不同时,会对经过NLS_LANG转换的字符进一步处理:对于?(即不存在对应关系的字符)直接以?形式存放入数据库,对于其他字符,在NLS_LANG和数据库字符集之间进行转换后存入。
下面来看一下最为常见的字符集及乱码的产生。
1.NLS_LANG字符集与数据库字符集不同
当NLS_LANG字符集与数据库字符集不同,且NLS_LANG不同于客户端字符集设置时,存在以下两种可能。
· 客户端输入的字符在NLS_LANG中没有对应的字符,这时无法转换,NLS_LANG使用替换字符替代这些无法映射的字符(这一步转换在TTS中完成),在很多字符集中这个替代字符就是“?”。
· 当客户端的字符在NLS_LANG中对应了不同的字符时,传递给数据库以后发生转换,存储的是字符,但是已经丢失了元数据,数据库中的字符不再代表客户端的输入。而且这个过程不可逆,这也就是为什么很多时候在客户端输入的是正常的编码,而查询之后会得到未知字符的原因。
下面通过图3-9来简单说明一下这个过程,当客户端在WE8ISO8859P15字符集时,输入欧元符号€,这时客户端NLS_LANG和数据库端字符集不同,进行第一次转换,客户端€符号编码是A4,在NLS_LANG转换时,A4对应了NLS_LANG中的“¤”,这一步的转换产生了错误映射。由于数据库字符集不同于NLS_LANG设置,这时进一步的转换发生了,存入数据库的编码变成了C2A4,虽然同NLS_LANG进行了正确的转换,但是客户端录入的数据已经损坏或者丢失了。
图3-9 字符集转换实例
可以用我们熟悉的字符集做一个简单的测试(测试环境是客户端代码点对应中文18030字符集,NLS_LANG设置为US7ASCII字符集,数据库
CHARACTER SET为ZHS16GBK)。 c:\>set NLS_LANG=AMERICAN_AMERICA.US7ASCII c:\>sqlplus eygle/eygle Connected to: Oracle9i Enterprise Edition Release 9.2.0.4.0 - Production With the Partitioning, Oracle Label Security, OLAP and Oracle Data Mining options JServer Release 9.2.0.4.0 - Production SQL> insert into test values('测试'); 1 row created. SQL> select name,dump(name) from test; NAME DUMP(NAME) -------------------------------------------------- 2bJT Typ=1 Len=4: 50,98,74,84
这时发现,查询出来的是混乱的字符,把这些字符转换为二进制就是:
110010 1100010 1001010 1010100
补全8位就是如下序列:
00110010 01100010 01001010 01010100
我们把首位换成1,得到如下序列:
10110010 11100010 11001010 11010100
接下来看正确的存储格式:
c:\>set nls_lang=AMERICAN_AMERICA.ZHS16GBK c:\>sqlplus eygle/eygle Connected to: Oracle9i Enterprise Edition Release 9.2.0.4.0 - Production With the Partitioning, Oracle Label Security, OLAP and Oracle Data Mining options JServer Release 9.2.0.4.0 - Production SQL> insert into test values('测试'); 1 row created. SQL> col dump(name) for a30 SQL> select name,dump(name) from test; NAME DUMP(NAME) ---------- ------------------------------ 测试 Typ=1 Len=4: 178,226,202,212
把这个结果转换为二进制表示:
10110010 11100010 11001010 11010100
这个结果正是前面乱码首位补全1后的结果。这个测试说明在US7ASCII转换中文的时候除去了首位的1,这样就丢失了元数据,导致乱码出现,NLS_LANG的转换作用由此可加一斑!
2.NLS_LANG和数据库字符集相同时
在这种情况下,数据库端对客户端传递过来的编码不进行任何转换(这样可以提高性能),直接存储进入数据库,那么这时候就存在和上面同样的问题,如果客户端传递过来的字符集在数据库中有正确的对应就可以正确存储,如果没有,就会被替换字符置换成,乱码就这样产生了。
如图3-10所示,当NLS_LANG和数据库字符集设置相同都为UTF8时,客户端的欧元符号的编码A4就不会经过任何转换就插入到数据库中,而在UTF8的数据库中,A4代表的是一个非法字符。
图3-10 字符集转换实例
来看一个简单的测试(测试环境是客户端代码点对应中文18030字符集,客户端NLS_LANG为US7ASCII,数据库字符集为US7ASCII)。
我们知道这个时候存入的数据,数据库不进行任何转换,在以下的测试中,看到中文在US7ASCII字符集下得以正确显示。
c:\>set nls_lang=AMERICAN_AMERICA.US7ASCII c:\>sqlplus eygle/eygle Connected to: Oracle9i Enterprise Edition Release 9.2.0.4.0 - Production With the Partitioning, Oracle Label Security, OLAP and Oracle Data Mining options JServer Release 9.2.0.4.0 - Production SQL> insert into test values('测试'); 1 row created. SQL> commit; Commit complete. SQL> select * from test; NAME ---------- 测试 SQL> col dump(name) for a30 SQL> select name,dump(name) from test; NAME DUMP(NAME) ---------- ------------------------------ 测试 Typ=1 Len=4: 178,226,202,212 SQL> select * from nls_database_parameters; PARAMETER VALUE ------------------------------ ---------------------------------------- NLS_LANGUAGE AMERICAN NLS_TERRITORY AMERICA NLS_CURRENCY $ NLS_ISO_CURRENCY AMERICA NLS_NUMERIC_CHARACTERS ., NLS_CHARACTERSET US7ASCII NLS_CALENDAR GREGORIAN NLS_DATE_FORMAT DD-MON-RR NLS_DATE_LANGUAGE AMERICAN ………
3.8 “靠”字的困惑
讨论过乱码的产生原因之后,很多朋友可能会想起“靠”字的困惑。很多人可能都遇到过查询返回一连串“靠”字的经历,其根本原因同样是由于字符集的原因导致的乱码。
我们通过测试来研究一下这种情况,测试开始之前首先区分测试中将会涉及的几个字符即相关应用:
· 客户端应用字符集(Client Application Character Set);
测试客户端应用使用命令行工具(cmd.exe),这个工具的字符集决定查询结果在终端上的输出显示,当前命令行工具的字符代码页为936,对应的是GBK字符集:
图3-11 命令行工具的字符集
· 客户端NLS_LANG参数设置;
为了测试异常情况,我们设置NLS_LANG为AMERICAN_AMERICA.WE8ISO8859P1
D:\>SET NLS_LANG=AMERICAN_AMERICA.WE8ISO8859P1
· 服务器端,数据库字符集(Character Set)设置。
数据库的字符集为ZHS16GBK
首先在数据库上创建一个测试表,存储一点中文数据:
SQL> create table tcharset (name varchar2(20)); SQL> insert into tcharset values('循序渐进深入浅出'); SQL> commit; SQL> select * from tcharset; NAME -------------------- 循序渐进深入浅出
然后在客户端WE8ISO8859P1字符集下执行查询:
D:\>SET NLS_LANG=AMERICAN_AMERICA.WE8ISO8859P1 D:\>sqlplus eygle/eygle@eygle SQL*Plus: Release 10.2.0.3.0 - Production on Tue Jun 26 10:51:45 2007 Copyright (c) 1982, 2006, Oracle. All Rights Reserved. Connected to: Oracle9i Enterprise Edition Release 9.2.0.4.0 - Production With the Partitioning option JServer Release 9.2.0.4.0 - Production SQL> select * from tcharset; NAME -------------------- 靠靠靠靠
这里八个汉字被转换成了四个“靠”字输出显示?这是怎么回事呢?
首先我们知道,Oracle数据库服务器是传输代码给客户端的,数据本身不存在问题,编码会原样传输到客户端:
SQL> select name,dump(name) from tcharset; NAME -------------------- DUMP(NAME) -------------------------------------------------------------------------------- 靠靠靠靠 Typ=1 Len=16: 209,173,208,242,189,165,189,248,201,238,200,235,199,179,179,246
那么可以确认存在问题的只是中间发生的转换环节。
由于WE8ISO8859P1是8位的单Byte编码方案,所以中文汉字编码在其中不存在对应关系,也就是无法转换,此时WE8ISO8859P1字符集会使用一个替换字符来代替中文,这个替换字符是“¿”,也就是一个倒过来的“?”,不同字符集的替换字符,我们可以通过Locale Builder工具打开字符文件查看:
图3-12 字符集WE8ISO8859P1的替换字符
注意,这个特殊字符的编码为BF,那么也就是说,如果无法转换ZHS16GBK的8个中文字,WE8ISO8859P1将使用8个“¿”来替换,也就是说经过转换之后,有了8个BF的编码,那么我们再来看看8个BF在客户端的GBK字符集里代表了什么。
通过微软网站上的936代码页我们可以找到这个以下图表:
图3-13 GBK的字符编码页
936代码页的网址链接为:
http://www.microsoft.com/globaldev/reference/dbcs/936.htm
从上图中可以看到,其中BFBF正好代表汉字“靠”,于是8个BF最后展现出来就变成了4个“靠”字。我们也可以通过ZHS16GBK字符集文件来找到这个编码,两者是一致的:
图3-14 字符集的编码
这也就是不同字符集、应用之间转换会导致的字符集问题。