Heim  >  Artikel  >  Datenbank  >  【不错】字符集、编码以及Oracle的那些事

【不错】字符集、编码以及Oracle的那些事

WBOY
WBOYOriginal
2016-06-07 15:45:581264Durchsuche

第一部分 字符集与编码常识 字符集: 人们根据需要把某些字符收集到一处,并赋以名称,于是便有了某某字符集。 编码: 当 前面收集的工作完成以后,为了让只认识数字的“愚蠢”的计算机也能够存储字符, 人们不得不为 集合里的每一 个 字符分配 ” 身份证号

第一部分字符集与编码常识

字符集:

人们根据需要把某些字符收集到一处,并赋以名称,于是便有了某某字符集。

编码:

前面收集的工作完成以后,为了让只认识数字的“愚蠢”的计算机也能够存储字符,人们不得不为集合里的每一字符分配身份证号码,这就是编码,从此,终于可以以存储编码的方式在计算机中存储字符了。

在字符集与编码世界的漫漫历史长河里(伪),出现过若干个让计算机工作者们如雷贯耳的名字,这些名字,有些已经成了浮云飘散了,有些还在我们的代码中折腾。

ASCII

ü ASCII字符集:包含大小写英文、阿拉伯数字、标点,以及一些不可见的控制符共128个。

ü ASCII编码:使用7位表示一个字符。编码范围是[0-127](即Hex[00-7F]),其中[0-31]Hex[00-1F])部分以及127Hex7F)是控制符,其余的都是些可见字符。

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 pageOEM 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 XPANSI code page=1252, OEM code page=437

PC 2:中文版Windows 7ANSI code page=936,   OEM code page=936

PS2cp1252cp437编码表下载请猛击这里,早期控制台应用程序常常需要画一些粗糙的表格等等图形,所以可以在437中看到不少不同的横线竖线这一类的特殊符号。

PS3CP1252ISO8859-1ASCII比较,就实际使用的编码范围来说:CP1252>ISO8859-1>ASCIIASCII[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以后,国家字符集可选的只有AL16UTF16AL32UTF8UTF-16UTF-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

其它如ZHS16GBKZHT16BIG5US8PC437(编码为OEM cp437),都可以类推。

3.例子很重要

3.1.准备两个数据库

上帝说要有例子,于是有了两个相同版本的数据库,AB

【不错】字符集、编码以及Oracle的那些事

【不错】字符集、编码以及Oracle的那些事

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(4PRIMARY KEY,
vc VARCHAR2(20),
nvc NVARCHAR2(20));

【不错】字符集、编码以及Oracle的那些事


3.2.
工具很重要

在测试之前,为避免工具本身的特性给人造成的困惑,介绍一下几个客户端工具对UNICODE 的支持情况:

ü SQLPLUS:不支持UNICODE字符集。是否支持中文取决于当前的OEM code page,如果是cp437,无论输入还是显示中文都是不可能的。但如果是cp936,则可以支持中文输入输出。

ü PLSQL Developer7.0版本的查询结果窗口支持UNICODE字符集,但是编辑窗口(即输入SQL语句的窗口)不支持。8.0版完全支持UNICODE

ü Oracle SQL Developer:查询结果窗口与编辑窗口都支持UNICODE字符集。


3.3.
出现乱码了

这里使用Oracle SQL Developer,分别在AB中插入并查询中文:

【不错】字符集、编码以及Oracle的那些事

【不错】字符集、编码以及Oracle的那些事

INSERT INTO charset_test VALUES(1,'','');
COMMIT;

--A库
SELECT * FROM charset_test;
1     ?     ?

--B库
SELECT * FROM charset_test;
1     中    中

【不错】字符集、编码以及Oracle的那些事

暂时先跳过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 clientOCI,客户端注册表中的NLS_LANG设置对像Oracle SQL Developer没有影响。

第四点,我们借助DUMP()函数来确定NVARCHAR2字段中具体的内容。

DUMP()的语法:DUMP([,[,[,]]])

其中的format参数:如果是8则表示结果使用8进制表示,如果是16则表示16进制,如果是016间的其它数则都使用10进制。如果是大于16的数,则分几种情况:如果是可见的ASCII字符则直接打印此字符,如果是控制字符则打印成“^x”,其它情况则把结果按16进制显示。为format加上1000则表示除了输出结果之外,还会附带输出所使用的字符集信息。

 这里我们使用:

【不错】字符集、编码以及Oracle的那些事

SELECT DUMP(nvc,1016FROM 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.
找不同

那么为什么两个库会不一样呢?嫌疑很快就落在了数据库字符集上,因为AB的区别只在数据库字符集上,一个是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”前缀:

【不错】字符集、编码以及Oracle的那些事

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时使用参数并选择正确的参数类型选:

【不错】字符集、编码以及Oracle的那些事

【不错】字符集、编码以及Oracle的那些事

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();

【不错】字符集、编码以及Oracle的那些事


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与数据库字符集相同,不作转换,否则要转换成数据库字符集并传送到服务器。服务器在接收到字符串编码之后,对于普通的CHARVARCHAR2类型,直接存储;对于NCHARNVARCHAR2类型,服务器端将其转换为国家字符集再存储。

【不错】字符集、编码以及Oracle的那些事

2) 在查询数据时,服务器端原样返回存储在库中的数据,由客户端根据返回的元数据中的字符集信息与NLS_LANGNLS_NCHAR的设置进行比较(如果NLS_NCHAR没有设置,则其默认值为NLS_LANG中的字符集设置),如果元数据中的字符集信息与客户端设置一致,不进行转换,否则要进行转换。国家字符集的转换根据NLS_NCHAR设置进行转换。

【不错】字符集、编码以及Oracle的那些事

这里我也举几个使用SQLPLUS的测试例子,分别在AB两库执行相同的语句,然后通过网络抓包查看从Oracle client传输到服务器的具体内容。

1 客户端NLS_LANGWE8MSWIN1252

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,而91Codepage1252中对应的是字符[],根据Codepage1252到数据字符集UTF8的转换,最终转换成了E2 80 98,即UTF8下的[]

2客户端NLS_LANGUS7ASCII

SQL命令:insert into charset_test values(1,'?',null);

网络抓包(A)BF

解释:由于应用程序(SQLPLUS)使用的编码是Codepage437,所以?的编码是91。当91被传给Oracle client后,Oracle client根据NLS_LANG误判其使用的编码是ASCII,而91ASCII中是无效编码,根据ASCII到数据字符集Codepage1252的转换,最终转换成了BFBFCodepage1252遇到无效编码时使用的默认替换编码。

网络抓包(B) EF BF BD

解释:由于应用程序(SQLPLUS)使用的编码是Codepage437,所以?的编码是91。当91被传给Oracle client后,Oracle client根据NLS_LANG误判其使用的编码是ASCII,而91ASCII中是无效编码,根据ASCII到数据字符集UTF8的转换,最终转换成了EF BF BDEF BF BDUTF8遇到无效编码时使用的默认替换编码。

3客户端NLS_LANGUS8PC437

SQL命令:insert into charset_test values(1,'?',null);

网络抓包(A)E6

解释:E6是字符[?]的正确的Codepage1252编码,此次由于应用程序(SQLPLUS)使用的是Codepage437Oracle clientNLS_LANG获得的编码信息也是Codepage437,于是进行了正确的编码转换。

网络抓包(B)C3 A6 

解释:C3 A6是字符[?]的正确的UTF8编码,此次由于应用程序(SQLPLUS)使用的是Codepage437Oracle clientNLS_LANG获得的编码信息也是Codepage437,于是进行了正确的编码转换。

我觉得,只有SQLPLUS的日子总是那么美好,一切看起来既合理又可解释。当其它工具出现之后,世界就变得一团乱麻了,Oracle SQL Developer完全忽略客户端NLS_LANG设置倒是让事情变得简单,不过PL/SQL Developer则是另一回事,我花了4天时间企图搞明白其中的编码转换过程,最终只证明它就是个不可理喻的玩意儿,唯一目前看起来还正确的结论是:如果要用PL/SQL Developer,只好还是把NLS_LANG设置得跟数据库字符集一致。其它就只能自求多福了。


5.NLS_LANG
ODP.NET的影响

【不错】字符集、编码以及Oracle的那些事View Code

唯一受客户端NLS_LANG影响的是OracleStringGetNonUnicodeBytes()方法,此方法依赖于客户端本地设置的字符集,例如我们把NLS_LANGAMERICAN_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 EditionSteven Feuerstein, Bill Pribyl

13. http://www.laruence.com/2009/08/22/1059.html

14. http://en.wikipedia.org/wiki/Windows_code_page

 

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn