perl文件編碼處理問題

在看perl時,發現文件編碼處理問題不是很懂。BG了下,發現還是有點東東,so,收藏下:

--------------------------1-----------------------------------

一個腳本用於分析應用程序的日誌,但是在這些日誌文件中,有的編碼是utf-8的,有的編碼是gbk的.對於utf-8的日誌文件,需要將內容轉換成gbk的編碼,這樣看到的中文才不至於亂碼:

因此,在perl腳本中加入以下行判斷日誌內容,如是不是gbk的編碼,就對其轉碼:

$line=encode ("gbk",decode("utf-8",$line)) unless (detect($line) =~ /gb/);

這裏用到的模塊有:

use Encode;

use Encode::CN;

use Encode::Detect::Detector;

Perl5.8中讀取其他編碼的文本文件

通過改變PerlIO層的編碼即可實現,配合encoding指示符可以使操作過程更爲自然,如讀取UTF-16編碼的文本文件:
use encoding 'gbk';                            # 系統默認編碼爲GBK
use open IN=>':encoding(utf16)';       # 讀入文件時認爲數據按UTF-16編碼,自動根據BOM頭判斷是LE還是BE
open(FH,"test.txt") or die;
while(<FH>) {                                     # 讀入數據時自動從UTF-16轉換爲標準的UTF-8編碼的Perl字符串
    chomp;
    print "$_\n";                                   # 由於encoding指定了系統默認編碼,輸出時數據自動從UTF-8轉換爲GBK
}

也可以用open的3參數形式指定單個文件句柄的PerlIO層編碼,並在輸出時手工轉換編碼:
use Encode;                                     # 需要使用encode函數實現手工轉碼
open(FH,"<:encoding(utf16)","test.txt") or die;        # 指定FH句柄的數據爲UTF-16編碼
while(<FH>) {                                    # 讀入數據同樣自動轉換爲UTF-8
    chomp;
    print encode("gbk",$_),"\n";         # 手動將UTF-8編碼字符串轉換爲GBK編碼字符串輸出
}

另外使用binmode可以隨時切換某個文件句柄的PerlIO層編碼,如:
binmode(FH,":encoding(utf16)");     # 將FH的數據編碼置爲UTF-16
binmode(FH,":raw");                         # 不對FH的數據進行編解碼處理
binmode(FH,":utf8");                        # 將FH的數據編碼置爲UTF-8

--------------------------2-----------------------------------

Perl從5.6開始已經開始在內部使用utf8編碼來表示字符,也就是說對中文以及其他語言字符的處理應該是完全沒有問題的。我們只需要利用好Encode這個模塊便能充分發揮Perl的utf8字符的優勢了。

下面就以中文文本的處理爲例進行說明,比如有一個字符串"測試文本",我們想要把這個中文字符串拆成單個字符,可以這樣寫:

use Encode;

$dat="測試文本";

$str=decode("gb2312",$dat);

@chars=split //,$str;

foreach $char (@chars) {

         print encode("gb2312",$char),"\n";

結果大家試一試就知道了,應該是令人滿意的。

這裏主要用到了Encode模塊的decode、encode函數。要了解這兩個函數的作用我們需要清楚幾個概念:

1、Perl字符串是使用utf8編碼的,它由Unicode字符組成而不是單個字節,每個utf8編碼的Unicode字符佔1~4個字節(變長)。

2、進入或離開Perl處理環境(比如輸出到屏幕、讀入和保存文件等等)時不是直接使用 Perl字符串,而需要把Perl字符串轉換成字節流,轉換過程中使用何種編碼方式完全取決於你(或者由Perl代勞)。一旦Perl字符串向字節流的編碼完成,字符的概念就不存在了,變成了純粹的字節組合,如何解釋這些組合則是你自己的工作。

我們可以看出如果想要Perl按照我們的字符概念來對待文本,文本數據就需要一直用Perl 字符串的形式存放。但是我們平時寫出的每個字符一般都被作爲純ASCII字符保存(包括在程序中明文寫出的字符串),也就是字節流的形式,這裏就需要 encode和decode函數的幫助了。

encode函數顧名思義是用來編碼Perl字符串的。它將Perl字符串中的字符用指定的編碼格式編碼,最終轉化爲字節流的形式,因此和Perl處理環境之外的事物打交道經常需要它。其格式很簡單:

$octets = encode(ENCODING, $string [, CHECK])

$string:  Perl字符串

encoding: 是給定的編碼方式

$octets:  是編碼之後的字節流

check:   表示轉換時如何處理畸變字符(也就是Perl認不出來的字符)。一般不需使用編碼方式視語言環境的不同有很大變化,默認可以識別utf8、ascii、ascii-ctrl、iso-8859-1等。

decode函數則是用來解碼字節流的。它按照你給出的編碼格式解釋給定的字節流,將其轉化爲使用utf8編碼的Perl字符串,一般來說從終端或者文件取得的文本數據都應該用decode轉換爲Perl字符串的形式。它的格式爲:

$string = decode(ENCODING, $octets [, CHECK])

$string、ENCODING、$octets和CHECK的含義同上。

現在就很容易理解上面寫的那段程序了。因爲字符串是用明文寫出的,存放的時候已經是字節流形式,喪失了本來的意義,所以首先就要用decode函數將其轉換爲Perl字符串,由於漢字一般都用gb2312格式編碼,這裏decode也要使用gb2312編碼格式。轉換完成後Perl對待字符的行爲就和我們一樣了,平時對字符串進行操作的函數基本上都能正確對字符進行處理,除了那些本來就把字符串當成一堆字節的函數(如vec、pack、unpack等)。於是split就能把字符串切成單個字符了。最後由於在輸出的時候不能直接使用utf8 編碼的字符串,還需要將切割後的字符用encode函數編碼爲gb2312格式的字節流,再用print輸出。

--------------------------3-----------------------------------

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

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

perl internal form
在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   中國

如果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的<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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章