跳轉到內容

Perl 程式設計/Unicode UTF-8

來自華夏公益教科書,開放的書籍,面向開放的世界
前一個:PSGI 索引 下一個:Perl 6

在應用程式開發的背景下,使用 UTF-8 編碼的 Unicode 是在應用程式中支援多種語言的最佳方式。即使在同一個網頁上也可以支援多種語言。

Unicode(通常以 UTF-8 形式出現)正在取代 ASCII 和使用 ISO-8859-1 和 Windows-1252 等 8 位“內碼表”。

另請參見Perl Unicode Cookbook - 44 個在 Perl 5 中使用 Unicode 的食譜。

Unicode 是一個標準,它指定了世界上大多數書寫系統的所有字元。每個字元都分配了一個唯一的碼位,例如 U+0030。前 256 個碼位與ISO-8859-1 相同,以便輕鬆地轉換現有的西方/拉丁-1 文字。

要檢視特定碼位的屬性

use Unicode::UCD 'charinfo';
use Data::Dumper;
print Dumper(charinfo(0x263a));  # U+263a

如果您檢視Unicode 字元引用,您會注意到並非每個碼位都分配了一個字元。此外,由於向後相容傳統編碼,某些字元具有多個碼位。

UTF-8 是 Unicode 的一種特定編碼 - 最受歡迎的編碼。其他編碼包括 UTF-7、UTF-16、UTF-32 等。如果您決定使用 Unicode,您可能需要使用 UTF-8。

編碼定義了每個 Unicode 碼位如何對映到位和位元組。在 UTF-8 編碼中,前 128 個 Unicode 碼位使用一個位元組。這些位元組值與US-ASCII 相同,如果只使用 ASCII 字元,則 UTF-8 編碼和 ASCII 編碼可以互換。接下來的 1,920 個碼位在 UTF-8 中使用兩位元組編碼。編碼剩餘的碼位需要三個或四個位元組。

請注意,儘管 Unicode 碼位 128-255 與 ISO-8859-1 相同,但 UTF-8 對這些碼位中的每一個進行不同的編碼。UTF-8 使用兩個位元組來編碼這些碼位中的每一個,而 ISO-8859-1 只使用一個位元組來編碼該範圍內的每個字元。因此,ISO-8859-1 和 UTF-8 不可互換。(如果只使用 ASCII 字元,那麼它們都是可以互換的,因為 ASCII、ISO-8859-1 和 UTF-8 對前 128 個 Unicode 碼位都使用相同的編碼。)

因此,重申一下,使用 UTF-8,並非所有字元都編碼到單個位元組中(與 ASCII 和 ISO-8859-1 不同)。思考一下:這將如何影響編輯器(如 vim 或 emacs)、網頁和表單、資料庫、Perl 本身、Perl IO、您的 Perl 原始碼(如果您想包含具有多位元組編碼的字元)?如果字串包含具有多位元組編碼的字元,那麼這將如何影響傳遞字串?正則表示式仍然有效嗎?

字元編碼比較
字元編碼 # 字元 128 個 US-ASCII 字元 接下來的 128 個字元 剩餘字元
US-ASCII 128 1 位元組 N/A N/A
ISO-8859-1 256 1 位元組 1 位元組 N/A
UTF-8 > 100,000 1 位元組 2 位元組 2 - 6 位元組

從上表可以看出,碼位 128-255 (0x80-0xff) 是您需要注意的地方。 稍後,您將發現碼位 128-159 (0x80-0x9F) 甚至更棘手,因為流行的 Windows-1252 字元集(另一個每個字元一個位元組的編碼)在該範圍內與 ISO-8859-1 不相容。

\x{c3}\x{ae}

UTF-8“成本”多少?

[編輯 | 編輯原始碼]
  • 一些函式在 Perl 中使用 UTF-8 編碼的字串時速度較慢
  • 您必須編寫一些額外的 Perl 程式碼來確保進入 Perl 的資料被正確解碼,並且離開 Perl 的資料被正確編碼 - 但您在使用除平臺的本地 8 位字元集(我們現在將其稱為N8CS[1])之外的任何字元集時都必須這樣做,這通常是 ISO-8859-1/Latin-1
  • 您必須適當地與您的資料庫互動 - 它是否使用 UTF-8?
  • 您必須確保您的網頁指定網頁以 UTF-8 編碼
  • 您可能需要進行 Web 伺服器調整(如果它配置為始終提供某些特定的字元集,而不是 UTF-8)

如何使用 UTF-8?

[編輯 | 編輯原始碼]

如果可能,最佳實踐方法是在任何地方都使用 UTF-8。這包括網頁以及由此產生的 Web 表單、資料庫、HTML 模板和儲存在 Perl 中的字串。一個例外可能是您的 Perl 原始碼本身。如果 N8CS 足夠(即,如果您的原始碼中不需要任何 UTF-8 字元或字串),那麼您的原始碼不需要以 UTF-8 編碼。(好的,另一個例外可能是您的 HTML 模板。如果您的模板只需要/包含 N8CS,那麼它們也不需要以 UTF-8 編碼。)

要在 Perl Web 應用程式中正確使用 UTF-8,以下是必須完成的操作摘要

  • 進入 Perl 的所有文字(非二進位制)資料/位元組(因此表單資料、資料庫資料、檔案讀取、HTML 模板等)必須被正確解碼。如果傳入的文字/位元組以 UTF-8 編碼,則必須對其進行 UTF-8 解碼。如果它們以 N8CS(通常是 ISO-8859-1)編碼,則應對其進行 N8CS 解碼。如果它們以其他字元集編碼,則必須使用該字元集對其進行解碼。
  • 所有從 Perl 輸出的文字資料(因此到瀏覽器、資料庫、檔案等)必須被正確編碼(編碼成位元組流)。STDOUT(到瀏覽器的輸出)必須使用 UTF-8 編碼。
  • 瀏覽器需要透過 HTTP 頭部和<meta>標籤被告知網頁是 UTF-8 編碼的。

不要使用早於 5.8.1 版本的 Perl。雖然對 UTF-8 的支援從 5.6.0 版本開始,但正則表示式在下一個版本 5.6.1 中仍然無法正常工作。5.8.1 版本增加了一些速度改進。到了 Perl 5.14,Unicode 支援在很大程度上是乾淨且流暢的。

在我們開始深入討論如何使用 UTF-8 的細節之前,我們需要先定義一些術語,然後談談 Perl 在內部儲存文字時的雙重特性。

術語

[edit | edit source]

一個字元是一個邏輯實體。為了使用、儲存、寫入、在程式之間交換等,字元必須被編碼(使用字元集)。編碼將邏輯字元轉換為我們在程式中可以使用的內容。根據用於編碼的字元集,單個字元可能需要一個或多個位元組來表示。

在引用傳入或傳出 Perl 程式的資料時,我們將使用位元組一詞。一個位元組是一個位元組,即 8 位。編碼後的字元組成一個位元組。當一個位元組流進入 Perl 時,位元組應該被解碼(使用正確的字元集——它們被編碼的字元集),以便 Perl 能夠確定編碼後的位元組流中包含哪些邏輯字元。然後,Perl 可以將這些字元儲存為字串——一個字元序列。

二進位制資料也作為位元組流傳入。它不應該使用字元集解碼,因為它可能根本不包含任何字元,或者它除了字元之外還包含其他資訊,因此無法使用字元集解碼。

Perl 字串/文字

[edit | edit source]

在內部,Perl 使用以下編碼之一儲存每個字串

  • 本機編碼——位元組編碼。它使用 N8CS[2]。這是一種每字元一個位元組的編碼,因此最多隻能編碼 255 個字元。如果 Perl 沒有被指示解碼(不推薦),這是所有傳入文字/位元組的預設編碼。使用這種編碼的字串被稱為位元組字串二進位制字串。除非你另行指示,否則 Perl 將認為這些位元組是 ISO-8859-1,而不是你的平臺編碼。這是一個常見的錯誤。
  • UTF-8 編碼——字元編碼。它使用(顯然)UTF-8。使用這種編碼的字串被稱為字元字串文字字串Unicode 字串

在建立你自己的字串時,Perl 儘可能使用 N8CS(出於向後相容性和效率原因)。但是,如果字元無法在 N8CS 中表示,則使用 UTF-8。換句話說,如果字串中的所有程式碼點都<= 0xFF,則使用 N8CS,否則使用 UTF-8。

$native_string = "\xf1";
$native_string = "\x{00f1}";    # still N8CS, since <= 0xff
$native_string = chr(0xf1);     # still N8CS, since <= 0xff
$utf8_string = "\x{0100}";

你可以使用以下方法將 N8CS 字串轉換為 UTF-8 字串utf8::upgrade():

$my_string = "\xf1";          # N8CS byte string (one byte is used internally to encode)
utf8::upgrade($my_string);    # UTF-8 character string now (two bytes are used internally to encode)

你的程式可以包含 Perl 的兩種內部格式的字串混合。Perl 使用“UTF8 標誌”來跟蹤字串內部使用的編碼。值得慶幸的是,格式/標誌跟隨字串。Perl 儘可能將字串保留在 N8CS 中。但是,當 N8CS/本機字串與 UTF-8 字串一起使用時,本機字串會使用 N8CS 靜默隱式解碼,並升級(編碼)到 UTF-8。換句話說,本機位元組字串使用本機字元集解碼,然後在內部編碼成 UTF-8。生成的字元字串將設定 UTF8 標誌。

UTF-8 流

[edit | edit source]

任何 Perl IO 都需要正確處理字串/文字的解碼和編碼。由於世界各地使用著多種字元編碼,Perl 無法正確猜測用於編碼某個特定傳入文字/位元組的字元編碼,也無法知道你想要使用哪種字元編碼進行傳出文字/位元組。傳入的 UTF-8 位元組流與傳入的 Windows-1252 位元組流並不相同。例如,Unicode 字元 U+201c(左雙引號)在 Windows-1252 中使用一個位元組編碼(0x93),但 UTF-8 使用三個位元組對其進行編碼(0xE2 0x80 0x9C)。如果你希望 Perl 正確地解釋你的傳入文字/位元組,你必須告訴 Perl 使用哪種字元集對它們進行編碼,以便它們能夠被正確解碼。

UTF-8 文字/位元組在 Perl 程式中進出時的典型流程如下

  1. 接收外部 UTF-8 編碼的文字/位元組流並正確解碼——即,告訴 Perl 位元組使用哪種字元集進行編碼(在本例中,編碼是 UTF-8)。Perl 可以檢查解碼過程中的格式錯誤資料(錯誤編碼),具體取決於你選擇的解碼方法。Perl 將字串在內部儲存為 N8CS 或 UTF-8,具體取決於你選擇的解碼方法,以及在位元組流中發現的字元。(通常,字串將在內部儲存為 UTF-8。)
  2. 像往常一樣處理字串。
  3. 將字串編碼成 UTF-8 編碼的位元組流並輸出。

1. 解碼文字輸入

[edit | edit source]

外部輸入包括提交的 HTML 表單資料、資料庫資料(例如,來自 SQL SELECT 語句)、HTML 模板、文字檔案、套接字、其他程式等。如果這些內容中可能包含 UTF-8 編碼的資料/文字,你必須對其進行解碼。Perl 中的 UTF-8 解碼涉及兩個步驟

  1. 根據 UTF-8 格式規則解碼文字。這可能會生成解碼錯誤,具體取決於你選擇的解碼方法。使用decode()始終會導致字串在內部儲存為 UTF-8,並設定 UTF8 標誌(儘管 Encode 的文件中這麼說)。使用utf8::decode()可能會導致 N8CS 或 UTF-8 內部編碼。如果傳入文字只包含 ASCII 字元,則使用 N8CS,否則使用 UTF-8。
  2. 編碼文字(這可能是一個無操作)並在內部將其儲存為 N8CS 或 UTF-8。如果儲存為 UTF-8,則設定 UTF8 標誌。

如果你確定傳入資料/位元組只包含 N8CS(Perl 將其解釋為 ISO-8859-1)文字,則無需顯式解碼它(因為 Perl 的預設內部編碼是 N8CS,這是一種每字元一個位元組的編碼)。但是,“最佳實踐”建議所有傳入資料/位元組都應該被顯式解碼——你可以顯式解碼 ISO-8859-1、ASCII 和許多其他字元編碼。

如果你不解碼,Perl 假設輸入文字/位元組是 N8CS 編碼的,因此每個位元組都被視為一個單獨的字元——顯然,如果你有一個多位元組 UTF-8 編碼的位元組流/文字進入,這不是你想要的。不正確的解碼會導致 雙重編碼,由於隱式解碼(如上所述),這可能難以定位。

這裡要強調的另一個重要點是:你需要知道每個輸入文字使用哪種編碼。不要猜測,不要假設。

輸入 - 檔案,檔案控制代碼

[edit | edit source]

Perl 可以使用 PerlIO 層在資料進入 Perl 時自動對其進行解碼

open (my $in_fh, "<:encoding(UTF-8)", $filename) || die;  # auto UTF-8 decoding on read

如果你已經有一個開啟的檔案控制代碼

binmode $in2_fh, ':encoding(UTF-8)';

不要使用:encoding(utf8)因為它不會檢查你的傳入文字是否為有效的 UTF-8,它只會將它標記為 UTF-8——參見Perlmonks.

如果你的文字檔案包含一個位元組順序標記,請參見Perlmonks.

輸入 - HTML 模板

[edit | edit source]

如果你使用 CGI 框架或模板引擎來拉取 UTF-8 編碼的 HTML 模板檔案,你可能需要告知它 UTF-8 編碼,以便它在讀取模板檔案時對其進行“UTF-8 解碼”。基本上,框架或模板引擎需要做我們在上一節中討論的事情。

對於Template::Toolkit,如果你在模板檔案中使用適當的位元組順序標記 (BOM) 來指示編碼,則工具包將自動對其進行適當的解碼。如果模板不使用 BOM,則使用 ENCODING 選項

my $template = Template->new({ ENCODING => 'utf8' });

HTML::Template 目前不支援對 UTF-8 編碼的 HTML 模板檔案進行解碼。這是一個已知限制/錯誤。有一些解決方法

  • 一個補丁可用。
  • 你可以使用 TMPL_VARs 將 UTF-8 內容插入 N8CS(甚至 ASCII)編碼的模板檔案中。在將引數/內容插入 HTML 模板使用 TMPL_VARs 之前,對其進行 UTF-8 解碼,隱式解碼應該將生成的文字(,模板和填充的變數)在內部升級到 UTF-8。對於許多應用程式來說,這通常已經足夠了。

輸入 - 網頁表單

[edit | edit source]

預設情況下,CGI.pm 不會解碼您的表單引數。您可以使用-utf8pragma,它將把所有引數都當作 UTF-8 字串處理(並解碼),但這在您有任何二進位制檔案上傳欄位時會失敗。一個更好的解決方案是覆蓋 param 方法

package CGI::as_utf8;
BEGIN {
    use strict;
    use warnings;
    use CGI 3.47;  # earlier versions have a UTF-8 double-decoding bug
    {
        no warnings 'redefine';
        my $param_org = \&CGI::param;
        my $might_decode = sub {
            my $p = shift;
            # make sure upload() filehandles are not modified
            return $p if !$p || ( ref $p && fileno($p) );
            utf8::decode($p);  # may fail, but only logs an error
            $p
        };
        *CGI::param = sub {
            # setting a param goes through the original interface
            goto &$param_org if scalar @_ != 2;
            my ($q, $p) = @_;    # assume object calls always
            return wantarray
                ? map { $might_decode->($_) } $q->$param_org($p)
                : $might_decode->( $q->$param_org($p) );
        }
    }
}
1
---
use CGI::as_utf8;  # put this line in your app, e.g., in your CGI::Application module(s)

以上是 rhesa 的解決方案,稍微修改了一下——utf8::decode()用於代替 Encode'sdecode_utf8(),因為當只涉及 ASCII 字元時,它更有效(因為 UTF8 標誌未設定)。請注意,該模組假定網頁和表單始終使用 UTF-8 編碼,並且始終使用 CGI.pm 的 OO 介面。

注意,瀏覽器應該使用與顯示錶單相同的字元編碼來編碼表單資料。因此,如果您傳送 UTF-8 表單,則應為文字欄位獲得 UTF-8 編碼的資料。您不應該使用accept-charset在您的 HTML 標記中。

輸入 - STDIN

[edit | edit source]

當 Web 表單被 POST 時,表單資料透過 STDIN 傳入 Perl。如果您使用的是 CGI.pm,文字表單資料可透過CGI.pm'sparam()方法獲取,上一節介紹瞭如何正確處理 UTF-8 編碼的文字表單資料。

如果您沒有任何檔案上傳(即,您的所有資料都是文字),那麼您可以使用 CGI::as_utf8 模組,而是將以下程式碼行新增到指令碼的開頭,以使在 STDIN 上接收的所有資料(即,所有 POST 的表單資料)自動解碼為 UTF-8

binmode STDIN, ":encoding(UTF-8)";

不要使用

binmode STDIN, ":utf8";  # do NOT use this!

因為它不檢查傳入的文字是否為有效的 UTF-8,它只是將其標記為 UTF-8——參見 Perl 5 Wiki

上一節中的方法更可取,因為它在存在任何二進位制表單資料(檔案上傳)時將“執行正確的事”。

如果您正在編寫其他(非 CGI)程式來接收 STDIN 上的資料,請適當地解碼

my $utf8_text    = decode('UTF-8',      readline STDIN);
my $iso8859_text = decode('ISO-8859-1', readline STDIN);
my $binary_data  = read(...);  # don't decode

注意decode()始終設定 Perl 的內部 UTF8 標誌。

輸入 - 資料庫

[edit | edit source]

在“在所有地方使用 UTF-8”模型中,將您的資料庫配置為以 UTF-8 儲存值。

從 UTF-8 資料庫讀取資料時,確保傳入的 UTF-8 編碼字串欄位資料被 UTF-8 解碼,但不要解碼傳入的二進位制欄位資料。

輸入 - MySQL
[edit | edit source]

對於 MySQL,如果您使用的是 mysql_enable_utf8 資料庫控制代碼屬性,字串欄位資料的 UTF-8 解碼(和編碼)是自動的。

use DBI();
my $dbh = DBI->connect('dbi:mysql:test_db', $username, $password,
                       {mysql_enable_utf8 => 1}
);

這意味著您不應該呼叫utf8::decode()(或任何其他 UTF-8 解碼函式)在傳入的字串欄位資料上——驅動程式會為您完成此操作。如果某個欄位的傳入資料只包含 ASCII 位元組,則該欄位的 UTF8 標誌不會被設定(因此它似乎使用的是utf8::decode())。驅動程式也足夠智慧,不會解碼二進位制資料。

需要DBD::mysql的 4.004 或更高版本。UTF-8 最初在 MySQL v4.1 中可用。從 v5.0 開始,它是系統預設值。

輸入 - PostgreSQL
[edit | edit source]

對於 PostgreSQL,從 DBD::Pg 3.0.0 版本開始,如果資料庫也設定為 UTF-8,則字串欄位資料的 UTF-8 解碼(和編碼)是自動的。

對於之前的版本,您必須使用 pg_enable_utf8 資料庫控制代碼屬性,它將把所有非二進位制資料設定為 UTF-8,而不管 client_encoding 值如何。

use DBI();
my $dbh = DBI->connect('dbi:Pg:test_db', $username, $password,
                       {pg_enable_utf8 => 1}
);

這意味著您不應該呼叫utf8::decode()(或任何其他 UTF-8 解碼函式)在傳入的字串欄位資料上——DBD::Pg 驅動程式會為您完成此操作。驅動程式也足夠智慧,不會解碼二進位制資料。

預設的 client_encoding 是使用資料庫編碼,因此如果您的資料庫是 UTF-8,則它將預設設定。在其他情況下,您可能需要告訴 PostgreSQL 在從資料庫傳送資料時使用 UTF-8

SET CLIENT_ENCODING TO 'UTF8';

SET NAMES 'UTF8';

例如,使用 Rose::DB

__PACKAGE__->register_db(
   domain           => 'development',
...
   connect_options  => {
       pg_server_prepare => 0,
       pg_enable_utf8    => 1,
   },
   post_connect_sql => "SET CLIENT_ENCODING TO 'UTF8';",
);

參見 伺服器和客戶端之間的自動字元集轉換

2. 處理字串

[edit | edit source]

一旦所有傳入的字串都被內部解碼為 UTF-8,您就可以像往常一樣處理您的文字。正則表示式將起作用(如果使用 Perl v5.8 或更高版本)。

如果您在原始碼中建立了包含非 ASCII 字元(高於0x7f)的字串,請確保您將它們升級到內部 UTF-8 編碼

my $text = "\xE0";  # 0xE0 = à in ISO-8859-1
utf8::upgrade($text);

my $unicode_char = "\x{00f1}";  # U+00F1 = ñ
utf8::upgrade($unicode_char);

Perl 5“Unicode 錯誤”

[edit | edit source]

(2011-05-03 更新:v5.14 現已可用,最終消除了 Unicode 錯誤。)

如果沒有指定語言環境,如果您有字元在 0x80-0xFF(128-255)範圍內的本地/N8CS 字串,那麼\d, \s, \w, \D, \S, \W(因此有正則表示式),以及lc(), uc()等等可能無法按預期工作,因為字元集的非 ASCII 部分(0x80-0xFF)對於這些操作將被忽略。(這是嘗試在所有地方使用 UTF-8 的另一個原因。)如果沒有語言環境,Perl 無法正確解釋此範圍內的字元,因為不同的編碼在該範圍內使用不同的字元,因此它會忽略它們——這被稱為 *ASCII 語義*。

有三種方法可以避免這種“Unicode 錯誤”。最好的方法是升級到 Perl 5.14 並新增一個use 5.014;在檔案頂部。其他兩種方法涉及讓本地編碼的字串切換到 UTF-8 編碼——因為當內部編碼為 UTF-8 時,將使用 *Unicode 語義*,它始終按預期工作。

1. 遵循“最佳實踐”,始終正確解碼所有外部輸入文字/位元組。在解碼過程中,發現包含非 ASCII 字元的任何文字/位元組將被轉換為 UTF-8 內部編碼。例如

use Encode;
# suppose $windows1252_octets contains text from an external input, and it contains the character
# "\xE0" (0xE0 = à). String $windows1252_octets will exhibit the Unicode bug -- it won't match /\w/
my $utf8_string = decode('cp1252',$windows1252_octets); # no Unicode bug, $utf8_string matches /\w/

2. 使用utf8::upgrade($native_string)強制 $native_string 切換到 UTF-8 內部編碼。(即使字串只包含 ASCII 字元,它仍然會被“升級”到 UTF-8。)

my $text = "\xE0";     # will exhibit Unicode bug, won't match /\w/
utf8::upgrade($text);  # no Unicode bug, matches /\w/

請注意,使用內部 UTF-8 編碼,\w表示更大得多的字元集,因此正則表示式操作將變慢(與本地編碼相比)。待辦事項:實際效能下降是多少?*Unicode 語義* 的 \w 字元集是什麼?

另請參見 Unicode::Semantics

2010-04-19 更新:v5.12 現已可用,並且“大小寫轉換元件”已修復:“Perl 5.12 現在捆綁了 Unicode 5.2。“feature”pragma 現在支援新的“unicode_strings”功能

 use feature "unicode_strings";

這將為字串上的所有大小寫轉換操作啟用 Unicode 語義,無論它們當前如何內部編碼。”閱讀 更多

3. 編碼和輸出

[edit | edit source]

Web 程式的輸出包括 STDOUT(傳送到您的瀏覽器以供 CGI 程式使用)、stderr(通常會進入 Web 伺服器的錯誤日誌)、資料庫寫入、日誌檔案輸出等。

如果未對傳出的文字進行編碼,則文字將使用 Perl 內部格式的位元組傳送,這可能是本地/N8CS 和 UTF-8 的混合。這可能有效,但不要冒險——“最佳實踐”要求明確地對所有輸出進行適當編碼。

如果您列印一個字串,其中包含一個字元,其序數值大於 255,Perl 會向您發出警告

$ perl -e 'print "\x{0100}\n"'
Wide character in print at -e line 1.
Ā

要避免此警告,請明確編碼輸出(如下所述)。

輸出 - STDOUT

[edit | edit source]

要確保傳送回 Web 瀏覽器(即 STDOUT)的所有輸出都使用 UTF8 編碼,請將以下內容新增到 Perl 指令碼的頂部附近

binmode STDOUT, ":encoding(utf8)";

如果您想要更高效一些(但沒有遵循“最佳實踐”),您可以選擇僅在傳出的頁面被標記為 UTF-8 時對其進行編碼

if(utf8::is_utf8($page)) {
   utf8::encode($page);
}
# else, $page is natively encoded, so skip encoding for output

這是一個 片段,可與 CGI::Application 框架一起使用

__PACKAGE__->add_callback('postrun', sub {
   my $self = shift;
   # Make sure the output is utf8 encoded if it needs it
   if($_[0] && ${$_[0]} && utf8::is_utf8(${$_[0]}) ){
       utf8::encode( ${$_[0]} );
       # ${$_[0]} .= 'utf8::encode() called';  # useful for debugging
   }
});

以上程式碼應放在 CGI::Application 基類中。可選地,該程式碼可以新增到 cgiapp_postrun() 中。

請注意,如果所有輸入 UTF-8 位元組都被正確解碼,那麼所有上述編碼技術才能正常工作。

輸出 - 資料庫

[edit | edit source]

如上所述,在“在所有地方使用 UTF-8”模型中,將您的資料庫配置為以 UTF-8 儲存值。

將資料寫入 UTF-8 資料庫(INSERT、UPDATE 等)時,確保您的 UTF-8 字串在寫入資料庫之前被 UTF-8 編碼。不要編碼二進位制欄位資料。

輸出 - MySQL
[edit | edit source]

如上所述,如果您使用 mysql_enable_utf8 資料庫控制代碼屬性,字串欄位資料的 UTF-8 編碼(和解碼)將自動進行。這意味著您在使用此屬性時不應呼叫utf8::encode()(或任何其他 UTF-8 編碼函式) — 驅動程式會為您執行此操作。驅動程式也很聰明,不會對二進位制資料進行編碼。

需要 DBD::mysql 的 4.004 或更高版本。UTF-8 最初在 MySQL v4.1 中可用。從 v5.0 開始,它是系統預設值。

輸出 - PostgreSQL
[編輯 | 編輯原始碼]

如上所述,如果您使用 pg_enable_utf8 資料庫控制代碼屬性,字串欄位資料的 UTF-8 編碼(和解碼)將自動進行。這意味著您在使用此屬性時不應呼叫utf8::encode()(或任何其他 UTF-8 編碼函式) — DBD::Pg 驅動程式會為您執行此操作。驅動程式也很聰明,不會對二進位制資料進行編碼。

您可能還需要告訴 PostgreSQL 預期傳入資料庫的 UTF-8(待定:何時?)。

SET CLIENT_ENCODING TO 'UTF8';

SET NAMES 'UTF8';

參見 伺服器和客戶端之間的自動字元集轉換

輸出 - 檔案、檔案控制代碼

[編輯 | 編輯原始碼]

如果您需要寫入檔案,Perl 可以使用PerlIO

open my $out_fh, ">:utf8", $filename  or die;  # auto UTF-8 encoding on write

如果你已經有一個開啟的檔案控制代碼

binmode $out2_fh, ':utf8';

告訴瀏覽器使用 UTF-8

[編輯 | 編輯原始碼]

要向瀏覽器提供 UTF-8 編碼的頁面,“最佳做法”是在 HTTP Content-Type 標頭中指定 UTF-8 字元集,並在 HTML 檔案中的 content-type <meta> 標記中指定 UTF-8 字元集。CGI.pm 預設傳送以下 Content-Type 標頭

Content-Type: text/html; charset=ISO-8859-1

新增以下內容以使 UTF-8 而不是 ISO-8859-1 被使用,其中 $q 是您的 CGI 物件

$q->charset('UTF-8');

如果您使用 CGI::Application 框架,請將上述行放在 cgiapp_init() 中。

如果您沒有使用 CGI.pm 生成 HTML 標記,請將以下 meta 標記作為 HTML 標記 <header> 部分中的第一個 meta 標記

<meta http-equiv="content-type" content="text/html; charset=UTF-8" />

Perl 原始碼

[編輯 | 編輯原始碼]

如果您只需要在原始碼中的幾個字串中嵌入幾個 Unicode 字元,則無需以 UTF-8 格式儲存原始碼/檔案。相反,使用\x{...}chr()在您的程式碼中

  my $smiley = "\x{263a}";
  or
  my $smiley = chr(0x263a);

如果您有很多 Unicode 字元,或者您更喜歡以 UTF-8 格式儲存原始碼,那麼您需要告訴 Perl 您的原始碼是以 UTF-8 格式編碼的。為此,請將以下行新增到您的原始碼中

 use utf8;  # this script is in UTF-8

這是您的程式應該永遠擁有上述行的唯一原因 — 請參閱 utf8

如果您的原始碼是以 UTF-8 格式編碼的,請確保您的編輯器支援以 UTF-8 格式讀取、編輯和寫入!

注意事項

[編輯 | 編輯原始碼]

通常,您可能不會注意到 Unicode 問題,直到使用程式碼點大於 128 的字元。這是因為 ASCII、ISO-8859-1、Windows-1252 和 UTF-8 都對前 128 個 Unicode 程式碼點使用相同的單位元組值進行編碼。要對您的應用程式進行良好的 Unicode 測試,請嘗試使用0x80 - 0x9F(128-159)範圍內的字元,以及大於0xFF (255).

print 中的寬字元位於…

[編輯 | 編輯原始碼]

如果您列印包含序數值大於 255 的字元的字串,Perl 將向您發出警告(因此它是一個“寬”字元,需要多於一個位元組的儲存空間)

print 中的寬字元位於…第…行

顯式地對您的輸出進行編碼以避免此警告。

無法解碼包含寬字元的字串,位於…

[編輯 | 編輯原始碼]

如果您收到此錯誤,您的程式碼可能正在嘗試第二次解碼同一個字串,這將失敗。

Web 伺服器始終傳送 ISO-8859-1 標頭

[編輯 | 編輯原始碼]

如果您遵循了上述步驟,但您的頁面顯示不正常,可能是您的 Web 伺服器配置為始終在標頭中傳送特定字元編碼,例如 ISO-8859-1。要確定 Web 伺服器是否正在傳送 content-type 標頭

$ lwp-request -de www.bing.com | grep Content

Apache 可能配置了以下內容

AddDefaultCharset ISO-8859-1

如果可以,請刪除該行,或將其更改為

AddDefaultCharset UTF-8

如果伺服器提供的頁面都使用 UTF-8。另請參閱 當 Apache 和 UTF-8 發生衝突時

ISO-8859-1 與 Windows-1252

[編輯 | 編輯原始碼]

由於您正在學習字元編碼,因此您需要了解國際 ISO-8859-1 和 Microsoft 專有 Windows-1252 之間的區別。來自 Windows-1252

[Windows-1252] 在可列印字元方面是 ISO 8859-1 的超集,但它與 IANA 的 ISO-8859-1 不同,因為在 80 到 9F(十六進位制)範圍內它使用的是可顯示字元,而不是控制字元。[…] 通常將 Windows-1252 文字錯誤地標記為字元集標籤 ISO-8859-1。[…] 大多數現代 Web 瀏覽器和電子郵件客戶端將媒體型別字元集 ISO-8859-1 視為 Windows-1252 以適應這種錯誤標記。這現在是 HTML5 規範中的標準行為,該規範要求以 ISO-8859-1 為廣告的文件實際上使用 Windows-1252 編碼進行解析。

以下是一個有趣的程式可以嘗試

my @undefined_chars_in_windows_1252 = (0x81, 0x8d, 0x8f, 0x90, 0x9d);
my %h = map { $_ => undef } @undefined_chars_in_windows_1252;
foreach my $i (0x80 .. 0x9f) {
	next if exists $h{$i};
	printf "%02x:%c ", $i,$i;
}

您看到了什麼?您看到了 Windows-1252 字元,沒有字元,還是方框?如果您使用的是 PuTTY,請更改設定...視窗,翻譯,然後嘗試選擇 ISO-8859-1 或 Windows-1252 並再次執行該程式。

Microsoft“智慧”引號

[編輯 | 編輯原始碼]

Microsoft Word 使用那些漂亮的左和右奇特/智慧引號。如果您將這些字元複製貼上到使用 Windows-1252 字元集(或可能甚至 ISO-8859-1 字元集)提供的 Web 表單中,這些字元可能會使用模糊的0x80-0x9F(128-159)範圍提交到 Web 伺服器。(回想一下,Unicode 在此範圍內定義了控制字元 — 而不是像智慧引號這樣的可列印字元。)如果您的 Perl 指令碼沒有正確解碼提交的表單(即,根據 Web 表單使用的相同字元編碼),您將得到亂碼。

正確地解碼和編碼,您就不會遇到 Microsoft 智慧引號或模糊範圍內的任何其他字元的任何問題。更好的是,如果您將所有 Web 頁面提供為 UTF-8,提交的表單不應該包含這些模糊值,因為“貼上”操作應該自動將這些字元轉換為有效的 Unicode 字元。然後,您的 Perl 指令碼將只接收有效的 UTF-8 編碼字元。

瀏覽器中的奇怪字元

[編輯 | 編輯原始碼]

奇怪的字元:

這是 Unicode 的“替換字元”(程式碼點 U+FFFD),用於指示 Unicode 解析器(如瀏覽器)何時無法解碼 Unicode 編碼資料的流。問題可能是鏈中某處的編碼/解碼問題。(U+FFFD 在 UTF-8 中編碼為 EF BF BD。如果您儲存 Web 頁面,然後在 bvi 中開啟它,您可能會看到 EF BF BD。)IE 將替換字元顯示為空方框。Firefox 使用帶有問號的黑色菱形。

通常,這些替換字元出現是因為 HTML 資料是 Windows-1252 編碼的,但瀏覽器被指示使用 UTF-8 編碼。在您的瀏覽器中,選擇檢視->字元編碼,看看是否設定為 UTF-8。如果是,嘗試選擇Windows-1252西歐 (Windows),看看是否解決了問題。如果解決了,那麼您就知道 Web 伺服器正在提供錯誤的字元編碼——傳送的內容(即資料如何編碼)與瀏覽器被告知使用的字元集(即 HTTP 標頭和/或元標記)之間存在不匹配。如果無法解決問題,可能是您的計算機上沒有安裝 Unicode 字型,或者 Unicode 字型沒有該特定字元的字形。

奇怪的字元: ‘ ’ “ †• – —

這些是與以下 Windows-1252 字元的多位元組 UTF-8 編碼相對應的單個字元

‘ ’ “ ” • – —

它們在模糊的 0x80-0x9F (128-159) 範圍內。通常,這些字元出現是因為 HTML 資料是 UTF-8 編碼的,但瀏覽器被指示使用 ISO-8859-1 或 Windows-1252。在您的瀏覽器中,嘗試將編碼更改為UTF-8,看看是否解決了問題。如果無法解決問題,或者編碼已經設定為 UTF-8,則可能在某個地方存在雙重編碼問題。

奇怪的字元: ‘ ’ “ †• – —

這些也對應於模糊的 0x80-0x9F (128-159) 範圍內的某些字元。如果您看到上述序列,則可能是您忘記在您的 Perl 程式中解碼傳入的 UTF-8 資料(例如從 UTF-8 編碼的 HTML 表單提交的表單資料),然後您將它 UTF-8 編碼以進行輸出——本機編碼的字串被 UTF-8 編碼(不好)。透過呼叫以下方法來解決問題utf8::decode()在傳入的 UTF-8 編碼資料上。

編輯器中的奇怪字元

[編輯 | 編輯原始碼]
  • 確保您的編輯器支援以 UTF-8 格式讀取、編輯和寫入
  • 確保您將編輯器設定為使用 Unicode 字型
  • 確保您已安裝 Unicode 字型

在 Windows 上安裝 Unicode 字型

[編輯 | 編輯原始碼]

如果您有 此頁面 上列出的其中一個 Microsoft 產品,您應該擁有Arial Unicode MS字型。如果未安裝,請按照以下步驟安裝:新增/刪除程式,選擇 MS-Office,新增或刪除功能,單擊“選擇高階”,Office 共享功能,國際支援,通用字型。應用更改並重新啟動您的 Web 瀏覽器。

我要求 UTF-8,但我得到了一些其他東西!?

[編輯 | 編輯原始碼]

如果您專門要求 UTF-8 文字,但您收到的八位位元組流不是有效的 UTF-8 編碼,在很多情況下,您可能可以假設傳入的文字/八位位元組是 ISO-8859-1/Latin-1 或 Windows-1252。使用 Windows-1252 解碼,因為它是一個 ISO-8859-1 的超集。

雙重編碼

[編輯 | 編輯原始碼]

如果您不解碼 UTF-8 文字/八位位元組,Perl 將假設它們使用 N8CS(通常是 ISO-8859-1/Latin-1)編碼。這意味著多位元組 UTF-8 字元的單個八位位元組被視為單獨的字元(不好)。如果這些單獨的字元後來被編碼為 UTF-8 以進行輸出,則會導致“雙重編碼”。這類似於 HTML 雙重編碼——例如,&amp;gt; 而不是 &gt;。

自動字型替換

[編輯 | 編輯原始碼]

大多數現代瀏覽器和文字處理器執行 字型替換,這意味著如果一個字元不在當前字型中,應用程式將搜尋您所有的字型,直到找到包含該字元的字型,然後它將使用該字型中的字形顯示該字元。

有時 IE7 和 IE8 似乎無法正確執行字型替換。一種解決方法是在 CSS 中將 Unicode 字型指定為第一個字型font-family屬性。IE6 不被視為現代瀏覽器,它不會執行字型替換。

建立 Unicode 字元

[編輯 | 編輯原始碼]

在 Windows 上,您始終可以使用字元對映應用程式來選擇、複製和(切換到您的應用程式,然後)貼上 Unicode 字元。確保“字元集”下拉框設定為“Unicode”。您也可以使用該應用程式檢視字型、字元以及每個字元的 Unicode 程式碼點值。

在 Perl 中

[編輯 | 編輯原始碼]
  my $utf8_char  =  "\x{263a}";    # for codepoints above 0xFF
  $utf8_char     =~ /\x{263a}/;    # same syntax for regex
  my $cloud_char =  chr(0x2601);   # run-time, ord() does the reverse

如果您的 Perl 原始碼檔案是 UTF-8 格式的,您可以直接輸入 Unicode 字元

  use utf8;                # tells Perl this file is UTF-8 encoded
  my $utf8_char  =  "☺";   # U+263a, "White Smiling Face"

在 Web 表單中

[編輯 | 編輯原始碼]

在 Windows 上

  • 要從 Windows-1252 內碼表 插入字元:開啟數字鎖定鍵,按住 Alt,然後使用數字鍵盤,輸入 0 後面跟著您想要的字元的十進位制值。
  • 要從當前 DOS 內碼表(通常是 CP-437)插入字元:按照上述步驟操作,但不要輸入初始的 0。

但是等等,我們想要插入的是 Unicode 字元,而不是 Windows-1252 或 CP-437 字元!好吧,如果應用程式期望 UTF-8,Windows 將為我們將其轉換為 Unicode/UTF-8。

在 Web 表單(文字框或文字區域)中,輸入 Alt-0147 以從 Windows-1252 字元集生成那些討厭的智慧引號之一。如果網頁的字元編碼設定為 UTF-8,Windows 應該將 147 字元轉換為相應的 UTF-8 編碼。(在內部,Windows 可能將 0147 轉換為 UTF-16,然後將其轉換為應用程式正在使用的字元集。在這種情況下,字元集是 Unicode,而 Windows-1252 字元 147 被轉換為其 Unicode 程式碼點等效值,U+201C。)提交表單時,該字元應作為三個八位位元組傳送到 Web 伺服器:E2 80 9C——這就是 U+201C 使用 UTF-8 編碼時的樣子。

如果網頁的字元編碼設定為 Windows-1252,則該字元應作為單個八位位元組傳送:0x93(即十進位制 147)。如果網頁的字元編碼設定為 ISO-8859-1,則該字元也將作為單個八位位元組傳送,但其值可以是 0x93 或 0x22(0x22 是 ASCII 和 ISO-8859-1 引號字元)。如果瀏覽器在指定 ISO-8859-1 時使用超集 Windows-1252 編碼,則會發送 0x93。否則,該字元將轉換為 ISO-8859-1 中正式定義的唯一引號字元,0x22。

希望您能明白為什麼必須知道用於傳入表單/文字的編碼方式,以便能夠在您的 Perl 程式中對其進行正確解碼(作為 UTF-8 或 Windows-1252)。

另請參見 如何輸入… - Yahoo Answers。

UTF-8 與 utf8

[編輯 | 編輯原始碼]

從 Perl 5.8.7 開始,UTF-8 是嚴格的、正式的 UTF-8。如果您嘗試編碼或解碼無效的 UTF-8,例如,Encode 模組會抱怨。

encode("UTF-8", "\x{FFFF_FFFF}", 1);  # croaks

相比之下,utf8 是寬鬆的、鬆散的版本,允許幾乎任何 4 位元組值

encode("utf8", "\x{FFFF_FFFF}", 1);   # okay
encode_utf8("\x{FFFF_FFFF}", 1);      # okay

Encode 從 2.10 版開始知道區別。

utf8::encode()utf8::decode()使用正式的 UTF-8。

Encode 模組與內建/核心 utf8:

[編輯 | 編輯原始碼]

要解碼和編碼 UTF-8,您可以使用 Encode 模組或 Perl 核心在 utf8:: 包中定義的函式。Encode 模組更靈活,允許不同的方法來處理格式錯誤的資料。但是,utf8:: 包可以執行一些不同的技巧。

您應該注意 Encode 模組中的一個錯誤:每當使用 Encode 模組解碼文字時,UTF8 標誌總是被開啟。文件會讓您相信,如果文字只包含 ASCII 字元並且您正在解碼 UTF-8,則 UTF8 標誌是關閉的。事實並非如此——該標誌始終處於開啟狀態,如下表所示。

如果可以在解碼後關閉 UTF8 標誌,則可以提高效能(如果文字只包含 ASCII 八位位元組,則可以這樣做)。使用utf8::decode()為了達到這種效率,因為如果八位位元組序列只包含 ASCII 八位位元組,它不會開啟標誌。(這是我通常使用的解碼函式。)

下面,請檢視 Encode 文件,瞭解 CHECK 選項,這些選項與模組處理畸形資料的機制相關。

UTF-8 函式
函式 UTF8 標誌 描述 / 備註
$flag = utf8::is_utf8($string); N/A 測試 $string 是否以 UTF-8 編碼。如果否,返回 false;否則返回 true
$flag = utf8::decode($utf8_octets); 依賴 嘗試就地將 UTF-8 八位位元組序列轉換為相應的 N8CS 或 UTF-8 字串,具體取決於情況。如果$utf8_octets包含非 ASCII 八位位元組(即多位元組 UTF-8 編碼字元),則 UTF8 標誌將被開啟,結果字串為 UTF-8。否則,UTF8 標誌將保持關閉狀態,結果字串為 N8CS。這是唯一可能導致 N8CS 位元組字串的解碼函式。如果返回 false$utf8_string未正確編碼為 UTF-8;否則返回 true
$utf8_string = decode('UTF-8', $utf8_octets [, CHECK]) 開啟 將 UTF-8 八位位元組序列解碼為 UTF-8 字串。遵循嚴格的官方 UTF-8 解碼規則(有關討論,請參見上一節)。
$utf8_string = decode('utf8', $utf8_octets [, CHECK]) 開啟 將 UTF-8 八位位元組序列解碼為 UTF-8 字串。遵循寬鬆的解碼規則(有關討論,請參見上一節)。
$utf8_string = decode_utf8($utf8_octets [, CHECK]) 開啟 將 UTF-8 八位位元組序列解碼為 UTF-8 字串。等效於decode("utf8", $utf8_octets),因此使用寬鬆解碼。
$octet_count = utf8::upgrade($n8cs_string); 開啟 就地將 N8CS 位元組字串轉換為相應的 UTF-8 字串。返回現在用於以 UTF-8 形式內部表示字串的八位位元組數。此函式應用於將 N8CS 位元組字串中包含的字元轉換為 UTF-8,從而避免 Perl 5“Unicode 錯誤”0x80-0xFFrange to UTF-8, thereby avoiding the Perl 5 "Unicode Bug".
utf8::encode($string) 關閉 就地將 N8CS 或 UTF-8 $string 轉換為 UTF-8 八位位元組序列。
$utf8_octets = encode('UTF-8', $string [, CHECK]) 關閉 將 N8CS 或 UTF-8$string編碼為 UTF-8 八位位元組序列。遵循嚴格的官方 UTF-8 編碼規則(有關討論,請參見上一節)。
$utf8_octets = encode('utf8', $string) 關閉 將 N8CS 或 UTF-8$string編碼為 UTF-8 八位位元組序列。遵循寬鬆的 UTF-8 編碼規則(有關討論,請參見上一節)。由於所有可能的字元都有寬鬆的 utf8 表示形式,因此此函式不會失敗。
$utf8_octets = encode_utf8($string) 關閉 將 N8CS 或 UTF-8$string編碼為 UTF-8 八位位元組序列。等效於encode("utf8", $string),因此使用寬鬆編碼。由於所有可能的字元都有寬鬆的 utf8 表示形式,因此此函式不會失敗。
$flag = utf8::downgrade($utf8_string [, FAIL_OK]); 關閉 就地將 UTF-8 字串轉換為等效的 N8CS 位元組字串。如果失敗,則$utf8_string不能用 N8CS 編碼表示。如果 FAIL_OK 為真,則在失敗時會死亡,否則返回 false。成功時返回 true

Perl 字元編碼

[edit | edit source]

要確定 Perl 支援哪些字元編碼

perl -MEncode -le "print for Encode->encodings(':all')"

重要的是要記住,Perl 內部只使用兩種字元編碼:本機/位元組和 UTF-8/字元。任何使用除 N8CS 之外的編碼(平臺的本機 8 位字元集,通常為 ISO-8859-1/Latin-1)的字元都必須在進入 Perl 時解碼。

網站“x”使用什麼?

[edit | edit source]

檢視頁面,然後在瀏覽器中,檢視->字元編碼以檢視選擇了哪種編碼。另外,檢視 HTML 原始碼,看看是否存在元標籤

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

您還可以使用以下方法檢視返回了哪個 Content-Type 標頭

$ lwp-request -de www.bing.com | grep Content

此維基使用 UTF-8。

HTML 字元實體

[edit | edit source]

在您的 UTF-8 旅程中,您可能會遇到 HTML 字元實體。從 HTML 4.0 開始,支援 252 個 字元實體。每個實體都有一個 Unicode 程式碼點和一個實體名稱。可以在 HTML 標記中使用它們中的任何一個。例如,註冊符號可以在 HTML 中表示為&#174;&reg;

許多字型支援此字元集,如果該字元集足夠滿足您的應用程式需求,則可能不需要 UTF-8,但您的應用程式需要在需要特殊字元的地方使用 HTML 編碼。

作業系統和 Unicode

[edit | edit source]

有趣的是,流行的作業系統使用哪種 Unicode 編碼。從 維基百科 中可以看出:“Windows NT(及其後代,Windows 2000、Windows XP、Windows Vista 和 Windows 7)使用 UTF-16 作為唯一的內部字元編碼。Java 和 .NET 位元組碼環境、Mac OS X 和 KDE 也將其用於內部表示。UTF-8 已成為大多數類 Unix 作業系統的主要儲存編碼(儘管某些庫也使用其他編碼),因為它可以相對輕鬆地替換傳統的擴充套件 ASCII 字元集。”

參考資料

[edit | edit source]

腳註

[edit | edit source]

^ - N8CS 是為本文件創造的術語。不要指望在其他地方看到這個術語。


前一個:PSGI 索引 下一個:Perl 6
華夏公益教科書