檢視原始碼 在 Erlang 中使用 Unicode

Unicode 實作

實作對 Unicode 字元集的支持是一個持續進行的過程。Erlang 增強提案 (EEP) 10 概述了 Unicode 支持的基本原理,並指定了二進制文件中所有 Unicode 感知模組未來應處理的預設編碼。

以下是目前已完成工作的概述

  • EEP10 中描述的功能已在 Erlang/OTP R13A 中實作。

  • Erlang/OTP R14B01 新增了對 Unicode 檔案名稱的支持,但它並不完整,並且在無法保證檔案名稱編碼的平台上預設為停用。

  • Erlang/OTP R16A 支援 UTF-8 編碼的原始碼,並增強了許多應用程式,以支援 Unicode 編碼的檔案名稱,以及在許多情況下支援 UTF-8 編碼的檔案。最值得注意的是,對 file:consult/1 讀取的檔案支援 UTF-8,發布處理程序支援 UTF-8,以及 I/O 系統中對 Unicode 字元集的更多支援。

  • 在 Erlang/OTP 17.0 中,Erlang 原始碼檔案的預設編碼已切換為 UTF-8。

  • 在 Erlang/OTP 20.0 中,原子和函數可以包含 Unicode 字元。模組名稱、應用程式名稱和節點名稱仍然限制在 ISO Latin-1 範圍內。

    unicode 中新增了對標準化形式的支持,而 string 模組現在可以處理 utf8 編碼的二進制檔案。

本節概述了目前的 Unicode 支持,並提供了一些使用 Unicode 資料的訣竅。

理解 Unicode

在 Erlang 中使用 Unicode 的經驗清楚地表明,理解 Unicode 字元和編碼並不像人們想像的那麼容易。該領域的複雜性和標準的含義需要徹底理解以前很少考慮的概念。

此外,Erlang 的實作需要理解許多 (Erlang) 程式設計師從未遇到過的問題。要理解和使用 Unicode 字元,即使您是一位經驗豐富的程式設計師,也需要徹底研究該主題。

例如,考慮大小寫字母轉換的問題。閱讀標準會讓您意識到,並非所有腳本中都有簡單的一對一映射,例如:

  • 在德語中,字母 "ß" (sharp s) 是小寫,但大寫等效項是 "SS"。
  • 在希臘語中,字母 "Σ" 有兩種不同的小寫形式,在單字末尾是 "ς",在其他地方是 "σ"。
  • 在土耳其語中,帶點和不帶點的 "i" 都存在小寫和大寫形式。
  • 西里爾字母 "I" 通常沒有小寫形式。
  • 沒有大寫概念(或小寫概念)的語言。

因此,轉換函數不僅要一次處理一個字元,還可能要處理整個句子、要翻譯成的自然語言、輸入和輸出字串長度的差異等等。Erlang/OTP 目前沒有針對語言特定處理的 Unicode uppercase/lowercase 功能,但公開可用的程式庫可以解決這些問題。

另一個例子是有重音的字元,其中相同的字形有兩種不同的表示形式。瑞典字母 "ö" 就是一個例子。Unicode 標準為它設定了一個碼位,但您也可以將它寫成 "o" 後面跟著 "U+0308" (組合變音符號,簡單來說,最後一個字母上方要有 "¨")。它們具有相同的字形,使用者可感知字元。在大多數情況下,它們是相同的,但具有不同的表示形式。例如,MacOS X 會轉換所有檔案名稱以使用組合變音符號,而大多數其他程式(包括 Erlang)會在列出目錄時嘗試通過執行相反的操作來隱藏它。無論如何完成,通常都需要標準化此類字元以避免混淆。

例子可以列出很多。當程式只考慮一到兩種語言時,需要一種不需要的知識。人類語言和腳本的複雜性肯定使在構建通用標準時成為一項挑戰。在您的程式中正確支持 Unicode 需要付出努力。

什麼是 Unicode

Unicode 是一種標準,它為所有已知的、現存的或已消失的腳本定義碼位(數字)。原則上,任何語言中使用的每個符號都有一個 Unicode 碼位。Unicode 碼位由非營利組織 Unicode 聯盟定義和發布。

隨著一個通用字元集在程式在全球環境中使用時所帶來的巨大好處,對 Unicode 的支持在全球計算領域不斷增長。除了標準的基礎,即所有腳本的碼位之外,還提供了一些編碼標準

理解編碼和 Unicode 字元之間的差異至關重要。Unicode 字元是根據 Unicode 標準的碼位,而編碼是表示此類碼位的方式。編碼僅是一種表示標準。例如,UTF-8 可以用於表示 Unicode 字元集中非常有限的部分(例如 ISO-Latin-1)或完整的 Unicode 範圍。它只是一種編碼格式。

由於所有字元集都限制為 256 個字元,因此每個字元都可以儲存在一個位元組中,因此字元實際上只有一種實用的編碼。以一個位元組編碼每個字元非常普遍,以至於編碼甚至沒有被命名。在 Unicode 系統中,字元遠遠超過 256 個,因此需要一種通用的方式來表示它們。表示碼位的常用方法是編碼。這表示程式設計師有一個全新的概念,即字元表示的概念,這在以前不是問題。

不同的作業系統和工具支持不同的編碼。例如,Linux 和 MacOS X 選擇了 UTF-8 編碼,它與 7 位 ASCII 向後相容,因此對以純英語編寫的程式的影響最小。Windows 支持有限版本的 UTF-16,即所有字元可以儲存在一個 16 位實體中的碼位,這足以應對大多數現存語言。

以下是最廣泛使用的編碼

  • 位元組表示法 - 這不是正確的 Unicode 表示法,而是 Unicode 標準之前用於字元的表示法。它仍然可以用於表示 Unicode 標準中數字 < 256 的字元碼位,這與 ISO Latin-1 字元集完全對應。在 Erlang 中,這通常表示為 latin1 編碼,這有點誤導,因為 ISO Latin-1 是一種字元碼範圍,而不是一種編碼。

  • UTF-8 - 每個字元都儲存在一到四個位元組中,具體取決於碼位。該編碼與 7 位 ASCII 的位元組表示向後相容,因為所有 7 位字元都儲存在 UTF-8 中的一個位元組中。超過碼位 127 的字元儲存在更多位元組中,讓第一個字元中的最高有效位指示一個多位元組字元。有關編碼的詳細資訊,RFC 是公開提供的。

    請注意,對於碼位 128 到 255,UTF-8 與位元組表示法相容,因此 ISO Latin-1 位元組表示法通常與 UTF-8 不相容。

  • UTF-16 - 此編碼與 UTF-8 有許多相似之處,但基本單位是一個 16 位數字。這表示所有字元至少佔用兩個位元組,而一些高位數字則佔用四個位元組。某些聲稱使用 UTF-16 的程式、程式庫和作業系統僅允許儲存在一個 16 位實體中的字元,這通常足以處理現存語言。由於基本單位不止一個位元組,因此會出現位元組順序問題,這就是 UTF-16 存在大端和小端變體的原因。

    在 Erlang 中,適用時支持完整的 UTF-16 範圍,例如在 unicode 模組和位元語法中。

  • UTF-32 - 最直接的表示法。每個字元都儲存在一個 32 位數字中。一個字元不需要跳脫字元或任何可變數量的實體。所有 Unicode 碼位都可以儲存在一個 32 位實體中。與 UTF-16 一樣,存在位元組順序問題。UTF-32 可以是大端和小端。

  • UCS-4 - 基本上與 UTF-32 相同,但沒有一些由 IEEE 定義的 Unicode 語義,並且作為一個單獨的編碼標準用途不大。對於所有正常(也可能是異常)使用,UTF-32 和 UCS-4 是可以互換的。

某些數字範圍在 Unicode 標準中未使用,甚至某些範圍被認為無效。最值得注意的無效範圍是 16#D800-16#DFFF,因為 UTF-16 編碼不允許對這些數字進行編碼。這可能是因為 UTF-16 編碼標準從一開始就被期望能夠將所有 Unicode 字元保留在一個 16 位實體中,但後來被擴展,在 Unicode 範圍中留下了一個漏洞以處理向後相容性。

碼位 16#FEFF 用於位元組順序標記 (BOM),並且不鼓勵在其他上下文中使用該字元。但它是有效的,因為該字元是 "ZWNBS" (零寬度不換行空格)。BOM 用於識別編碼和位元組順序,適用於事先不知道此類參數的程式。BOM 的使用次數比預期的少,但隨著它們為程式提供對某個檔案的 Unicode 格式做出有根據的猜測的方法,它們可能會變得更加廣泛。

Unicode 支持的領域

為了在 Erlang 中支持 Unicode,已解決了各個領域的問題。本節簡要介紹每個領域,並在本使用手冊的後面進行更詳細的介紹。

  • 表示法 - 為了在 Erlang 中處理 Unicode 字元,需要清單和二進制檔案中都有通用的表示法。EEP (10) 和隨後在 Erlang/OTP R13A 中的初始實作確定了 Erlang 中 Unicode 字元的標準表示法。

  • 操作 - Unicode 字元需要由 Erlang 程式處理,這就是為什麼函式庫必須能夠處理它們的原因。在某些情況下,功能已添加到現有的介面中(例如,string 模組現在可以處理任何碼位的字串)。在某些情況下,則新增了新的功能或選項(例如,在 io 模組、檔案處理、unicode 模組和位元語法中)。如今,Kernel 和 STDLIB 中的大多數模組以及 VM 都已支援 Unicode。

  • 檔案 I/O - 就 Unicode 而言,I/O 是迄今為止問題最多的領域。檔案是儲存位元組的實體,而程式設計的慣例是將字元和位元組視為可互換的。對於 Unicode 字元,您必須在將資料儲存到檔案中時決定編碼。在 Erlang 中,您可以使用編碼選項開啟文字檔,以便可以從中讀取字元而不是位元組,但您也可以開啟檔案進行位元組式 I/O。

    Erlang I/O 系統的設計方式(或至少使用方式)是您希望任何 I/O 伺服器都能處理任何字串資料。然而,在使用 Unicode 字元時,情況已不再如此。Erlang 程式設計師現在必須了解資料最終到達的裝置的功能。此外,Erlang 中的埠是面向位元組的,因此在未先將其轉換為所選的編碼之前,無法將任意字串(Unicode)字元傳送到埠。

  • 終端 I/O - 終端 I/O 比檔案 I/O 稍微容易一些。輸出是供人類閱讀的,通常是 Erlang 語法(例如,在 shell 中)。任何 Unicode 字元都存在語法表示法,而無需顯示字符形狀(而是寫為 \x{HHH})。因此,即使終端本身不支援整個 Unicode 範圍,通常也可以顯示 Unicode 資料。

  • 檔案名稱 - 檔案名稱可以不同方式儲存為 Unicode 字串,具體取決於底層作業系統和檔案系統。程式可以相當輕鬆地處理這個問題。當檔案系統的編碼不一致時,就會出現問題。例如,Linux 允許使用任何位元組序列來命名檔案,並將這些位元組的解釋留給每個程式。在使用這些「透明」檔案名稱的系統上,必須透過啟動標誌告知 Erlang 檔案名稱的編碼。預設是位元組式解釋,這通常是錯誤的,但允許解釋所有檔案名稱。

    如果啟用 Unicode 檔案名稱轉換 (+fnu),則可以使用「原始檔案名稱」的概念來處理編碼錯誤的檔案名稱,而在預設情況下,此功能在某些平台上並未啟用。

  • 原始碼編碼 - Erlang 原始碼支援 UTF-8 編碼和位元組式編碼。在 Erlang/OTP R16B 中的預設值是位元組式 (latin1) 編碼。在 Erlang/OTP 17.0 中更改為 UTF-8。您可以使用檔案開頭的類似以下註解來控制編碼

    %% -*- coding: utf-8 -*-

    當然,這也要求您的編輯器支援 UTF-8。相同的註解也會被諸如 file:consult/1、發行處理程式等函式解譯,以便您可以將來源目錄中的所有文字檔都設定為 UTF-8 編碼。

  • 語言 - 使用 UTF-8 編碼的原始碼也可以讓您編寫字串文字、函式名稱和包含碼位 > 255 的 Unicode 字元的原子。模組名稱、應用程式名稱和節點名稱仍然限制在 ISO Latin-1 範圍內。二進位文字 (您在其中使用類型 /utf8) 也可以使用 Unicode 字元 > 255 來表示。使用 7 位元 ASCII 以外的字元來命名模組名稱或應用程式名稱可能會在檔案命名方案不一致的作業系統上造成問題,並可能會損害可攜性,因此不建議使用。

    EEP 40 建議該語言也允許變數名稱中使用 Unicode 字元 > 255。是否實作該 EEP 仍有待決定。

標準 Unicode 表示法

在 Erlang 中,字串是整數列表。直到 Erlang/OTP R13,字串才定義為以 ISO Latin-1 (ISO 8859-1) 字元集編碼,該字元集是 Unicode 字元集的子範圍,逐個碼位。

因此,字串的標準列表編碼很容易擴展到處理整個 Unicode 範圍。Erlang 中的 Unicode 字串是一個包含整數的列表,其中每個整數都是有效的 Unicode 碼位,並表示 Unicode 字元集中的一個字元。

ISO Latin-1 中的 Erlang 字串是 Unicode 字串的子集。

僅當字串包含碼位 < 256 時,才能使用例如 erlang:iolist_to_binary/1 直接將其轉換為二進位,或直接傳送到埠。如果字串包含 Unicode 字元 > 255,則必須決定編碼,並使用 unicode:characters_to_binary/1,2,3 將字串轉換為首選編碼的二進位。字串通常不是位元組的列表,就像在 Erlang/OTP R13 之前一樣,它們是字元的列表。字元通常不是位元組,它們是 Unicode 碼位。

二進位比較麻煩。為了效能考量,程式通常將文字資料儲存在二進位而不是列表中,主要是因為它們更緊湊(每個字元一個位元組,而不是列表的每個字元兩個字)。使用 erlang:list_to_binary/1,可以將 ISO Latin-1 Erlang 字串轉換為二進位,有效地使用位元組式編碼:每個字元一個位元組。這對於那些有限的 Erlang 字串來說很方便,但對於任意 Unicode 列表來說卻無法做到。

由於 UTF-8 編碼廣泛使用並在 7 位元 ASCII 範圍內提供一些回溯相容性,因此它被選為 Erlang 中二進位 Unicode 字元的標準編碼。

當 Erlang 中的函式庫函式要處理二進位中的 Unicode 資料時,會使用標準二進位編碼,但在外部通訊時當然不會強制執行。存在函式和位元語法來編碼和解碼二進位中的 UTF-8、UTF-16 和 UTF-32。但是,一般來說,處理二進位和 Unicode 的函式庫函式只處理預設編碼。

字元資料可以來自許多來源,有時可以混合使用字串和二進位。長期以來,Erlang 一直具有 iodataiolist 的概念,其中二進位和列表可以組合來表示位元組序列。以相同的方式,支援 Unicode 的模組通常允許二進位和列表的組合,其中二進位具有以 UTF-8 編碼的字元,而列表則包含此類二進位或表示 Unicode 碼位的數字

unicode_binary() = binary() with characters encoded in UTF-8 coding standard

chardata() = charlist() | unicode_binary()

charlist() = maybe_improper_list(char() | unicode_binary() | charlist(),
  unicode_binary() | nil())

模組 unicode 甚至支援與包含 UTF-8 以外其他編碼的二進位檔類似的混合,但這是一種特殊情況,允許與外部資料進行轉換。

external_unicode_binary() = binary() with characters coded in a user-specified
  Unicode encoding other than UTF-8 (UTF-16 or UTF-32)

external_chardata() = external_charlist() | external_unicode_binary()

external_charlist() = maybe_improper_list(char() | external_unicode_binary() |
  external_charlist(), external_unicode_binary() | nil())

基本語言支援

從 Erlang/OTP R16 開始,Erlang 原始檔可以使用 UTF-8 或位元組式 (latin1) 編碼來編寫。如需有關如何說明 Erlang 原始檔編碼的資訊,請參閱 epp 模組。從 Erlang/OTP R16 開始,可以使用 Unicode 編寫字串和註解。從 Erlang/OTP 20 開始,也可以使用 Unicode 編寫原子和函式。模組、應用程式和節點仍然必須使用 ISO Latin-1 字元集中的字元來命名。(語言中的這些限制與原始檔的編碼無關。)

位元語法

位元語法包含處理三種主要編碼中二進位資料的類型。這些類型命名為 utf8utf16utf32utf16utf32 類型可以是 big-endian 或 little-endian 變體

<<Ch/utf8,_/binary>> = Bin1,
<<Ch/utf16-little,_/binary>> = Bin2,
Bin3 = <<$H/utf32-little, $e/utf32-little, $l/utf32-little, $l/utf32-little,
$o/utf32-little>>,

為了方便起見,可以使用以下(或類似)語法,在二進位中使用 Unicode 編碼來編碼文字字串

Bin4 = <<"Hello"/utf16>>,

字串和字元文字

對於原始碼,語法有擴充功能 \OOO(反斜線後跟三個八進位數字)和 \xHH(反斜線後跟 x,後跟兩個十六進位字元),即 \x{H ...}(反斜線後跟 x,後跟左大括號、任意數量的十六進位數字,以及終止右大括號)。即使原始檔的編碼是位元組式 (latin1),這也允許以文字方式輸入具有任何碼位的字元到字串中。

在 shell 中,如果使用 Unicode 輸入裝置,或在儲存在 UTF-8 中的原始碼中,則 $ 可以直接後跟產生整數的 Unicode 字元。在以下範例中,會輸出西里爾文 с 的碼位

7> $с.
1089

啟發式字串偵測

在某些輸出函式和 shell 中的傳回值輸出中,Erlang 會嘗試以啟發式方式偵測列表和二進位中的字串資料。您通常會在以下情況看到啟發式偵測

1> [97,98,99].
"abc"
2> <<97,98,99>>.
<<"abc">>
3> <<195,165,195,164,195,182>>.
<<"åäö"/utf8>>

這裡,shell 會偵測包含可列印字元的列表或包含位元組式或 UTF-8 編碼的可列印字元的二進位。但是,什麼是可列印字元?一種觀點認為,Unicode 標準認為可列印的任何內容,根據啟發式偵測也是可列印的。然後,結果是幾乎任何整數列表都被視為字串,並且會列印各種字元,也可能是終端字型集中所缺少的字元(導致出現一些不受歡迎的通用輸出)。另一種方法是使其保持回溯相容性,以便僅使用 ISO Latin-1 字元集來偵測字串。第三種方法是讓使用者確切地決定哪些 Unicode 範圍應視為字元。

從 Erlang/OTP R16B 開始,您可以分別透過提供啟動標誌 +pc latin1+pc unicode 來選擇 ISO Latin-1 範圍或整個 Unicode 範圍。為了回溯相容性,預設值為 latin1。這僅控制如何進行啟發式字串偵測。預計未來會加入更多範圍,讓使用者可以根據與其相關的語言和區域來調整啟發式方法。

以下範例顯示了兩個啟動選項

$ erl +pc latin1
Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.10.1  (abort with ^G)
1> [1024].
[1024]
2> [1070,1085,1080,1082,1086,1076].
[1070,1085,1080,1082,1086,1076]
3> [229,228,246].
"åäö"
4> <<208,174,208,189,208,184,208,186,208,190,208,180>>.
<<208,174,208,189,208,184,208,186,208,190,208,180>>
5> <<229/utf8,228/utf8,246/utf8>>.
<<"åäö"/utf8>>
$ erl +pc unicode
Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.10.1  (abort with ^G)
1> [1024].
"Ѐ"
2> [1070,1085,1080,1082,1086,1076].
"Юникод"
3> [229,228,246].
"åäö"
4> <<208,174,208,189,208,184,208,186,208,190,208,180>>.
<<"Юникод"/utf8>>
5> <<229/utf8,228/utf8,246/utf8>>.
<<"åäö"/utf8>>

在範例中,您可以看到預設 Erlang shell 只將 ISO Latin1 範圍中的字元解譯為可列印的字元,並且只偵測具有這些「可列印」字元的列表或二進位是否包含字串資料。包含俄文單字 "Юникод" 的有效 UTF-8 二進位不會列印為字串。當以所有可列印的 Unicode 字元啟動 (+pc unicode) 時,shell 會將任何包含可列印 Unicode 資料(在二進位中,無論是 UTF-8 還是位元組式編碼)的內容輸出為字串資料。

當搭配 ~p~P 使用修飾符 t 時,io:format/2io_lib:format/2 和相關的函式也會使用這些啟發式方法

$ erl +pc latin1
Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.10.1  (abort with ^G)
1> io:format("~tp~n",[{<<"åäö">>, <<"åäö"/utf8>>, <<208,174,208,189,208,184,208,186,208,190,208,180>>}]).
{<<"åäö">>,<<"åäö"/utf8>>,<<208,174,208,189,208,184,208,186,208,190,208,180>>}
ok
$ erl +pc unicode
Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.10.1  (abort with ^G)
1> io:format("~tp~n",[{<<"åäö">>, <<"åäö"/utf8>>, <<208,174,208,189,208,184,208,186,208,190,208,180>>}]).
{<<"åäö">>,<<"åäö"/utf8>>,<<"Юникод"/utf8>>}
ok

請注意,這只會影響輸出時對列表和二進位的啟發式解釋。例如,無論 +pc 設定為何,~ts 格式序列總是會輸出有效的字元列表,因為程式設計師已明確要求字串輸出。

互動式 Shell

互動式 Erlang shell 可以支援 Unicode 輸入和輸出。

在 Windows 上,要正常運作,需要為 Erlang 應用程式安裝並選取合適的字型。如果您的系統上沒有合適的字型,請嘗試安裝DejaVu 字型,這些字型是免費提供的,然後在 Erlang shell 應用程式中選取該字型。

在類 Unix 作業系統上,終端機必須能夠處理輸入和輸出的 UTF-8(例如,透過現代版本的 XTerm、KDE Konsole 和 Gnome 終端機來完成),並且您的地區設定必須正確。例如,可以將 LANG 環境變數設定如下:

$ echo $LANG
en_US.UTF-8

大多數系統在 LANG 之前會處理變數 LC_CTYPE,因此如果設定了該變數,則必須將其設定為 UTF-8

$ echo $LC_CTYPE
en_US.UTF-8

LANGLC_CTYPE 設定必須與終端機的功能一致。Erlang 沒有可攜式的方式可以詢問終端機的 UTF-8 容量,我們必須依賴語言和字元類型設定。

為了調查 Erlang 對於終端機的看法,當 shell 啟動時,可以使用呼叫 io:getopts()

$ LC_CTYPE=en_US.ISO-8859-1 erl
Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.10.1  (abort with ^G)
1> lists:keyfind(encoding, 1, io:getopts()).
{encoding,latin1}
2> q().
ok
$ LC_CTYPE=en_US.UTF-8 erl
Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.10.1  (abort with ^G)
1> lists:keyfind(encoding, 1, io:getopts()).
{encoding,unicode}
2>

當(終於?)地區設定、字型和終端模擬器都設定好時,您可能已經找到一種在您想要的腳本中輸入字元的方法。為了測試,最簡單的方法是為其他語言新增一些鍵盤映射,通常透過桌面環境中的一些小程式來完成。

在 KDE 環境中,選取「KDE 控制中心(個人設定)」>「地區和協助工具」>「鍵盤配置」。

在 Windows XP 上,選取「控制台」>「地區及語言選項」,選取「語言」索引標籤,然後按一下「文字服務和輸入語言」方塊中的「詳細資料...」按鈕。

您的環境可能提供類似的方式來變更鍵盤配置。如果您不習慣這樣做,請確保您可以輕鬆地在鍵盤之間切換。例如,在 Erlang shell 中使用西里爾字元集輸入命令並不容易。

現在您已設定好進行一些 Unicode 輸入和輸出。最簡單的事情是在 shell 中輸入字串:

$ erl
Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.10.1  (abort with ^G)
1> lists:keyfind(encoding, 1, io:getopts()).
{encoding,unicode}
2> "Юникод".
"Юникод"
3> io:format("~ts~n", [v(2)]).
Юникод
ok
4>

雖然字串可以輸入為 Unicode 字元,但語言元素仍然限制為 ISO Latin-1 字元集。只有字元常數和字串才允許超出該範圍。

$ erl
Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.10.1  (abort with ^G)
1> $ξ.
958
2> Юникод.
* 1: illegal character
2>

Escripts 和非互動式 I/O

當 Erlang 在沒有互動式 shell 的情況下啟動時(-noshell-noinput 或作為 escript),Unicode 支援會使用環境變數來識別,就像互動式 shell 一樣。在非互動式工作階段中使用 Unicode 的方式與互動式工作階段相同。

在某些情況下,您可能需要能夠從 standard_io 讀取和寫入原始位元組。如果是這種情況,您需要將 standard_io_encoding 組態參數設定為 latin1,並使用 file API 來讀取和寫入資料(如檔案中的 Unicode 資料中所述)。

在下面的範例中,我們首先從 standard_io 讀取字元 ξ,然後印出它所表示的 charlist()

#!/usr/bin/env escript
%%! -kernel standard_io_encoding latin1

main(_) ->
  {ok, Char} = file:read_line(standard_io),
  ok = file:write(standard_io, string:trim(Char)),
  ok = file:write(standard_io, io_lib:format(": ~w~n",[string:trim(Char)])),
  ok.
$ escript test.es
ξ
ξ: [206,190]

ξ 通常會表示為整數 958,但由於我們使用的是位元組編碼(latin1),因此會表示為 206 和 190,這是表示 ξ 的 utf-8 位元組。當我們將這些位元組回傳到 standard_io 時,終端機將會將這些位元組視為 utf-8,並顯示正確的值,即使在 Erlang 中,我們從未知道它實際上是一個 Unicode 字串。

Unicode 檔案名稱

大多數現代作業系統都以某種方式支援 Unicode 檔案名稱。有很多不同的方法可以做到這一點,而 Erlang 預設會以不同的方式對待不同的方法。

  • 強制 Unicode 檔案命名 - Windows、Android 和大多數情況下的 MacOS X 都強制支援檔案名稱的 Unicode。在檔案系統中建立的所有檔案都具有可以一致解釋的名稱。在 MacOS X 和 Android 中,所有檔案名稱都是以 UTF-8 編碼擷取的。在 Windows 中,每個處理檔案名稱的系統呼叫都有一個特殊的 Unicode 感知變體,效果大致相同。在這些系統上,沒有非 Unicode 檔案名稱的檔案。因此,Erlang VM 的預設行為是在「Unicode 檔案名稱轉換模式」下運作。這表示檔案名稱可以指定為 Unicode 列表,該列表會自動轉換為基礎作業系統和檔案系統的正確名稱編碼。

    例如,在其中一個系統上執行 file:list_dir/1 可以傳回具有程式碼點 > 255 的 Unicode 列表,具體取決於檔案系統的內容。

  • 透明檔案命名 - 大多數 Unix 作業系統採用了更簡單的方法,即不強制執行 Unicode 檔案命名,而是依慣例。這些系統通常使用 UTF-8 編碼來表示 Unicode 檔案名稱,但不強制執行。在這樣的系統上,包含程式碼點從 128 到 255 的字元的檔案名稱可以命名為純 ISO Latin-1 或使用 UTF-8 編碼。由於沒有強制執行一致性,Erlang VM 無法對所有檔案名稱執行一致的轉換。

    在預設情況下,如果終端機支援 UTF-8,則在這些系統上,Erlang 會以 utf8 檔案名稱模式啟動,否則會以 latin1 模式啟動。

    latin1 模式下,檔案名稱會以位元組方式編碼。這允許系統中所有檔案名稱的列表表示。但是,名為「Östersund.txt」的檔案在 file:list_dir/1 中會顯示為「Östersund.txt」(如果檔案名稱是由建立檔案的程式以位元組方式 ISO Latin-1 編碼),或者更可能顯示為 [195,150,115,116,101,114,115,117,110,100],這是包含 UTF-8 位元組的列表(不是您想要的)。如果您在這樣的系統上使用 Unicode 檔案名稱轉換,則 file:list_dir/1 之類的功能會忽略非 UTF-8 檔案名稱。可以使用函式 file:list_dir_all/1 擷取它們,但錯誤編碼的檔案名稱會顯示為「原始檔案名稱」。

Unicode 檔案命名支援是在 Erlang/OTP R14B01 中引入的。以 Unicode 檔案名稱轉換模式運作的 VM 可以使用任何語言或字元集的檔案名稱(只要基礎作業系統和檔案系統支援)。Unicode 字元列表用於表示檔案名稱或目錄名稱。如果列出了檔案系統內容,您也會收到 Unicode 列表作為傳回值。支援位於 Kernel 和 STDLIB 模組中,這就是為什麼大多數應用程式(不明確要求檔案名稱位於 ISO Latin-1 範圍內)都能夠從 Unicode 支援中獲益,而無需變更。

在具有強制 Unicode 檔案名稱的作業系統上,這表示您可以更輕鬆地符合其他(非 Erlang)應用程式的檔案名稱。您還可以處理在 Windows 上至少無法存取的檔案名稱(因為它們的名稱無法以 ISO Latin-1 表示)。此外,您還可以避免在 MacOS X 上建立無法理解的檔案名稱,因為作業系統的 vfs 層會接受您的所有檔案名稱作為 UTF-8,而不會重寫它們。

對於大多數系統來說,即使它使用透明檔案命名,開啟 Unicode 檔案名稱轉換也沒有問題。極少有系統具有混合的檔案名稱編碼。一致的 UTF-8 命名系統在 Unicode 檔案名稱模式下運作良好。然而,它在 Erlang/OTP R14B01 中仍然被視為實驗性的,並且在這些系統上仍然不是預設設定。

Unicode 檔案名稱轉換是透過開關 +fnu 開啟的。在 Linux 上,未明確聲明檔案名稱轉換模式而啟動的 VM 預設為 latin1 作為原生檔案名稱編碼。在 Windows、MacOS X 和 Android 上,預設行為是 Unicode 檔案名稱轉換。因此,在這些系統上,預設情況下,file:native_name_encoding/0 會傳回 utf8(Windows 不在檔案系統層級使用 UTF-8,但 Erlang 程式設計師可以安全地忽略這一點)。如前所述,預設行為可以使用 VM 的選項 +fnu+fnl 來變更,請參閱 erl 程式。如果 VM 是在 Unicode 檔案名稱轉換模式下啟動的,則 file:native_name_encoding/0 會傳回原子 utf8。開關 +fnu 後面可以跟著 wie,以控制如何報告錯誤編碼的檔案名稱。

  • w 表示每當在目錄清單中「跳過」錯誤編碼的檔案名稱時,就會將警告傳送至 error_loggerw 是預設值。
  • i 表示會靜態忽略錯誤編碼的檔案名稱。
  • e 表示每當遇到錯誤編碼的檔案名稱(或目錄名稱)時,API 函式就會傳回錯誤。

請注意,如果連結指向無效的檔案名稱,file:read_link/1 總是會傳回錯誤。

在 Unicode 檔案名稱模式下,使用選項 {spawn_executable,...} 提供給 BIF open_port/2 的檔案名稱也會被解釋為 Unicode。使用 spawn_executable 時,選項 args 中指定的參數列表也是如此。可以使用二進位檔來避免引數的 UTF-8 轉換,請參閱關於原始檔案名稱的注意事項一節。

請注意,開啟檔案時指定的檔案編碼選項與檔案名稱編碼慣例無關。您可以很好地開啟包含以 UTF-8 編碼的資料的檔案,但其檔案名稱是以位元組方式 (latin1) 編碼,反之亦然。

注意

Erlang 驅動程式和 NIF 共用物件仍然不能以包含程式碼點 > 127 的名稱命名。此限制將在未來版本中移除。但是,Erlang 模組可以這樣做,但這絕對不是一個好主意,並且仍然被視為實驗性的。

關於原始檔案名稱的注意事項

注意

請注意,原始檔案名稱一定以與 OS 層級相同的方式編碼。

原始檔案名稱是在 ERTS 5.8.2 (Erlang/OTP R14B01) 中與 Unicode 檔案名稱支援一起引入的。系統中引入「原始檔案名稱」的原因是要能夠一致地表示在同一系統上以不同編碼指定的檔案名稱。讓 VM 自動將非 UTF-8 的檔案名稱轉換為 Unicode 字元列表似乎很實用,但這會導致重複的檔案名稱和其他不一致的行為。

假設有一個目錄包含一個名為 "björn" 的檔案,其編碼為 ISO Latin-1,而 Erlang VM 則以 Unicode 檔案名稱模式運作(因此期望檔案名稱為 UTF-8 編碼)。ISO Latin-1 名稱並非有效的 UTF-8,因此可能會有人認為,在例如 file:list_dir/1 中進行自動轉換會是個好主意。但如果我們稍後嘗試開啟該檔案,並將名稱作為 Unicode 列表(從 ISO Latin-1 檔案名稱神奇地轉換而來)會發生什麼事?VM 會將檔案名稱轉換為 UTF-8,因為這是預期的編碼。實際上,這意味著嘗試開啟名為 <<"björn"/utf8>> 的檔案。這個檔案不存在,即使它存在,也不會與列出的檔案相同。我們甚至可以建立兩個名為 "björn" 的檔案,一個使用 UTF-8 編碼,另一個則否。如果 file:list_dir/1 自動將 ISO Latin-1 檔案名稱轉換為列表,我們會得到兩個相同的檔案名稱作為結果。為了避免這種情況,我們必須區分根據 Unicode 檔案命名慣例正確編碼的檔案名稱(即 UTF-8),以及在該編碼下無效的檔案名稱。透過常用的函數 file:list_dir/1,錯誤編碼的檔案名稱在 Unicode 檔案名稱轉換模式下會被忽略,但透過函數 file:list_dir_all/1,具有無效編碼的檔案名稱會以「原始」檔案名稱的形式傳回,也就是以二進位資料的形式傳回。

file 模組接受原始檔案名稱作為輸入。open_port({spawn_executable, ...} ...) 也接受它們。如前所述,在 open_port({spawn_executable, ...} ...) 的選項列表中指定的參數會經過與檔案名稱相同的轉換,這表示執行檔也會收到 UTF-8 編碼的參數。為了保持檔案名稱處理方式的一致性,這種轉換會被避免,也就是將參數以二進位資料的形式提供。

在 Erlang/OTP R14B01 中,強制在預設情況下不使用 Unicode 檔案名稱轉換模式的系統上啟用此模式被認為是實驗性的。這是因為最初的實作並未忽略錯誤編碼的檔案名稱,因此原始檔案名稱可能會意外地在整個系統中傳播。從 Erlang/OTP R16B 開始,錯誤編碼的檔案名稱僅會透過特殊函數(例如 file:list_dir_all/1)擷取。由於對現有程式碼的影響較小,因此現在支援此功能。預期 Unicode 檔案名稱轉換將在未來版本中成為預設模式。

即使您在沒有 VM 自動執行 Unicode 檔案名稱轉換的情況下運作,您仍然可以使用以 UTF-8 編碼的原始檔案名稱來存取和建立 UTF-8 編碼的檔案名稱。無論 Erlang VM 的啟動模式為何,強制使用 UTF-8 編碼在某些情況下可能是個好主意,因為使用 UTF-8 檔案名稱的慣例正在普及。

關於 MacOS X 的注意事項

MacOS X 的 vfs 層會以積極的方式強制使用 UTF-8 檔案名稱。舊版本會透過拒絕建立不符合 UTF-8 標準的檔案名稱來實現此目的,而較新版本則會將違規的位元組替換為 "%HH" 序列,其中 HH 是原始字元的十六進位表示法。由於 MacOS X 預設啟用 Unicode 轉換,因此唯一會遇到此問題的情況是使用 +fnl 標誌啟動 VM,或使用位元組編碼(latin1)的原始檔案名稱。如果使用包含 127 到 255 字元的位元組編碼的原始檔案名稱來建立檔案,則無法使用與建立時相同的名稱來開啟該檔案。除了保持檔案名稱的正確編碼之外,沒有解決此行為的方法。

MacOS X 會重新組織檔案名稱,使重音符號等的表示方式使用「組合字元」。例如,字元 ö 表示為碼位 [111,776],其中 111 是字元 o,而 776 是特殊的重音符號字元「組合分音符號」。這種正規化 Unicode 的方式在其他情況下很少使用。Erlang 在擷取時會以相反的方式正規化這些檔案名稱,使使用組合重音符號的檔案名稱不會傳遞到 Erlang 應用程式。在 Erlang 中,檔案名稱 "björn" 會被擷取為 [98,106,246,114,110],而不是 [98,106,117,776,114,110],儘管檔案系統可能會認為不同。當存取檔案時,會重新進行正規化為組合重音符號,因此 Erlang 程式設計師通常可以忽略這一點。

環境和參數中的 Unicode

環境變數及其解譯方式與檔案名稱的處理方式大致相同。如果啟用 Unicode 檔案名稱,則環境變數以及 Erlang VM 的參數都應使用 Unicode 編碼。

如果啟用 Unicode 檔案名稱,則對 os:getenv/0,1os:putenv/2os:unsetenv/1 的呼叫會處理 Unicode 字串。在類 Unix 平台上,內建函式會將 UTF-8 中的環境變數轉換為 Unicode 字串,反之亦然,可能包含 > 255 的碼位。在 Windows 上,會使用環境系統 API 的 Unicode 版本,並允許使用 > 255 的碼位。

在類 Unix 作業系統上,如果啟用 Unicode 檔案名稱,則參數預期為 UTF-8 編碼,且不進行轉換。

Unicode 感知模組

Erlang/OTP 中的大多數模組在某種意義上是不感知 Unicode 的,它們沒有 Unicode 的概念,也不應該有。它們通常處理非文字或以位元組為導向的資料(例如 gen_tcp)。

處理文字資料的模組(例如 io_libstring)有時會進行轉換或擴展,以便能夠處理 Unicode 字元。

幸運的是,大多數文字資料都儲存在列表中,並且範圍檢查很少,因此像 string 這樣的模組對於 Unicode 字串運作良好,幾乎不需要轉換或擴展。

然而,有些模組會被更改為明確感知 Unicode。這些模組包括:

  • unicode - unicode 模組顯然是感知 Unicode 的。它包含在不同 Unicode 格式之間進行轉換的函數,以及一些用於識別位元組順序標記的實用程式。處理 Unicode 資料的程式很少能不使用此模組。

  • io - io 模組已隨著實際的 I/O 協定進行擴展,以處理 Unicode 資料。這表示許多函數需要二進位資料採用 UTF-8 編碼,並且有一些修飾符可以格式化控制序列,以允許輸出 Unicode 字串。

  • filegroupuser - 整個系統的 I/O 伺服器都可以處理 Unicode 資料,並且具有在輸出或輸入時轉換資料到/從裝置的選項。如前所示,shell 模組支援 Unicode 終端機,而 file 模組允許在磁碟上與各種 Unicode 格式之間進行轉換。

    但是,讀取和寫入包含 Unicode 資料的檔案最好不要使用 file 模組,因為它的介面是以位元組為導向的。以 Unicode 編碼(例如 UTF-8)開啟的檔案最好使用 io 模組進行讀取或寫入。

  • re - re 模組允許將 Unicode 字串作為特殊選項進行匹配。由於該程式庫以在二進位資料中進行匹配為中心,因此 Unicode 支援以 UTF-8 為中心。

  • wx - 圖形程式庫 wx 對 Unicode 文字提供廣泛的支援。

string 模組對於 Unicode 字串和 ISO Latin-1 字串都能完美運作,但語言相關的函數 string:uppercase/1string:lowercase/1 除外。這兩個函數目前的形式永遠無法正確地處理 Unicode 字元,因為在文字大小寫轉換時需要考慮語言和地區設定問題。在國際環境中轉換大小寫是一個尚未在 OTP 中解決的龐大主題。

檔案中的 Unicode 資料

雖然 Erlang 可以處理多種形式的 Unicode 資料,但不代表任何檔案的內容都可以是 Unicode 文字。外部實體(例如埠和 I/O 伺服器)通常不支援 Unicode。

埠始終是以位元組為導向的,因此在將不確定是否為位元組編碼的資料傳送到埠之前,請確保以適當的 Unicode 編碼對其進行編碼。有時,這表示只有部分資料必須編碼為 UTF-8 等。某些部分可以是二進位資料(例如長度指示器)或其他不應進行字元編碼的內容,因此不存在自動轉換。

I/O 伺服器的行為略有不同。連接到終端機(或 stdout)的 I/O 伺服器通常可以處理 Unicode 資料,無論編碼選項為何。當預期現代環境但不希望在寫入到舊式終端機或管道時當機時,這很方便。

檔案可以具有編碼選項,使其通常可由 io 模組使用(例如 {encoding,utf8}),但預設會以位元組導向的檔案開啟。 file 模組是以位元組為導向的,因此只能使用該模組寫入 ISO Latin-1 字元。如果要將 Unicode 資料輸出到具有其他 encoding 而非 latin1(位元組編碼)的檔案,請使用 io 模組。令人有些困惑的是,使用例如 file:open(Name,[read,{encoding,utf8}]) 開啟的檔案無法使用 file:read(File,N) 正確讀取,但可以使用 io 模組從中擷取 Unicode 資料。原因在於 file:readfile:write(及其相關函數)純粹是以位元組為導向的,並且應該如此,因為這是存取文字檔案以外的檔案的方式,即逐位元組存取。與埠一樣,您可以透過「手動」將資料轉換為所選的編碼(使用 unicode 模組或位元語法),然後將其輸出到位元組編碼(latin1)的檔案中,將編碼資料寫入檔案。

建議

  • 對於以位元組方式存取開啟的檔案({encoding,latin1}),請使用 file 模組。
  • 當存取具有任何其他編碼(例如 {encoding,utf8})的檔案時,請使用 io 模組。

從檔案讀取 Erlang 語法的函數會識別 coding: 註解,因此可以處理輸入中的 Unicode 資料。將 Erlang 項寫入檔案時,建議在適用的情況下插入此類註解。

$ erl +fna +pc unicode
Erlang R16B (erts-5.10.1) [source]  [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.10.1  (abort with ^G)
1> file:write_file("test.term",<<"%% coding: utf-8\n[{\"Юникод\",4711}].\n"/utf8>>).
ok
2> file:consult("test.term").
{ok,[[{"Юникод",4711}]]}

選項摘要

Unicode 支援由命令列開關、某些標準環境變數以及您正在使用的 OTP 版本控制。大多數選項主要影響 Unicode 資料的顯示方式,而不是標準程式庫中 API 的功能。這表示 Erlang 程式通常不需要關心這些選項,它們更多的是用於開發環境。Erlang 程式可以編寫成無論系統類型或生效的 Unicode 選項為何,都能良好運作。

以下是影響 Unicode 的設定摘要:

  • LANGLC_CTYPE 環境變數 - 作業系統中的語言設定主要影響 shell。只有當環境告知允許 UTF-8 時,終端機(也就是群組領導者)才會以 {encoding, unicode} 運作。此設定是為了對應您正在使用的終端機。

    如果 Erlang 啟動時帶有 +fna 標誌(從 Erlang/OTP 17.0 開始的預設值),環境也可能會影響檔名的解讀。

    您可以使用 io:getopts() 呼叫來檢查此設定,它會提供一個選項列表,其中包含 {encoding,unicode}{encoding,latin1}

  • erl(1)+pc {unicode|latin1} 標誌 - 當在 shell 和 io/ io_lib:format 中使用 "~tp"~tP 格式化指令進行啟發式字串偵測時,此標誌會影響哪些內容被解讀為字串資料,如先前所述。

    您可以使用 io:printable_range/0 呼叫來檢查此選項,它會傳回 unicodelatin1。為了與未來(預期)的設定擴展相容,最好使用 io_lib:printable_list/1 來檢查列表是否根據設定可列印。該函數會考慮 io:printable_range/0 傳回的新可能設定。

  • erl(1)+fn{l|u|a} [{w|i|e}] 標誌 - 此標誌會影響檔名的解讀方式。在具有透明檔名系統的作業系統上,必須指定此選項才能允許使用 Unicode 字元命名檔案(並正確解讀包含字元 > 255 的檔名)。

    • +fnl 表示按位元組解讀檔名,這是在 UTF-8 檔案命名普及之前表示 ISO Latin-1 檔名的常用方式。
    • +fnu 表示檔名以 UTF-8 編碼,這是當今通用的方案(儘管不是強制性的)。
    • +fna 表示您根據環境變數 LANGLC_CTYPE 自動選擇 +fnl+fnu。這確實是樂觀的啟發式方法,沒有任何強制使用者擁有與檔案系統編碼相同的終端機,但通常情況下是這樣。這是所有類 Unix 作業系統上的預設值,除了 MacOS X。

    可以使用函數 file:native_name_encoding/0 讀取檔名轉換模式,它會傳回 latin1(按位元組編碼)或 utf8

  • epp:default_encoding/0 - 此函數會傳回目前執行版本中 Erlang 原始檔的預設編碼(如果沒有編碼註解)。在 Erlang/OTP R16B 中,傳回 latin1(按位元組編碼)。從 Erlang/OTP 17.0 開始,傳回 utf8

    每個檔案的編碼可以使用註解來指定,如 epp 模組中所述。

  • io:setopts/1,2standard_io_encoding - 當 Erlang 啟動時,standard_io 的編碼預設設定為 地區設定所指示的編碼。您可以將核心設定參數 standard_io_encoding 設定為所需的編碼,來覆寫預設值。

    您可以使用函數 io:setopts/2 來設定檔案或其他 I/O 伺服器的編碼。這也可以在開啟檔案時設定。將終端機(或其他 standard_io 伺服器)無條件設定為選項 {encoding,utf8},表示無論 Erlang 如何啟動或使用者的環境如何,UTF-8 編碼的字元都會寫入裝置。

    注意

    如果您使用 io:setopts/2 來變更 standard_io 的編碼,I/O 伺服器可能已經使用預設編碼讀取了一些資料。為了避免這種情況,您應該使用 standard_io_encoding 設定編碼。

    當以已知的編碼寫入或讀取文字檔案時,使用選項 encoding 開啟檔案很方便。

    您可以使用函數 io:getopts() 來檢索 I/O 伺服器的 encoding 設定。

使用範例

當開始使用 Unicode 時,人們經常會遇到一些常見問題。本節介紹一些處理 Unicode 資料的方法。

位元組順序標記

識別文字檔案中編碼的常見方法是在檔案開頭放置位元組順序標記 (BOM)。BOM 是以與其餘檔案相同的方式編碼的碼位 16#FEFF。如果要讀取此類檔案,則前幾個位元組(取決於編碼)不屬於文字的一部分。此程式碼概述瞭如何開啟一個被認為具有 BOM 的檔案,並設定檔案的編碼和位置,以進行進一步的循序讀取(最好使用 io 模組)。

請注意,程式碼中省略了錯誤處理

open_bom_file_for_reading(File) ->
    {ok,F} = file:open(File,[read,binary]),
    {ok,Bin} = file:read(F,4),
    {Type,Bytes} = unicode:bom_to_encoding(Bin),
    file:position(F,Bytes),
    io:setopts(F,[{encoding,Type}]),
    {ok,F}.

函數 unicode:bom_to_encoding/1 從至少四個位元組的二進制檔案中識別編碼。它會連同適用於設定檔案編碼的詞彙,傳回 BOM 的位元組長度,以便可以相應地設定檔案位置。請注意,函數 file:position/2 總是針對位元組偏移量運作,因此需要 BOM 的位元組長度。

開啟檔案進行寫入並將 BOM 放在開頭甚至更簡單

open_bom_file_for_writing(File,Encoding) ->
    {ok,F} = file:open(File,[write,binary]),
    ok = file:write(File,unicode:encoding_to_bom(Encoding)),
    io:setopts(F,[{encoding,Encoding}]),
    {ok,F}.

在這些情況下,最好使用 io 模組來處理檔案,因為該模組中的函數可以處理超出 ISO Latin-1 範圍的碼位。

格式化的 I/O

當讀寫到支援 Unicode 的實體時,例如為 Unicode 轉換開啟的檔案,您可能想要使用 io 模組或 io_lib 模組中的函數來格式化文字字串。由於向後相容性的原因,這些函數不接受任何列表作為字串,但在處理 Unicode 文字時需要特殊的轉換修飾符。修飾符是 t。當應用於格式化字串中的控制字元 s 時,它會接受所有 Unicode 碼位,並預期二進制檔案為 UTF-8。

1> io:format("~ts~n",[<<"åäö"/utf8>>]).
åäö
ok
2> io:format("~s~n",[<<"åäö"/utf8>>]).
åäÃ
ok

顯然,第二個 io:format/2 會產生不想要的輸出,因為 UTF-8 二進制檔案不在 latin1 中。為了向後相容性,未加前綴的控制字元 s 預期二進制檔案和僅包含碼位 < 256 的列表,以按位元組編碼的 ISO Latin-1 字元表示。

只要資料始終是列表,修飾符 t 就可以用於任何字串,但是當涉及二進制資料時,必須小心選擇正確的格式化字元。按位元組編碼的二進制檔案也被解讀為字串,即使使用 ~ts 輸出,但它可能會被誤認為是有效的 UTF-8 字串。因此,如果二進制檔案包含按位元組編碼的字元而不是 UTF-8,請避免使用 ~ts 控制。

函數 io_lib:format/2 的行為類似。它被定義為傳回字元的深度列表,並且可以透過簡單的 erlang:list_to_binary/1 輕鬆將輸出轉換為二進制資料,以便在任何裝置上輸出。當使用轉換修飾符時,列表可以包含無法儲存在一個位元組中的字元。然後,對 erlang:list_to_binary/1 的呼叫會失敗。但是,如果您要與之通訊的 I/O 伺服器支援 Unicode,則仍可以直接使用傳回的列表

$ erl +pc unicode
Erlang R16B (erts-5.10.1) [source] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.10.1 (abort with ^G)
1> io_lib:format("~ts~n", ["Γιούνικοντ"]).
["Γιούνικοντ","\n"]
2> io:put_chars(io_lib:format("~ts~n", ["Γιούνικοντ"])).
Γιούνικοντ
ok

Unicode 字串會以 Unicode 列表的形式傳回,由於 Erlang shell 使用 Unicode 編碼(並且啟動時所有 Unicode 字元都被視為可列印),因此會被識別為 Unicode 列表。Unicode 列表是函數 io:put_chars/2 的有效輸入,因此可以在任何支援 Unicode 的裝置上輸出資料。如果裝置是終端機,則如果編碼為 latin1,字元會以 \x{H...} 格式輸出。否則,以 UTF-8 輸出(對於非互動式終端機:"oldshell" 或 "noshell")或適合正確顯示字元的任何格式輸出(對於互動式終端機:常規 shell)。

因此,您始終可以將 Unicode 資料傳送到 standard_io 裝置。但是,只有當 encoding 設定為 latin1 以外的值時,檔案才會接受超出 ISO Latin-1 範圍的 Unicode 碼位。

UTF-8 的啟發式識別

雖然強烈建議在處理之前已知二進制資料中字元的編碼,但並非總是如此。在典型的 Linux 系統上,有 UTF-8 和 ISO Latin-1 文字檔案的混合,而且檔案中很少有任何 BOM 來識別它們。

UTF-8 的設計使得當解碼為 UTF-8 時,數字超出 7 位元 ASCII 範圍的 ISO Latin-1 字元很少被視為有效。因此,通常可以使用啟發式方法來確定檔案是 UTF-8 還是以 ISO Latin-1 編碼(每個字元一個位元組)。可以使用 unicode 模組來確定資料是否可以解讀為 UTF-8

heuristic_encoding_bin(Bin) when is_binary(Bin) ->
    case unicode:characters_to_binary(Bin,utf8,utf8) of
	Bin ->
	    utf8;
	_ ->
	    latin1
    end.

如果沒有檔案內容的完整二進制檔案,您可以改為逐塊處理檔案並逐部分檢查。來自函數 unicode:characters_to_binary/1,2,3 的回傳元組 {incomplete,Decoded,Rest} 會派上用場。從檔案讀取的一塊資料中的不完整剩餘部分會附加到下一塊,因此我們避免了在 UTF-8 編碼中讀取位元組塊時的字元邊界問題

heuristic_encoding_file(FileName) ->
    {ok,F} = file:open(FileName,[read,binary]),
    loop_through_file(F,<<>>,file:read(F,1024)).

loop_through_file(_,<<>>,eof) ->
    utf8;
loop_through_file(_,_,eof) ->
    latin1;
loop_through_file(F,Acc,{ok,Bin}) when is_binary(Bin) ->
    case unicode:characters_to_binary([Acc,Bin]) of
	{error,_,_} ->
	    latin1;
	{incomplete,_,Rest} ->
	    loop_through_file(F,Rest,file:read(F,1024));
	Res when is_binary(Res) ->
	    loop_through_file(F,<<>>,file:read(F,1024))
    end.

另一種選擇是嘗試以 UTF-8 編碼讀取整個檔案,看看它是否失敗。在這裡,我們需要使用函數 io:get_chars/3 讀取檔案,因為我們必須讀取碼位 > 255 的字元

heuristic_encoding_file2(FileName) ->
    {ok,F} = file:open(FileName,[read,binary,{encoding,utf8}]),
    loop_through_file2(F,io:get_chars(F,'',1024)).

loop_through_file2(_,eof) ->
    utf8;
loop_through_file2(_,{error,_Err}) ->
    latin1;
loop_through_file2(F,Bin) when is_binary(Bin) ->
    loop_through_file2(F,io:get_chars(F,'',1024)).

UTF-8 位元組的列表

由於各種原因,您有時可能會得到 UTF-8 位元組的列表。這不是常規的 Unicode 字元字串,因為每個列表元素不包含一個字元。相反,您會得到二進制檔案中擁有的「原始」UTF-8 編碼。這很容易透過首先將每個位元組轉換為二進制檔案,然後將 UTF-8 編碼的字元的二進制檔案轉換回 Unicode 字串來轉換為正確的 Unicode 字串

utf8_list_to_string(StrangeList) ->
  unicode:characters_to_list(list_to_binary(StrangeList)).

雙重 UTF-8 編碼

當處理二進制數據時,您可能會遇到可怕的「雙重 UTF-8 編碼」問題,導致奇怪的字符被編碼到您的二進制數據或檔案中。換句話說,您可能會得到一個 UTF-8 編碼的二進制數據,而該數據又再次被編碼為 UTF-8。常見的情況是,您逐字節讀取一個檔案,但該檔案的內容已經是 UTF-8 編碼。如果您接著使用例如 unicode 模組,或以 {encoding,utf8} 選項開啟檔案並寫入,將這些位元組轉換為 UTF-8,那麼您將會把輸入檔案中的每個位元組都編碼為 UTF-8,而不是原始文本中的每個字符(一個字符可能已經用多個位元組編碼)。對於這種情況,沒有真正的解決辦法,除了要確定哪些數據是以哪種格式編碼的,並且永遠不要將 UTF-8 數據(可能是從檔案逐位元組讀取的)再次轉換為 UTF-8。

最常見的發生情況是,當您取得的是 UTF-8 的列表,而非正確的 Unicode 字串,然後將它們轉換為二進制數據或檔案中的 UTF-8。

wrong_thing_to_do() ->
  {ok,Bin} = file:read_file("an_utf8_encoded_file.txt"),
  MyList = binary_to_list(Bin), %% Wrong! It is an utf8 binary!
  {ok,C} = file:open("catastrophe.txt",[write,{encoding,utf8}]),
  io:put_chars(C,MyList), %% Expects a Unicode string, but get UTF-8
                          %% bytes in a list!
  file:close(C). %% The file catastrophe.txt contains more or less unreadable
                 %% garbage!

在將二進制數據轉換為字串之前,請確保您知道其中包含什麼。如果沒有其他選擇,請嘗試使用啟發式方法。

if_you_can_not_know() ->
  {ok,Bin} = file:read_file("maybe_utf8_encoded_file.txt"),
  MyList = case unicode:characters_to_list(Bin) of
    L when is_list(L) ->
      L;
    _ ->
      binary_to_list(Bin) %% The file was bytewise encoded
  end,
  %% Now we know that the list is a Unicode string, not a list of UTF-8 bytes
  {ok,G} = file:open("greatness.txt",[write,{encoding,utf8}]),
  io:put_chars(G,MyList), %% Expects a Unicode string, which is what it gets!
  file:close(G). %% The file contains valid UTF-8 encoded Unicode characters!