程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 數據庫知識 >> Oracle數據庫 >> Oracle教程 >> Oracle字符集、編碼

Oracle字符集、編碼

編輯:Oracle教程

Oracle字符集、編碼


Morven.Huang: C#, ORACLE, etc.

ORACLE HANDBOOK系列之十:字符集、編碼以及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標准的編碼實現。)

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. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved