標籤:.net 個數 浮雲 lap -- client 語言 完全 bae
第一部分字元集與編碼常識
字元集:
人們根據需要把某些字元收集到一處,並賦以名稱,於是便有了某某字元集。
編碼:
當前面收集的工作完成以後,為了讓只認識數位“愚蠢”的電腦也能夠儲存字元,人們不得不為集合裡的每一個字元分配”社會安全號碼碼”,這就是編碼,從此,終於可以以儲存編碼的方式在電腦中儲存字元了。
在字元集與編碼世界的漫漫曆史長河裡(偽),出現過若干個讓電腦工作者們如雷貫耳的名字,這些名字,有些已經成了浮雲飄散了,有些還在我們的代碼中折騰。
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中插入並查詢中文:
View Code
暫時先跳過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(<value>[,<format>[,<offset>[,<length>]]])
其中的format參數:如果是8則表示結果使用8進位表示,如果是16則表示16進位,如果是0到16間的其它數則都使用10進位。如果是大於16的數,則分幾種情況:如果是可見的ASCII字元則直接列印此字元,如果是控制字元則列印成“^x”,其它情況則把結果按16進位顯示。為format加上1000則表示除了輸出結果之外,還會附帶輸出所使用的字元集資訊。
這裡我們使用:
View Code
我們知道“中”字的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”首碼:
View Code
這個配置起到的作用是這樣的:在INSERT語句從用戶端傳輸到伺服器端之前,Oracle SQL Developer檢測(實際上是JDBC檢測)語句,如果發現“N”首碼,則事先將這部分的字串按UTF-16進行編碼得到16進位串。也就是相當於執行了這個命令:
INSERT INTO charset_test VALUES(2,’中’,UNISTR(‘\4e2d‘));
C#不需要做特殊的配置來讓NVARCHAR2正常工作,只需要在執行INSERT時使用參數並選擇正確的參數類型選:
View Code
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
oracle字元集