关于字符集--总结,补遗以及问题

 1.一些废话:
        因为前些天工作中遇到一些字符集相关的问题。想到以前也遇到过类似状况,不过一直没真正搞清楚原理。所以干脆花了一个通宵时间,ITPUB上相关文章基本看完。总算明白了个七七八八。看到类似问题被反复问。就萌发了写个总结帖子的念头,一来算自己学习的一个总结。二来也算造福大众吧。
首先,之前ITPUB已经有数位先辈总结贴:
http://www.eygle.com/index-special.htm
eygle的网站字符集问题专题帖。一共7篇文章,貌似发表在ITPUB出版物上。从字符集基本知识讲到内部详细处理,必看!
http://www.itpub.net/showthread.php?threadid=276524
jeffli73的字符集问题分析,包括了字符集的基础知识和几个实验。几个实验如果能真正理解了,字符集问题也就很少能困扰你了。
http://www.itpub.net/showthread. ... =%D7%D6%B7%FB%BC%AF
重点在Toms_zhang的最后几个回贴。虽然是主讲JAVA环境下字符集转换问题,但在理解ORACLE字符集转化上颇有值得借鉴之处。而且现在也常遇到数据库,基于JAVA的WEB服务搅和在一起的状况,这就将字符集问题更加复杂化了。
因为有上面这些相关帖子,我这文章的内容算一些零碎知识的补充,加上自己实验和一些理解上的心得。希望以后的朋友们遇到类似问题看了我的帖子(其实主要是看我上里的链接)就能自己搞定大部分状况。



2.字符编码历史,和常见字符集简介
很多是照搬 http://www.chedong.com/tech/hello_unicode.html的,有兴趣的可以看看原贴。
a.最基本的字符集

一个 character set (字符集)是一组符号和编码,比如,大家计算机原理第一课学的ASCII字符集,包括的字符有:数字,大小写字母,分号、换行之类的符号,编码方式是用一个7bit表示一个字符 (A的编码是65,b的编码是98)。ASCII只规定了英文字母的编码,非英文语言不能用ASCII编码表示,为此,不同的国家,都为自己的语言做了编码。这就有了字符集第一步的扩展:

b.英文和欧洲其他语言的单字节字符集(SingleByte Charsets):

ISO(国际标准化组织)制定了一个针对欧洲多国的字符集标准系列ISO-8859,可以将该系列的字符集都想象成一个:2^8 = 16 * 16 = 256个格子的棋盘,这样所有的西文字符(英文)用这样一个16×16的坐标系就基本可以覆盖全了。而英文实际上只用其中小于128(/x80)的部分就够了。利用大于128部分的空间的不同定义规则形成了针对其他欧洲语言的扩展字符集:ISO-8859-2 ISO-8859-4等。不同国家使用复合自己国家语言的字符集标准。所有这些国家的字符集都有公共交集ASCII。
在oracle 7.3.4这样老的系统中,很常见的就是WE8ISO8859P1(Western European 8-bit ISO 8859 Part 1)。记得即使中文系统好多也用的这个,忘了是不是默认值了。


c.亚洲语言和双字节字符集

汉字这么多,用这么一个256格的小棋盘肯定放不下,所以要区别成千上万的汉字解决办法就是用2个字节(坐标)来定位一个“字”在棋盘上的位置,将以上规则做一个扩展:



    * 如果第1个字符是小于128(/x80)的仍和英文字符集编码方式保持兼容;

    * 如果第1个字符是大于128(/x80)的,就当成是汉字的第1个字节,这个自己和后面紧跟的1个字节组成一个汉字;



其结果相当于在位于128以上的小棋格里每个小棋格又划分出了一个16×16的小棋盘。这样一个棋盘中的格子数(可能容纳的字符数)就变成了128 + 128 * 256。按照类似的方式有了简体中文的GB2312标准,繁体中文的BIG5字符集和日文的SJIS字符集等,GB2312字符集包含大约有六仟多个常用简体汉字。所有这些从ASCII扩展式的编码方式中:英文部分都是兼容的,但扩展部分的编码方式是不兼容的,虽然很多字在3种体系中写法一致(比如“中文”这2个字)但在相应字符集中的坐标不一致,所以GB2312编写的页面用BIG5看就变得面目全非了。而且有时候经常在浏览其他非英语国家的页面时(比如包含有德语的人名时)经常出现奇怪的汉字,其实就是扩展位的编码冲突造成的。我把GBK和GB18030理解成一个小UNICODE:GBK字符集是GB2312的扩展(K),GBK里大约有贰万玖仟多个字符,除了保持和 GB2312兼容外,繁体中文字,甚至连日文的假名字符也能显示。而GB18030-2000则是一个更复杂的字符集,采用变长字节的编码方式(2~4字节),能够支持更多的字符。
oracle8,8i中,中文环境默认是ZHS16GB2312?还是ZHS16GBK?谁比较清楚滴?



c.横空出世UNICODE

ASCII(英文) ==> 西欧文字 ==> 东欧字符集(俄文,希腊语等) ==> 东亚字符集(GB2312 BIG5 SJIS等)==> 扩展字符集GBK GB18030这个发展过程基本上也反映了字符集标准的发展过程,但这么随着时间的推移,尤其是互联网让跨语言的信息的交互变得越来越多的时候,太多多针对本地语言的编码标准的出现导致一个应用程序的国际化变得成本非常高。尤其是你要编写一个同时包含法文和简体中文的文档,这时候一般都会想到要是用一个通用的字符集能够显示所有语言的所有文字就好了,而且这样做应用也能够比较方便的国际化,为了达到这个目标,即使应用牺牲一些空间和程序效率也是非常值得的。UNICODE就是这样一个通用的解决方案。你可以把UNICODE想象成这样:让所有的字符(包括英文)都用2个字节(2个8位)表示,这样就有了一个2^(8*2) = 256 * 256 = 65536个格子的大棋盘。在这个棋盘中,这样中(简繁)日韩(还包括越南)文字作为CJK字符集都放在一定的区位内,为了减少重复,各种语言中写法一样的字共享一个“棋格”。



d.为什么还要有UTF-8?

毕竟互联网70%以上的信息仍然是英文。如果连英文都用2个字节存取(UCS-2),空间浪费不就太多了?所谓UTF-8就是这样一个为了提高英文存取效率的字符集转换格式:Unicode Transformation Form 8-bit form。用UTF-8,UNICODE的2字节字符用变长个(1-3个字节)表示:1. 对英文,仍然和ASCII一样用1个字节表示,这个字节的值小于128(/x80);  2. 扩展的ASCII字符(主要是西欧),第一字节用C2 - DF之间的范围,双字节表示。3.对其他语言,比如亚洲语系,还有各种特殊符号,使用3个字节表示;

因此,在应用中程序处理过程中所有字符都是16位(双字节),但在存取转换成字节流时使用UTF-8格式转换,对于英文字符来说和原来用ASCII方式存取时相比大小仍然是一样的,而对中文来说和原来的GB2312编码方式相比,大小为:(3字节/2字节)=1.5倍

ps:UTF8标准也在变化。最新的标准,UTF8可以是1-4个字符。4个字符是为了将来可能的新字符,符号进行扩展。(用10g文档上来说,就是:historic characters; musical symbols; mathematical symbols)为此oracle也定义了新的字符集AL32UTF8,也就是说其实UTF8的国际化标准就一个,只是版本在变化。而oracle公司根据不同国际标准版本有了UTF8和AL32UTF8两个具体在数据库上实现的字符集。UTF8不支持4字节扩展的。
总结:对于不同字符集关系的理解,我们可以用个比喻来说明。如果说一个字符集就是一个公司的仓库,字符的编码就是货架编号,字符的实际值就是货物本身。那么:
ASCII就是最小的仓库,就是127个货架,放了127样货物,比如 100号货架上放的是雨伞;
西欧扩展iso-8859系列,这些仓库都比A记(ASCII)多了差不多一倍的货物,而且编号1~127的货物也和A记一模一样。不过如果你去看160号货架的话,可能这家放的茶杯,那家就是毛巾了;
ZHS16GBK,这是个百货公司 ,有上万的货物,即是如此还是没有忘本,你如果找100号货架,肯定还是能拿到雨伞;
UNICODE,他的口号是别人有的我也要有。所以修了有65535个货架的大仓库。虽然Z记有的东西他也全有,不过货物放的货架(编码)全不一样了。
UTF8,真正的托拉斯,不但做到了人无我有(包括了现在所有的字符集的字符),还想千秋万代一桶浆糊(为以后扩展留了余地),和U记类似,东西非常多,货物编号除了1~127号全和别人不一样了。

3.oracle中字符集的转化
常见有讨论三种情况:
a. exp/imp时候
在这种情况下,可能存在四方字符集差异:源数据库字符集、Export过程中用户会话字符集、Import过程中用户会话字符集、目标数据库字符集。具体可能的转化过程eygle帖子里面有讲。就不多说了。记住一条:为了确保Export、Import过程中,Oracle字符集不发生转换或正确转换,最好在进行这个过程前,检查一下源数据库字符集与Export 用户会话字符集是否一致,源数据库字符集与目标数据库字符集是否一致,目标数据库字符与Import用户会话字符集是否一致。如果能够保证这四个字符集是一致的,则在Export、Import过程中,Oracle字符集就不用发生转换。


b.客户端会话输入字符,最终进入服务器存储的过程
这个在上面说的帖子里面也详细讲述了。我后面会有个实验做进一步的细节补充说明。

c.SQLLDR时候
按SQLLOADER的原理来看,应该和字符集没有关系。如果CLIENT和SERVER端不一致,查出来的汉字当然是乱码,但实际上已经LOADER成功,只是显示问题。



4.如何查看字符集
    a.如何查看服务器的字符集

    通过初始化文件InitXXXX.ora文件进行查看;

    借助SQL语句查看: SELECT NAME,VALUE$ FROM SYS.PROPS$ WHERE NAME=‘NLS_CHARACTERSET’

  有很多种方法可以查出oracle server端的字符集,比较直观的查询方法是以下这种:
        SQL>select userenv(‘language’) from dual;



  b、如何查询dmp文件的字符集

  用oracle的exp工具导出的dmp文件也包含了字符集信息,dmp文件的第2和第3个字节记录了dmp文件的字符集。如果dmp文件不大,比如只有几M或几十M,在windows下面可以用UltraEdit打开(16进制方式),看第2第3个字节的内容,如0354,然后用以下SQL查出它对应的字符集:



  SQL> select nls_charset_name(to_number('0354','xxxx')) from dual;
    UNIX下面可以用FTP到WINDOWS下面用WINHEX或者UE看,特别注意FTP方式要BIN。据说winHEX比UltraEdit打开大文件更快。



  如果dmp文件很大,比如有2G以上(这也是最常见的情况),用文本编辑器打开很慢或者完全打不开,可以用以下命令(在unix主机上):

  cat exp.dmp |od -x|head -1|awk '{print $2 $3}'|cut -c 3-6

  然后用上述SQL也可以得到它对应的字符集。



  c、查询oracle client端的字符集

  在windows平台下,就是注册表里面相应OracleHome的NLS_LANG。还可以在dos窗口里面自己设置,比如:

  set nls_lang=AMERICAN_AMERICA.ZHS16GBK,这样就只影响这个窗口里面的环境变量。


  在unix平台下,就是环境变量NLS_LANG。使用命令 echo $NLS_LANG 查看

  
    如果检查的结果发现server端与client端字符集不一致,请统一修改为同server端相同的字符集。

总结:字符集子集向其超集转换是可行的,如US7ASCII向WEISO8859P1转换;而字符集超类向字符集子集进行转换时,可能会损失部分数据。在理解了字符集转化原则的基础上,我们可以知道:1.极端的情况,即使完全不兼容的字符集,可能也不会有数据丢失!只包含英文字符数据的双字节字符集可向单字节字符集正确转换,如ZHS16GBK(English Only)向US7ASCII(因为实在没有什么好丢失滴 )。2.编码范围相同的单字节字符集之间通常可以进行相互转换。比如iso-8859系列。

请注意,这里所说的没有数据损失,是指一种字符集A转换成另一种字符集B之后,可以再从字符集B正确转换成字符集A或字符集B能够正确表示字符集A中转换过来的数据。

ps:一个英文字母是一个字符,一个中文汉字是几个字符呢?我们知道,os字符集是gbk环境下,一个中文汉字是双字节字符,但它算做几个字符与其数据库字符集有关。如果数据库字符集使用单字节US7ASCII,则一个中文汉字是二个字符;如果数据库字符集使用双字节字符集ZHS16GBK,则一个中文汉字被视为一个字符。有关这一点可以使用 Oracle的函数Substr得到证明。

使用US7ASCⅡ字符集时:

Select substr(‘东北大学’,1,2) from dual;

语句执行结果返回‘东’。



使用ZHS16GBK字符集时:

Select substr(‘东北大学’,1,2) from dual;

语句执行结果返回‘东北’。
5.我的一些实验
为了验证一些字符集转换的细节,我作了下面的实验。为了节约版面,就不用给一条命令,显示一条结果的方式了。下面列表中一条记录各代表一种环境变量,运行程序的组合。表当中char_set表示环境变量NLS_LANG,所有memo字段,最初输入时候都是汉字'测试'(同样的汉字,不同的OS环境可能内码完全不同)。
windows和linux平台数据库字符集都是ZHS16GBK

在windows 2003下面,chcp结果代码页显示936,也就是GBK字符集。char_set字段,后面什么不带表示dos窗口下运行于sqlplus输出的结果。带-W表示dos窗口下启动的sqlplusw,带-WW的表示windows下启动的sqlplusw。


当NLS_LANG=AMERICAN_AMERICA.UTF8时候,显示如下

SQL> select id, char_set, memo, dump(memo) from testc;



ID CHAR_SET     MEMO           DUMP(MEMO)                                      

--- ------------ -------------- ----------------------------------------        

  7 US7ASCII     ????           Typ=1 Len=4: 63,63,63,63                        

  8 US7ASCII-W   ????           Typ=1 Len=4: 63,63,63,63                        

  9 ZHS16GBK     娴嬭瘯           Typ=1 Len=4: 178,226,202,212                    

10 ZHS16GBK-W   娴嬭瘯           Typ=1 Len=4: 178,226,202,212                    

11 UTF8         ?锛?           Typ=1 Len=3: 63,163,191                        

12 UTF8-W       ?锛?           Typ=1 Len=3: 63,163,191  

  1 UTF8-WW      娴嬭瘯           Typ=1 Len=4: 178,226,202,212                    

  2 US7ASCII-WW  娴嬭瘯           Typ=1 Len=4: 178,226,202,212                    



8 rows selected.



当NLS_LANG=AMERICAN_AMERICA.ZHS16GBK时候,环境变量和os一致。汉字正常显示了:

SQL> select id, char_set, memo, dump(memo) from testc;



ID CHAR_SET     MEMO           DUMP(MEMO)

--- ------------ -------------- ----------------------------------------

  7 US7ASCII     ????           Typ=1 Len=4: 63,63,63,63

  8 US7ASCII-W   ????           Typ=1 Len=4: 63,63,63,63

  9 ZHS16GBK     测试           Typ=1 Len=4: 178,226,202,212

10 ZHS16GBK-W   测试           Typ=1 Len=4: 178,226,202,212

11 UTF8         ??            Typ=1 Len=3: 63,163,191

12 UTF8-W       ??            Typ=1 Len=3: 63,163,191

  1 UTF8-WW      测试           Typ=1 Len=4: 178,226,202,212

  2 US7ASCII-WW  测试           Typ=1 Len=4: 178,226,202,212



已选择8行。


$1.上面2次查询汉字显示不同是因为环境变量NLS_LANG的区别。大家都应该明白了。
$2.注意11, 12行,经过了GBK-》UTF8-》GBK,2个汉字怎么存到硬盘成了3个字节?我这么理解:OS是GBK,最初进入客户端的是4字节的gbk内码。由于客户端环境为UTF8,客户端把这4字节的编码理解成为4字节的UTF8编码(注意这里!!并没有真正的编码转化过程,只是客户端按照环境变量定义给予代码表示的字符不同的解释,即是eygle说的“映射”),前三个字节估计视为一个汉字,最后的字节被视为一个特殊字符。然后进入服务器,由于服务器知道是UTF8->GBK的转换,而前面3字节组成的UTF8字符在GBK中没有对应字符(大字符集到小字符集),所以被简单的替换为?(63),后面的1字节编码,在gbk中找到对应的编码为2个字节。于是最终存入硬盘的编成了63, 163, 191.
$3.id为1,2的2条记录。windows菜单下启动的sqlplusw(char_set字段以-WW结束), 未受到cmd窗口下SET NLS_LANG的影响,而是受注册表(值为GBK)的影响,所以保持不变,看对应的8, 10号记录可以做对比。

最后,当我们试图在NLS_LANG=AMERICAN_AMERICA.UTF8时候输入
SQL> insert into testc values(3, 'UTF8', '测');

ERROR:

ORA-01756: quoted string not properly terminated

出现这种情况可以这么理解:因为我windows2003系统,中文输入的汉字是2字节编码,而NLS_LANG设定为UTF8, 客户端在读取2字节编码时候,发现UTF8中没有对应代码(汉字都是3字节编码),而insert语句又明确说输入的是字符串,所以报错。这里留下一个问题:为什么前面环境变量设置为UTF8时候,输入2个汉字(也就是4字节编码)不报错,难道sqlplus只检查一个字符的情况?



LINUX环境
OS环境(bash窗口下面)为gb2312(即最初输入的汉字编码是双字节的国标码),char_set字段,不带-w表示终端窗口运行的sqlplus,带-w表示是在sqldeveloper中输入的记录(基于java的类似worksheet的软件)

SQL> select id, char_set, memo, dump(memo) from testc order by id;

ID CHAR_SET     MEMO           DUMP(MEMO)
--- ------------ -------------- ----------------------------------------
  1 US7ASCII     ??????         Typ=1 Len=6: 63,63,63,63,63,63
  2 US7ASCII-W   测试           Typ=1 Len=4: 178,226,202,212
  3 ZHS16GBK     娴嬭瘯         Typ=1 Len=6: 230,181,139,232,175,149
  4 ZHS16GBK-W   测试           Typ=1 Len=4: 178,226,202,212
  5 UTF8         测试           Typ=1 Len=4: 178,226,202,212
  6 UTF8-W       测试           Typ=1 Len=4: 178,226,202,212
  7 UTF8         测             Typ=1 Len=2: 178,226
  8 US7ASCII     ???            Typ=1 Len=3: 63,63,63

8 rows selected.

UTF8环境下,输入单个‘测’字正常,显示也正常。                  
ID CHAR_SET     MEMO           DUMP(MEMO)                                      
--- ------------ -------------- ----------------------------------------        
  7 UTF8         测             Typ=1 Len=2: 178,226                           


但如果我os终端(bash)设定字符编码为UTF8,设ZHS16GBK的环境变量,输入单个字符出错:
SQL> insert into testc values(9, 'ZHS16GBK', '测');
ERROR:
ORA-01756: quoted string not properly terminated
这里因为输入的最初内码为UTF8的3字节,NLS_LANG设定的字符集为ZHSK16GBK,SQLPLUS对输入的字符判断3字节认为不合法。报错。当然,如果我们输入2个汉字,6字节的编码sqlplus会认为是3个汉字编码。当输入3个汉字,又会报错。。。这个问题(还有上面windows环境下的同样问题)我一直没想明白,oracle到底根据什么来判断的。不知道谁能给我答案。


总结:首先明确下“客户端的字符集”概念,对于Oralce来说,客户端并不是指一台计算机,而是指某个客户端程序,可以是一个SQLPLus程序,可以是exp/imp的程序,也可以sqlload,也可以是其他的各种程序,这些程序的所使用的字符集,有些按照会话环境变量,注册表,或者某些参数一样的文件设置的先后顺序来指定。
OS, 客户端,数据库三者字符集关系:我们输入到计算机的信息,都是从OS级别的终端开始的。最开始输入的字符信息,只跟os或者其终端的语言环境有关。当用户在输入字符完毕按下回车后,字符对应的编码被送入客户端程序(这里我们讨论oracle客户端程序,比如sqlplus),客户端程序根据自身的字符集设定(sqlplus受NLS_LANG影响,有的程序可能不同),可能对输入的编码做另外一番“理解”。至于客户端程序到数据库,就是实质上的编码转换过程了。(如果客户端和数据库字符集设定不同,存在转换的话)
另外我们注意到,同样WINDOWS下,不同条件起动的SQLPLUSW表现不同。另外,LINUX下面没有注册表,基于JAVA的SQLDEVELOPER毫无影响。不知道IE环境下是否也相对独立?客户端程序受环境变量影响的条件,估计还得具体环境具体测试才能确定了。

6.我对oracle转化字符集意图的理解,一些问题的回答

Q、oracle数据库字符集要转换的意义何在?为什么要如此的规则(根据服务器/客户端字符集异同确定转化字符集与否)转换?
面对众多使用各种五花八门字符集的客户端,为了尽量减少信息丢失和混乱,只能尽力往服务器端统一。只不过由于目前字符集实在太多太乱,所以效果很不理想。猜测一下oracle如此规则的设计思想:当服务器和客户端字符集设置一样,他认为用户可能想原样保持初始的数据。所以不进行转换。当设置不一样,oracle会默认客户输入最初的字符所用的字符集应该和oracle客户端程序的字符集一致。即OS到客户端程序这步不应该有信息丢失。(其实这才应该是最普遍的情况,一般的用户,谁会没事主动改nls_lang之类的?)oracle做的,只是将客户端程序的字符编码改为服务器端字符编码方式存储。只要客户端没变化,将来客户端读取数据还是客户希望的样子。
当然,这样的原则只是能适应上述理想状况,现实中总会遇到种种意外。不过话又说回来,IT行业好象很难有什么包治百病的方案。或许,在不远的将来,数据库服务器端都采用UTF8(不考虑存储浪费)能解决相当的客户端程序到服务器之间信息丢失的问题。但谁知道,到时候又会有什么新问题出现?


Q.问题 http://www.itpub.net/showthread. ... =%D7%D6%B7%FB%BC%AF

http://www.itpub.net/showthread. ... =%D7%D6%B7%FB%BC%AF

A.记住2点:1.无论什么平台,什么软件,最终磁盘上存储的是基本单位为一个字节的数据;2.所有要放到数据库中的字符,不管什么编码,最初都是从OS环境进去的。你测试的平台,能同时输入不同语言,那么OS级别肯定支持UNICODE,(我估计UTF16,因为IE支持正常显示)进入数据库的应该是UNICODE编码,哪样那怕ASC, GBK, UTF8数据库,存的编码都是哪些。只不过只有UTF8是"正确理解"上的存储。如果客户端只支持单一语言字符集,给客户端的UNICODE编码,但单一语种客户端比如繁体,同样的字编码格式不同,出来肯定乱码。



Q.有一点我不能理解。我数据库字符集原为ZHS16GBK,然后改为WE8ISO8859P1。这时我查询原数据(含中文字符),客户端的用户会话字符集为WE8ISO8859P1,但查出的中文却不是乱码,而是正确的。按理说,如果原来的数据字符集仍为ZHS16GBK,我查询它,应该会发生字符集转换。不知我哪里理解错了?

A.因为服务器端,客户端字符集设定一样。所以不进行转化,系统直接把旧的GBK中文当作数据传送。WE8ISO8859P1是扩展的ASCII字符集,高位也显示数据,客户端直接拿传过来的数据显示。如果客户端会话字符集设置为US7ASCII,就会显示乱码,但也只是显示问题,服务器里面的数据是没有变化的。

你可能感兴趣的:(c#.net,/vb.net,asp.net,c/s)