第一部分 字符集与编码常识 字符集: 人们根据需要把某些字符收集到一处,并赋以名称,于是便有了某某字符集。 编码: 当 前面收集的工作完成以后,为了让只认识数字的“愚蠢”的计算机也能够存储字符, 人们不得不为 集合里的每一 个 字符分配 ” 身份证号
第一部分字符集与编码常识
字符集:
人们根据需要把某些字符收集到一处,并赋以名称,于是便有了某某字符集。
编码:
当前面收集的工作完成以后,为了让只认识数字的“愚蠢”的计算机也能够存储字符,人们不得不为集合里的每一个字符分配”身份证号码”,这就是编码,从此,终于可以以存储编码的方式在计算机中存储字符了。
在字符集与编码世界的漫漫历史长河里(伪),出现过若干个让计算机工作者们如雷贯耳的名字,这些名字,有些已经成了浮云飘散了,有些还在我们的代码中折腾。
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://www.cnblogs.com/skynet/archive/2011/05/03/2035105.html
2. http://www.laoxiong.net/category/oracle/orainternal/page/2
3. http://en.wikipedia.org/wiki/windows-1252
4. http://en.wikipedia.org/wiki/ascii
5. http://en.wikipedia.org/wiki/code_page_936
6. http://en.wikipedia.org/wiki/code_page_437
7. http://en.wikipedia.org/wiki/utf-8
8. http://en.wikipedia.org/wiki/utf-16/ucs-2
9. http://en.wikipedia.org/wiki/utf-32/ucs-4
10. http://en.wikipedia.org/wiki/gb_18030
11. http://en.wikipedia.org/wiki/unicode
12. oreilly oracle pl sql programming 5th edition,steven feuerstein, bill pribyl
13. http://www.laruence.com/2009/08/22/1059.html
14. http://en.wikipedia.org/wiki/windows_code_page