Oracle實例解析:編碼與字符集(轉)


字符集:人們根據需要把某些字符收集到一處,並賦以名稱,於是便有了某某字符集。

  編碼:當前面收集的工作完成以后,為了讓只認識數字的“愚蠢”的計算機也能夠存儲字符,人們不得不為集合里的每一個字符分配”身份證號碼”,這就是編碼,從此,終於可以以存儲編碼的方式在計算機中存儲字符了。

  在字符集與編碼世界的漫漫歷史長河里(偽),出現過若干個讓計算機工作者們如雷貫耳的名字,這些名字,有些已經成了浮雲飄散了,有些還在我們的代碼中折騰。

  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標准的編碼實現。)

  ①UTF-32編碼:固定使用4個字節來表示一個字符,存在空間利用效率的問題。

  ② UTF-16編碼:對相對常用的60000余個字符使用兩個字節進行編碼,其余的(即’補充字符supplementary characters’)使用4字節。

  ③UCS-2編碼:是對UNICODE早期版本的實現,它與UTF-16的唯一區別是它不包括’補充字符’,所以它對字符的編碼只使用兩個字節。目前此編碼模式已過時。

  ④ 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中有兩個字符集:數據庫字符集和國家字符集。

  為什么要有兩個字符集?如果我知道只需要英文,設置數據庫字符集=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)例子很重要

  ①准備兩個數據庫

  上帝說要有例子,於是有了兩個相同版本的數據庫,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( 4PRIMARY  KEY,
vc  VARCHAR2( 20),
nvc NVARCHAR2( 20));

   ②工具很重要

  在測試之前,為避免工具本身的特性給人造成的困惑,介紹一下幾個客戶端工具對UNICODE 的支持情況:

  SQLPLUS:不支持UNICODE字符集。是否支持中文取決於當前的OEM code page,如果是cp437,無論輸入還是顯示中文都是不可能的。但如果是cp936,則可以支持中文輸入輸出。

  PLSQL Developer:7.0版本的查詢結果窗口支持UNICODE字符集,但是編輯窗口(即輸入SQL語句的窗口)不支持。8.0版完全支持UNICODE。

  Oracle SQL Developer:查詢結果窗口與編輯窗口都支持UNICODE字符集。

  ③出現亂碼了

  這里使用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, 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實際上就是一個倒的問號字符,其存儲在數據庫中的原始數據已經不對了,更何況是客戶端的顯示。

  ④找不同

  那么為什么兩個庫會不一樣呢?嫌疑很快就落在了數據庫字符集上,因為A和B的區別只在數據庫字符集上,一個是WE8MSWIN1252,另一個是AL32UTF8。經過測試,結論是:

  Oracle SQL Developer忽略NLS_LANG,字符串直接以照數據庫字符集進行編碼后由客戶端傳輸到服務器端。由於A庫數據庫字符集不支持漢字,很不幸地被替換成了默認的BF並最終被存儲到數據庫中,永遠地錯下去。B庫則相反,中文在傳輸的過程中“存活”下來並成功到達服務器端,最終自動轉換成NVARCHAR2所用的編碼並存儲到庫中。

  ⑤如何讓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編碼轉換的過程:

  ①在Oracle客戶端向服務器端提交SQL語句時,Oracle客戶端根據NLS_LANG和數據庫字符集,對從應用程序接傳送過來的字符串編碼進行轉換處理。如果NLS_LANG與數據庫字符集相同,不作轉換,否則要轉換成數據庫字符集並傳送到服務器。服務器在接收到字符串編碼之后,對於普通的CHAR或VARCHAR2類型,直接存儲;對於NCHAR或NVARCHAR2類型,服務器端將其轉換為國家字符集再存儲。

  

  ②在查詢數據時,服務器端原樣返回存儲在庫中的數據,由客戶端根據返回的元數據中的字符集信息與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的影響

  唯一受客戶端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還是會報錯。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM