[Perl] Perl Unicode全攻略(轉)

轉載本站文章請註明,轉載自:扶凱 [ http://www.php-oa.com ]
本文鏈接: http://www.php-oa.com/2008/12/13/perl-unicode.html
轉自:http://blog.chinaunix.net/u2/70049/showart_1210487.html

耐心看完本文, 相信你今後在unicode處理上不會再有什麼問題.

本文內容適用於perl 5.8及其以上版本.

以Perl 看來, 字符串只有兩種形式. 一種是octets, 即8位序列, 也就是我們通常說的字節數組二進製文件. 另一種utf8編碼的字符串, perl管它叫string. 也就是說: Perl只認識兩種編碼: Ascii(octets)和utf8(string).

utf8 flag

那麼perl如何確定一個字符串是octets 還是utf8編碼的字符串呢? perl可沒有什麼智能, 他完全是靠字符串上的utf8 flag. 在perl內部, 字符串結構由兩部分組成: 數據和utf8 flag (標記). 比如字符串"中國"在perl內部的存儲是這樣:

utf8 flag 數據
On On

如果utf8 flag 是On 的話, perl就會把中國當成utf8字符串來處理, 如果utf8 flag 為Off, perl 就會把他當成octets 來處理. 所有字符串相關的函數包括正則表達式都會受utf8 flag 的影響. 讓我們來看個例子:

程序代碼:

use  Encode;
use  strict;

my  $str  = "中國" ;
Encode::_utf8_on( $str );
print  length ( $str ) . "\n" ;
Encode::_utf8_off( $str );
print  length ( $str ) . "\n" ;

運行結果是:

程序代碼:
2
6

這裡我們使用Encode 模塊的_utf8_on 函數和_utf8_off 函數來開關字符串"中國"的utf8 flag. 可以看到, utf8 flag 打開的時候, "中國"被當成utf8字符串處理, 所以其長度是2. utf8 flag 關閉的時候, "中國"被當成octets(字節數組)處理, 出來的長度是6(我的編輯器用的是utf8 編碼, 如果你的編輯器用的是gb2312 編碼, 那麼長度應該是4) .

再來看看正則表達式的例子:

程序代碼:

use  Encode;
use  strict;

my  $a  = "china----中國" ;
my  $b  = "china----中國" ;
Encode::_utf8_on( $a );
Encode::_utf8_off( $b );
$a  =~ s/\W+//g;
$b  =~ s/\W+//g;
print  $a , "\n" ;
print  $b , "\n" ;

運行結果:

程序代碼:
Wide character in print at unicode.pl line 10.
china中國
china

結果第一行是一條警告, 這個我們稍後再討論. 結果的第二行說明, utf8 flag 開啟的情況下, 正則表達式中的\w能夠匹配中文, 反之則不能.

如何確定一個字符串的utf8 flag 是否已開啟? 使用Encode::is_utf8($str). 這個函數並不是用來檢測一個字符串是不是utf8 編碼, 而是僅僅看看它的utf8 flag 是否開啟.

eq

eq 是一個字符串比較操作符, 只有當字符串的內容一致並且utf8 flag 的狀態也是一致的時候, eq 才會返回真.

理論就是上面這些, 一定要搞明白, 記清楚! 下面是實際應用.

unicode 轉碼

如果你有一個字符串"中國", 它是gb2312編碼的. 如果它的utf8 flag 是關閉的, 它就會被當成octets 來處理, length() 會返回4, 這通常不是你想要的. 而如果你開啟它的utf8 flag, 則它會被當做utf8 編碼的字符串來處理. 由於它本來的編碼是gb2312的, 不是utf8的, 這就可能導致錯誤發生. 由於gb2312和utf8內碼範圍部分重疊, 所以很多時候, 不會有錯誤報出來, 但是perl可能已經錯誤地拆解了字符. 嚴重的時候, perl會報警, 說某個字節不是合法的utf8內碼.

解決的方法很顯然, 如果你的字符串本來不是utf8編碼的, 應該先把它轉成utf8編碼, 並且使它的utf8 flag處於開啟狀態. 對於一個gb2312編碼的字符串, 你可以使用

程序代碼:

$str  = Encode::decode( "gb2312" , $str );

來將其轉化為utf8 編碼並開啟utf8 flag. 如果你的字符串編碼本來就是utf8, 只是utf8 flag 沒有打開, 那麼你可以使用以下三種方式中的任一種來開啟utf8 flag:

程序代碼:

$str  = Encode::decode_utf8( $str );
$str  = Encode::decode( "utf8" , $str );
Encode::_utf8_on( $str );

最後一種方式效率最高, 但是官方不推薦. 以下劃線開頭的函數是內部函數, 出於禮貌, 一般不從外部調用.

字符串連接

. 是字符串連接操作符. 連接兩個字符串時, 如果兩個字符串的utf8 flag都是Off, 那麼結果字符串也是Off. 如果其中任何一個字符串的utf8 flag是On的話, 那麼結果字符串的utf8 flag將是On. 連接字符串並不會改變它們原來的編碼, 所以如果你把兩個不同編碼的字符串連在一起, 那麼以後不管對這個字符串怎麼轉碼, 都總會有一段是亂碼. 這種情況一定要避免, 連接兩個字符串之前應該確保它們編碼一致. 如有必要, 先進行轉碼, 再連接字符串.

perl unicode編程基本原則

對於任何要處理的unicode字符串, 1)把它的編碼轉換成utf8; 2)開啟它的utf8 flag

字符串來源

為了應用上面說到的基本原則, 我們首先要知道字符串本來的編碼和utf8 flag開關情況, 這裡我們討論幾種情況.

1) 命令行參數和標準輸入. 從命令行參數或標準輸入(STDIN)來的字符串, 它的編碼跟locale有關. 如果你的locale是zh_CN或zh_CN.gb2312, 那麼進來的字符串就是gb2312編碼, 如果你的locale是zh_CN.gbk, 那麼進來的編碼就是gbk, 如果你的編碼是zh_CN.UTF8, 那進來的編碼就是utf8. 不管是什麼編碼, 進來的字符串的utf8 flag都是關閉的狀態.

2) 你的源代碼裡的字符串. 這要看你編寫源代碼時用的是什麼編碼. 在editplus裡, 你可以通過"文件"->"另存為"查看和更改編碼. 在linux下, 你可以cat一個源代碼文件, 如果中文正常顯示, 說明源代碼的編碼跟locale是一致的. 源代碼裡的字符串的utf8 flag同樣是關閉的狀態.

如果你的源代碼裡含有中文, 那麼你最好遵循這個原則: 1) 編寫代碼時使用utf8編碼, 2)在文件的開頭加上use utf8;語句. 這樣, 你源代碼裡的字符串就都會是utf8編碼的, 並且utf8 flag也已經打開.

3) 從文件讀入. 這個毫無疑問, 你的文件是什麼編碼, 讀進來就是什麼編碼了. 讀進來以後, utf8 flag是off狀態.

4) 抓取網頁. 網頁是什麼編碼就是什麼編碼, utf8 flag是off狀態. 網站的編碼可以從響應頭里或者html的<head>標籤裡獲得. 也有可能出現響應頭和html head裡都沒說明編碼的情況, 這個就是做的很不禮貌的網頁了. 這時候只能用程序來猜:

程序代碼:

use  Encode;
use  LWP::Simple qw(get);
use  strict;

my  $str  = get " http://www.sina.com.cn " ;

eval  { my  $str2  = $str ; Encode::decode( "gbk" , $str2 , 1)};
print  "not gbk: $@\n"  if  $@;

eval  { my  $str2  = $str ; Encode::decode( "utf8" , $str2 , 1)};
print  "not utf8: $@\n"  if  $@;

eval  { my  $str2  = $str ; Encode::decode( "big5" , $str2 , 1)};
print  "not big5: $@\n"  if  $@;

輸出:

程序代碼:
not utf8: utf8 "\xD0" does not map to Unicode at /usr/local/lib/perl/5.8.8/Encode.pm line 162.

not big5: big5-eten "\xC8" does not map to Unicode at /usr/local/lib/perl/5.8.8/Encode.pm line 162.

我們給decode函數傳遞了第三個參數, 要求有異常字符的時候報錯. 我們用eval捕獲錯誤, 轉碼失敗說明字符串本來不是這種編碼. 另外注意我們每次都把$str拷貝到$ str2, 這是因為decode第三個參數為1時, decode以後, 傳給它的字符串參數(第二個參數會被清空). 我們拷貝一下, 這樣每次被清空的都是$str2 , $str不變.

來看結果, 既然不是utf8, 也不是big5, 那就應該是gbk了. 對於其他不知編碼的字符串, 也可以使用這種方法來猜. 不過因為幾種編碼的內碼範圍都差不多, 所以如果字符串比較短, 就可能出不了異常字符, 所以這個方法只適用於大段的文字.

輸出

字符串在程序內被正確地處理後, 要展現給用戶. 這時我們需要把字符串從perl internal form轉化成用戶能接受的形式. 簡單地說, 就是把字符串從utf8編碼轉換成輸出的編碼或表現界面的編碼. 這時候, 我們使用$str = Encode::encode('charset', $str);. 同樣可以分為幾種情況.

1) 標準輸出. 標準輸出的編碼跟locale一致. 輸出的時候utf8 flag應該關閉, 不然就會出現我們前面看到的那行警告:

程序代碼:
Wide character in print at unicode.pl line 10.

2) GUI程序. 這個應該是不用乾什麼, utf8編碼, utf8 flag開啟就行. 沒有實際測試過.

3) 做http post. 看網頁表單要求什麼編碼. utf8 flag開或關無所謂, 因為http post發送出去的只是字符串中的數據部分, 不管utf8 flag.

PerlIO

PerlIO為我們的輸入/輸出轉碼提供了便利. 它可以針對某個文件句柄, 輸入的時候自動幫你轉碼並開啟utf8 flag, 輸出的時候, 自動幫你轉碼並關閉utf8 flag. 假設你的終端locale是gb2312, 看下面的例子:

程序代碼:

use  strict;
binmode (STDIN, ":encoding(gb2312)" );
binmode (STDOUT, ":encoding(gb2312)" );
while  (<>) {
    chomp ;
    print  $_ , length , "\n" ;
}

運行後輸入"中國", 結果:

程序代碼:
中國2

這樣我們就省去了輸入和輸出時轉碼的麻煩. PerlIO可以作用於任何文件句柄, 具體請參考perldoc PerlIO.

相關API

都是Encode模塊的:

$octets = encode(ENCODING, $string [, CHECK]) 把字符串從utf8編碼轉成指定的編碼, 並關閉utf8 flag.

$string = decode(ENCODING, $octets [, CHECK]) 把字符串從其他編碼轉成utf8編碼, 並開啟utf8 flag, 不過有個例外就是, 如果字符串是僅僅ascii編碼或EBCDIC編碼的話, 不開啟utf8 flag.

is_utf8(STRING [, CHECK]) 看看utf8 flag是否開啟. 如果第二個參數為真, 則同時檢查編碼是否符合utf8. 這個檢測不一定準確, 跟decode方式檢測效果一樣.

_utf8_on(STRING) 打開字符串的utf flag

_utf8_off(STRING) 關閉字符串的utf flag

最後兩個是內部函數, 不推薦使用.

參考perldoc Encode.

utf8 和 utf-8

前面我們提到的一直都是utf8. 在perl中, utf8和utf-8是不一樣的. utf-8是指國際上標準的utf-8定義, 而utf8是perl在國際標准上做了一些擴展, 能兼容的內碼要比國際標準的多一些. perl internal form使用的是utf8. 另外順便提一下, 字符集的名稱是不區分大小寫的並且"_"和"-"是等價的.

EBCDIC

EBCDIC是一套遺留的寬字符解決方案, 不同於unicode, 它不是Ascii的超集. 上面介紹的方案並不完全適用於EBCDIC. 關於EBCDIC, 請參考perldoc perlebcdic

重要度:
文章分類:
電腦標籤:

回應

這篇是我看過分析解譯最清楚的文章,請問您是作者本人嗎?

您好:

我不是作者,我也是抄來的。文章最前面有列當初轉貼的來源網址,不過剛試一下也都連不上了。

網路上還可以找到其他同樣的文章,不確定誰是原始的作者。 :)

發表新回應

借我放一下廣告