天天看點

Perl中字元串編碼的處理

在 Perl看來, 字元串隻有兩種形式. 一種是octets, 即8位序列, 也就是我們通常說的位元組數組. 另一種utf8編碼的字元串, perl管它叫string. 也就是說: Perl隻熟悉兩種編碼: Ascii(octets)和utf8(string).

utf8 flag

在perl内部, 字元串結構由兩部分組成: 資料和utf8 flag. 比如字元串"中國"在perl内部的存儲是這樣:

utf8 flag 資料

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是一個字元串比較操作符, 隻有當字元串的内容一緻并且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的http://www.sina.com.cn";

eval {my $str2 = $str; Encode::decode("gbk", $str2, 1)};

print "not gbk: [email protected]\n" if [email protected];

eval {my $str2 = $str; Encode::decode("utf8", $str2, 1)};

print "not utf8: [email protected]\n" if [email protected];

eval {my $str2 = $str; Encode::decode("big5", $str2, 1)};

print "not big5: [email protected]\n" if [email protected];

輸出:

程式代碼:

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是一套遺留的寬字元解決方案, 不同于unicode, 它不是Ascii的超集. 上面介紹的方案并不完全适用于EBCDIC. 關于EBCDIC, 請參考perldoc perlebcdic.

繼續閱讀