第一部分 字符集与编码常识 字符集: 人们根据需要把某些字符收集到一处,并赋以名称,于是便有了某某字符集。 编码: 当 前面收集的工作完成以后,为了让只认识数字的“愚蠢”的计算机也能够存储字符, 人们不得不为 集合里的每一 个 字符分配 ” 身份证号
第一部分 字符集与编码常识
字符集:
人们根据需要把某些字符收集到一处,并赋以名称,于是便有了某某字符集。
编码:
当 前面收集的工作完成以后,为了让只认识数字的“愚蠢”的计算机也能够存储字符, 人们不得不为 集合里的每一 个 字符分配 ” 身份证号码 ” ,这就是编码,从此,终于可以以存储编码的方式在计算机中存储字符了。
在字符集与编码世界的漫漫历史长河里(伪),出现过若干个让计算机工作者们如雷贯耳的名字,这些名字,有些已经成了浮云飘散了,有些还在我们的代码中折腾。
ASCII :
ü ASCII 字符集 :包含大小写英文、阿拉伯数字、标点,以及一些不可见的控制符共 128 个。
ü ASCII 编码 :使用 7 位表示一个字符。 编码范围是 [0-127] (即 Hex[00-7F] ),其中 [0-31] ( Hex[00-1F] )部分以及 127 ( Hex7F )是控制符,其余的都是些可见字符。
GB2312 :
ü GB2312 字符集 : ASCII 字符集 +7000 左右汉字字符。
ü GB2312 编码 :兼容 ASCII 编码。对字节进行判断,如值 127 ,则意义等同于 ASCII 编码;如值 >127 ,则它需要跟其后的另一个字节合并表示一个字符。其理论汉字编码空间为 128X256 ,超过 3 万个字符。
GBK :
ü GBK 字符集 : GB2312 字符集 +20000 左右汉字字符。
ü GBK 编码 :兼容 GB2312 编码。利用了 GB2312 编码闲置的编码空间。
GB18030 :
ü GB18030 字符集 : GBK 字符集 + 若干汉字 + 若干少数民族字符,为目前国内最新的字符集。
ü GB18030 编码 :兼容 GBK 编码。继续利用 GBK 编码闲置的编码空间,对于超出编码空间的则采用 4 个字节表示。
BIG5 :
ü BIG5 字符集 : ASCII 字符集 +13000 左右汉字(繁体)。
ü BIG 编码 :兼容 ASCII 编码。其编码模式类似于 GB2312.
UNICODE :( UNICODE 一词在日常使用中显得宽泛、混乱,在不同的语境中可以是以下意思之一。 )
ü UNICODE 标准 : 由一些组织提出的一套标准,对人类文字的显示、编码等进行了一系列的规定。
ü UNICODE 字符集 :目前最新版的 UNICODE 字符集中已经包含各种语言的超过 10 万的字符。
ü UNICODE 编码 :( 狭义的 UNICODE 编码 可能 指 UCS-2 , 也可能 指 UTF-16 ;广义的 UNICODE 编码可以指包括以下四种在内的若干种对 UNICODE 标准的编码实现。 )
1. UTF-32 编码 : 固定使用 4 个字节来表示一个字符,存在空间利用效率的问题。
2. UTF-16 编码 : 对相对常用的 60000 余个字符使用两个字节进行编码,其余的 ( 即 ’ 补充字符 supplementary characters’) 使用 4 字节。
3. UCS-2 编码 :是对 UNICODE 早期版本的实现,它与 UTF-16 的唯一区别是它不包括’补充字符’,所以它对字符的编码只使用两个字节。 目前此编码模式已过时 。
4. UTF-8 编码 :兼容 ASCII 编码;拉丁文、希腊文等使用两个字节;包括汉字在内的其它常用字符使用三个字节;剩下的极少使用的字符使用四个字节。
ISO8859-1 :( 使用 Oracle 的同志们可能见过这个 WE8ISO89859P1 ,没错,就是它。 )
ü ISO8859-1 字符集 : ASCII 字符集 + 若干西欧字符,例如 字母 ? 、 ? 。
ü ISO8859-1 编码 : 使用 8 位表示一个字符,同时移除了原 ASCII 编码中的控制符(即 [0-31] ,及 127 )。
Code page :( 可以把 ”code page” 认为是 ” 编码 ” 的近义词。至于为什么有这个名称?历史遗留问题。 )
ü ANSI code pages :你一定见过 ANSI ,想想另存文本文件时。 ANSI code pages 实际上是一系列的编码集合,根据操作系统区域设置而激活其中一种作为默认 ANSI 编码。例如公司电脑(英文系统)上的 ANSI code page 可能是 1252 ,而家里的中文系统则可能是 936 。所以在家里可以用 ANSI 存储一个包含中文的文本文件,在公司则不行。可以在注册表键: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\NLS\CodePage\ACP 中查看到当前使用的 ANSI code page 。 C# 可以通过 Encoding .Default 查看。
ü OEM code pages : OEM code pages 是给控制台应用程序(如 SQLPLUS )使用的。除 CJK 环境( Chinese-Japanese-Korean )外, Windows 使用不同的 ANSI code page 和 OEM code page 。例如,公司英文系统上使用的是 437 。可以使用 CHCP 命令查看当前使用的 OEM code page , C# 可以通过 Console .OutputEncoding 查看。
Code page 1252 :
ü cp1252 字符集 : ASCII 字符集 + 若干西欧字符 + 若干特殊符号,比如 ? 、 ‰.
ü cp1252 编码 :使用 8 位表示一个字符。编码范围是 [0-255] (即 Hex[00-FF] ), [0-127] 部分与 ASCII 相同,新增的大部分是西欧的字符,例如一些带上标的字母 ? 、 ? ,以及像这样一类特殊符号)
PS1 :现实中两台 PC 上的 code
page 信息
PC 1 :英文版 Windows XP , ANSI code page=1252, OEM code page=437
PC 2 :中文版 Windows 7 , ANSI code page=936, OEM code page=936
PS2 : cp1252 与 cp437 编码表下载请猛击这里,早期控制台应用程序常常需要画一些粗糙的表格等等图形,所以可以在 437 中看到不少不同的横线竖线这一类的特殊符号。
PS3 : CP1252 、 ISO8859-1 、 ASCII 比较, 就实际使用的编码范围来说: CP1252>ISO8859-1>ASCII 。 ASCII 是 [0-127] , CP1252 是 [0-255] , ISO8859-1 则移除了 cp1252 中 [0-31] 及 127 这些不可见的控制符,同进移除了 [128-159] (即 Hex[80-9F] )中的特殊符号。
第二部分 Oracle 中的编码与字符集
1. 为什么需要两个字符集?
Oracle 中有两个字符集:
1 )数据库字符集
2 )国家字符集
为什么要有两个字符集?如果我知道只需要英文,设置数据库字符集 =US7ASCII ,如果我知道只需要西欧字符,设置数据库字符集 =WE8MSWIN1252 或者 WE8ISO89859P1 ,或者干脆就用 AL32UTF8 。你看,我只需要设定“数据库字符集”,那么“国家字符集”有什么必要呢?
其实,考虑到历史遗留问题以及数据库创建者们无法避免的“短视”,很多现有数据库都无法支持 UNICODE 字符集,例如要在现有的 US7ASCII 数据库字符集的数据库中存储中文,这个时候“国家字符集” +NVARCHAR2 这样的组合就能救你一命了。对于数据类型为 NVARCHAR2( 以及 NCHAR, NCLOB) 的字段,它使用是国家字符集,与数据库字符集的设置无关。自 9i 以后,国家字符集可选的只有 AL16UTF16 与 AL32UTF8 , UTF-16 与 UTF-8 都是 UNICODE 编码标准的实现,因些可以表示世界上几乎所有的文字。
当然,如果数据库字符集本身就使了 UNICODE 字符集,就没有必要使用 NVARCHAR2,
NCHAR, NCLOB 这些类型了。
2. 字符集名称的玄机
Oracle 对字符集的命名实际上有一定的规则可寻,例如:
AL32UTF8
【 AL 】支持所有语言( All Language )。
【 32 】每字符最多占用 32 位( 4 字节)。
【 UTF8 】编码为 UTF-8 。
WE8MSWIN1252
【 WE 】支持西欧语言( Western Europe )。
【 8 】每字符需要占用 8 位(单字节)。
【 MSWIN1252 】编码为 CP1252 。
US7ASCII
【 US 】表示美国( United States )。
【 7 】每字符需要占用 7 位。
【 ASCII 】编码为 ASCII 。
其它如 ZHS16GBK , ZHT16BIG5 , US8PC437 (编码为 OEM
cp437 ),都可以类推。
3. 例子很重要
3.1. 准备两个数据库
上帝说要有例子,于是有了两个相同版本的数据库, A 跟 B :
SELECT parameter, VALUE
FROM nls_database_parameters
WHERE parameter IN ( ' NLS_CHARACTERSET ' , ' NLS_NCHAR_CHARACTERSET ' )
-- 数据库A:
PARAMETER VALUE
-- ---------------------------- -------------------
NLS_CHARACTERSET WE8MSWIN1252
NLS_NCHAR_CHARACTERSET AL16UTF16
-- 数据库B:
PARAMETER VALUE
-- ---------------------------- -----------------
NLS_CHARACTERSET AL32UTF8
NLS_NCHAR_CHARACTERSET AL16UTF16
-- 在A和B中分别创建一张表。
CREATE TABLE charset_test
(id NUMBER ( 4 ) PRIMARY KEY ,
vc VARCHAR2 ( 20 ),
nvc NVARCHAR2( 20 ));
3.2. 工具很重要
在测试之前,为避免工具本身的特性给人造成的困惑,介绍一下几个客户端工具对 UNICODE 的支持情况:
ü SQLPLUS :不支持 UNICODE 字符集。是否支持中文取决于当前的 OEM code page ,如果是 cp437 ,无论输入还是显示中文都是不可能的。但如果是 cp936 ,则可以支持中文输入 输出。
ü PLSQL Developer : 7.0 版本的查询结果窗口支持 UNICODE 字符集,但是编辑窗口 ( 即输入 SQL 语句的窗口 ) 不支持。 8.0 版完全支持 UNICODE 。
ü Oracle SQL Developer :查询结果窗口与编辑窗口都支持 UNICODE 字符集。
3.3. 出现乱码了
这里使用 Oracle SQL Developer ,分别在 A 、 B 中插入并查询中文:
INSERT INTO charset_test VALUES ( 1 , ' 中 ' , ' 中 ' );
COMMIT ;
-- A库
SELECT * FROM charset_test;
1 ? ?
-- B库
SELECT * FROM charset_test;
1 中 中
暂时先跳过 VARCHAR2 字段,先来关注 NVARCHAR2 字段,为什么在 A 库不能正常显示?无非有这几种可能:
ü 客户端操作系统不支持显示中文。
ü Oracle 客户端工具(这里是 Oracle SQL Developer )不支持显示中文。
ü Oracle 客户端有相关设置(比如 NLS_LANG )不正确。
ü 存储在数据库中的数据已经是不正确的数据。
第一点,客户端操作系统是否支持中文对运行于其上的应用程序有影响吗?应该有两种情况,一种是应用程序依赖于操作系统的中文支持;另一种是有一些软件自己带有语言包及字体(比如 Adobe 的一些产品, .NET 程序在编译的时候也可以选择将字体文件打包进去),那么它应该不依赖于操作系统。
我猜测 Oracle SQL Developer 应该是属于前一种,同时我检查了操作系统,确定其已经支持东亚语言( Control panel—Regional and language options—Language tab—Supplemental languages support—Install files for East Asian languages ,如果 checkbox 已经选中,说明已经安装东亚语言包)。
第二点,无论查询结果窗口还是编辑窗口都支持 UNICODE 字符集。
第三点,由于不依赖于 Oracle client 的 OCI ,客户端注册表中的 NLS_LANG 设置对像 Oracle SQL Developer 没有影响。
第四点,我们借助 DUMP() 函数来确定 NVARCHAR2 字段中具体的内容。
DUMP() 的语法: DUMP( [, [, [, ]]])
其中的 format 参数:如果是 8 则表示结果使用 8 进制表示,如果是 16 则表示 16 进制,如果是 0 到 16 间的其它数则都使用 10 进制。如果是大于 16 的数,则分几种情况:如果是可见的 ASCII 字符则直接打印此字符,如果是控制字符则打印成 “^x” ,其它情况则把结果按 16 进制显示。为 format 加上 1000 则表示除了 输出结果之外,还会附带 输出所使用的字符集信息。
这里我们使用:
SELECT DUMP (nvc, 1016 ) FROM charset_test;
-- A库
Typ = 1 Len = 2 CharacterSet = AL16UTF16: 0 ,bf
-- B库
Typ = 1 Len = 2 CharacterSet = AL16UTF16: 4e,2d
我们知道 “ 中 ” 字的 UTF-16 编码是 4E2D ,显然在 A 库中存储的数据已经是不对的, 00BF 实际上就是一个倒的问号字符,其存储在数据库中的原始数据已经不对了,更何况是客户端的显示。
3.4. 找不同
那么为什么两个库会不一样呢?嫌疑很快就落在了数据库字符集上,因为 A 和 B 的区别只在数据库字符集上,一个是 WE8MSWIN1252 ,另一个是 AL32UTF8 。经过测试,结论是:
Oracle SQL Developer 忽略 NLS_LANG ,字符串直接以 照数据库字符集 进行编码后由客户端传输到服务器端。由于 A 库数据库字符集不支持汉字,很不幸地被替换成了默认的 BF 并最终被存储到数据库中,永远地错下去。 B 库则相反,中文在传输的过程中 “ 存活 ” 下来并成功到达服务器端,最终自动转换成 NVARCHAR2 所用的编码并存储到库中。
3.5. 如何让 NVARCHAR2 字段工作
看起来似乎 A 库中的 NVARCHAR2 字段永远也无法正常使用了,并非这样,对于 Oracle SQL Developer ,通过一些设置,就可以让 NVARCHAR2 可以正常地插入、查询。
找到 {ORACLE_HOME}\sqldeveloper\sqldeveloper\bin\sqldeveloper.conf (依赖于你的 Oracle SQL Developer 安装路径),添加一行配置:
AddVMOption -Doracle.jdbc.convertNcharLiterals=true
同时在中文字符串前添加“ N ”前缀:
INSERT INTO charset_test VALUES ( 2 , ' 中 ' ,N ' 中 ' );
-- NVARCHAR2列中的中文不再是乱码了
SELECT * FROM charset_test WHERE id = 2 ;
2 ? 中
这个配置起到的作用是这样的:在 INSERT 语句从客户端传输到服务器端之前, Oracle SQL Developer 检测(实际上是 JDBC 检测)语句,如果发现“ N ”前缀,则事先将这部分的字符串按 UTF-16 进行编码得到 16 进制串。也就是相当于执行了这个命令:
INSERT INTO charset_test VALUES(2,’ 中 ’,UNISTR('\4e2d'));
C# 不需要做特殊的配置来让 NVARCHAR2 正常工作,只需要在执行 INSERT 时使用参数并选择正确的参数类型选:
cmd.CommandText = " insert into charset_test values(3,:vc,:nvc) " ;
OracleParameter p1 = new OracleParameter( " vc " , OracleDbType.Varchar2);
OracleParameter p2 = new OracleParameter( " nvc " , OracleDbType.NVarchar2);
p1.Value = " 中 " ;
p2.Value = " 中 " ;
cmd.Parameters.Add(p1);
cmd.Parameters.Add(p2);
cmd.ExecuteNonQuery();
4. 客户端的 NLS_LANG 设置及编码转换
前面我说过 Oracle SQL Developer 忽略客户端 NLS_LANG 设置,那么对于其它的工具呢?(这里我们主要关注字符集及编码,不讨论 NLS_LANG 对日期格式、排序方式、数字显示格式等等的影响)
ü SQLPLUS ,插入与查询都依赖于客户端 NLS_LANG 设置。通常,客户端 NLS_LANG 设置要与当前的 OEM Codepage 一致,比如 US8PC437 。
ü PL/SQL Developer , 插入与查询都依赖于客户端 NLS_LANG 设置。通常,客户端 NLS_LANG 设置要与数据库字符集一致。
使用 SQLPLUS 可以清晰地看到 Oracle 编码转换的过程:
1) 在 Oracle 客户端向服务器端提交 SQL 语句时, Oracle 客户端根据 NLS_LANG 和数据库字符集,对从应用程序接传送过来的字符串编码进行转换处理。如果 NLS_LANG 与数据库字符集相同,不作转换,否则要转换成数据库字符集并传送到服务器。服务器在接收到字符串编码之后,对于普通的 CHAR 或 VARCHAR2 类型,直接存储;对于 NCHAR 或 NVARCHAR2 类型,服务器端将其转换为国家字符集再存储。
2) 在查询数据时,服务器端原样返回存储在库中的数据,由客户端根据返回的元数据中的字符集信息与 NLS_LANG 和 NLS_NCHAR 的设置进行比较(如果 NLS_NCHAR 没有设置,则其默认值为 NLS_LANG 中的字符集设置),如果元数据中的字符集信息与客户端设置一致,不进行转换,否则要进行转换。国家字符集的转换根据 NLS_NCHAR 设置进行转换。
这里我也举几个使用 SQLPLUS 的测试例子,分别在 A 、 B 两库执行相同的语句,然后通过网络抓包查看从 Oracle client 传输到服务器的具体内容。
例 1 客户端 NLS_LANG : WE8MSWIN1252
SQL 命令: insert into charset_test values(1,'?',null);
网络抓包 (A 库,数据库字符集为 WE8MSWIN1252) : 91
解释:由于应用程序 ( 即 SQLPLUS) 使用的编码是 Codepage437 ,所以 ? 的编码是 91 。当 91 被传给 Oracle client 后, Oracle client 根据 NLS_LANG 误判其使用的编码是 Codepage1252 ,又由于 NLS_LANG 设置与数据库字符集一致,于是 Oracle client 不进行编码转换, 91 被直接传给服务器并存储,考虑到数据库字符集是 Codepage1252 ,很显然 91 是错误的数据 ( 字符 [?] 在 Codepage1252 下的编码是 E6 ,而非 91) 。
这个错误导致了一个有趣的现象,那就是在同一个客户端使用 SQLPLUS 查询居然可以看到正确字符 [?] ,这是由于 SELECT 的时候 91 也被直接返回,并且在 Oracle client 也不进行编码转换而是直接传给了应用程序,恰巧应用程序根据自己使用的编码可以正确解析 91 。但是换一个客户端机器,或者换一个客户端工具都可能得到不一样的查询结果。
网络抓包 (B 库,数据库字符集为 AL32UTF8) : E2 80 98
解释:由于应用程序 ( 即 SQLPLUS) 使用的编码是 Codepage437 ,所以 ? 的编码是 91 。当 91 被传给 Oracle
client 后, Oracle client 根据 NLS_LANG 误判其使用的编码是 Codepage1252 ,而 91 在 Codepage1252 中对应的是字符 [ ‘ ] ,根据 Codepage1252 到数据字符集 UTF8 的转换,最终转换成了 E2
80 98 ,即 UTF8 下的 [ ‘ ] 。
例 2 客户端 NLS_LANG : US7ASCII
SQL 命令: insert into charset_test values(1,'?',null);
网络抓包 (A 库 ) : BF
解释:由于应用程序 ( 即 SQLPLUS) 使用的编码是 Codepage437 ,所以 ? 的编码是 91 。当 91 被传给 Oracle client 后, Oracle client 根据 NLS_LANG 误判其使用的编码是 ASCII ,而 91 在 ASCII 中是无效编码,根据 ASCII 到数据字符集 Codepage1252 的转换,最终转换成了 BF , BF 是 Codepage1252 遇到无效编码时使用的默认替换编码。
网络抓包 (B 库 ) : EF BF BD
解释:由于应用程序 ( 即 SQLPLUS) 使用的编码是 Codepage437 ,所以 ? 的编码是 91 。当 91 被传给 Oracle
client 后, Oracle client 根据 NLS_LANG 误判其使用的编码是 ASCII ,而 91 在 ASCII 中是无效编码,根据 ASCII 到数据字符集 UTF8 的转换,最终转换成了 EF
BF BD , EF BF BD 是 UTF8 遇到无效编码时使用的默认替换编码。
例 3 客户端 NLS_LANG : US8PC437
SQL 命令: insert into charset_test values(1,'?',null);
网络抓包 (A 库 ) : E6
解释: E6 是字符 [?] 的正确的 Codepage1252 编码,此次由于应用程序 ( 即 SQLPLUS) 使用的是 Codepage437 , Oracle client 从 NLS_LANG 获得的编码信息也是 Codepage437 ,于是进行了正确的编码转换。
网络抓包 (B 库 ) : C3 A6
解释: C3 A6 是字符 [?] 的正确的 UTF8 编码,此次由于应用程序 ( 即 SQLPLUS) 使用的是 Codepage437 , Oracle client 从 NLS_LANG 获得的编码信息也是 Codepage437 ,于是进行了正确的编码转换。
我觉得,只有 SQLPLUS 的日子总是那么美好,一切看起来既合理又可解释。当其它工具出现之后,世界就变得一团乱麻了, Oracle
SQL Developer 完全忽略客户端 NLS_LANG 设置倒是让事情变得简单,不过 PL/SQL
Developer 则是另一回事,我花了 4 天时间企图搞明白其中的编码转换过程,最终只证明它就是个不可理喻的玩意儿,唯一目前看起来还正确的结论是:如果要用 PL/SQL
Developer ,只好还是把 NLS_LANG 设置得跟数据库字符集一致。其它就只能自求多福了。
5.NLS_LANG 对 ODP.NET 的影响
View Code
唯一受客户端 NLS_LANG 影响的是 OracleString 的 GetNonUnicodeBytes() 方法,此方法依赖于客户端本地设置的字符集,例如我们把 NLS_LANG 从 AMERICAN_AMERICA.WE8MSWIN1252 改成 AMERICAN_AMERICA.US7ASCII
其中 230 (即 HexE6 )正是字符‘ ? ’的编码,而 63 (即 Hex3F )是 ASCII 中的问号(由于 ASCII 字符集中没有‘ ? ’,故用问号代替)。
6.关于 VARCHAR2, NVARCHAR2 的其它问题
NVARCHAR2(N) ,其中的 N 是指字符数,不是字节数。不过其最大长度是以字节为单位,即 4000 字节。
VARCHAR2(N) ,其中的 N 可能是指字符数,也可能是指字节数。你可以显式地在声明的时候指定,比如 VARCHAR2(10 BYTE) 或者 VARCHAR2(10 CHAR) ,未显式指明时,则由参数 NLS_LENGTH_SEMANTICS 决定。需要注意的是你能成功声明 VARCHAR2(4000 CHAR) 并不能保证你能真的存储 4000 个字符,如果超过 4000 字节,该报错 Oracle 还是会报错。
【参考及引用】:
1. http://HdhCmsTestcnblogs测试数据/skynet/archive/2011/05/03/2035105.html
4. http://en.wikipedia.org/wiki/ASCII
11. http://en.wikipedia.org/wiki/Unicode
12. OReilly Oracle PL SQL Programming 5th Edition , Steven Feuerstein, Bill Pribyl
13. http://HdhCmsTestlaruence测试数据/2009/08/22/1059.html
14. http://en.wikipedia.org/wiki/Windows_code_page
查看更多关于【不错】字符集、编码以及Oracle的那些事的详细内容...