# **第 19 章 Encoding 類**
在本章中,我們將介紹 `Encoding` 類、以及 Ruby 中編碼的相關用法。
-
**Ruby 的編碼與字符串**
介紹 Ruby 中字符串與編碼的使用方法。
-
**腳本編碼與魔法注釋**
再次介紹腳本編碼與魔法注釋
-
**Encoding 類**
介紹編碼的基礎——`Encoding` 類的相關用法。
-
**正則表達式與編碼**
說明正則表達式與編碼的關系。
-
**IO 類與編碼**
說明 `IO` 類與編碼的關系。
### **19.1 Ruby 的編碼與字符串**
字符編碼是計算機進行字符操作的基礎,這一點我們已經在第 14 章的專欄中做過了介紹。就像在專欄中介紹的那樣,字符編碼有多種,而且即使是在同一個程序中,有時候輸入 / 輸出的字符編碼也有可能不一樣。例如程序輸入是 UTF-8 字符編碼,而輸出卻是 Shift_JIS 字符編碼等情況。雖然“あ”的 UTF-8 的字符編碼與 Shift_JIS 的字符編碼實際上是不同的,但經過適當的轉換,也是可以編寫這樣的程序的。
至于程序如何處理字符編碼,不同的編程語言有不同的解決方案。Ruby 的每個字符串對象都包含“字符串數據本身”以及“該數據的字符編碼”兩個信息。其中,關于字符編碼的信息即我們一般所講的編碼。
創建字符串對象一般有兩種方法,一種是在腳本中直接以字面量的形式定義,另外一種是從程序的外部(文件、控制臺、網絡等)獲取字符串數據。數據的獲取方式決定了它的編碼方式。截取字符串的某部分,或者連接多個字符串生成新字符串等的時候,編碼會繼承原有的字符串的編碼。
程序向外部輸出字符串時,必須指定適當的編碼。
Ruby 會按照以下信息決定字符串對象的編碼,或者在輸入 / 輸出處理時轉換編碼。
-
**腳本編碼**
決定字面量字符串對象編碼的信息,與腳本的字符編碼一致。詳細內容請參考 19.2 節。
-
**內部編碼與外部編碼**
內部編碼是指從外部獲取的數據在程序中如何處理的信息。與之相反,外部編碼是指程序向外部輸出時與編碼相關的信息。兩者都與 `IO` 對象有關聯。詳細內容請參考 19.5 節。
### **19.2 腳本編碼與魔法注釋**
我們在第 1 章中簡單介紹過魔法注釋。Ruby 腳本的編碼就是通過在腳本的開頭書寫魔法注釋來指定的。
腳本自身的編碼稱為腳本編碼(script encoding)。腳本中的字符串、正則表達式的字面量會依據腳本編碼進行解釋。腳本編碼為 EUC-JP 時,字符串、正則表達式的字面量也都為 EUC-JP。同樣,如果腳本編碼為 Shift_JIS,那么字符串、正則表達式的字面量也為 Shift_JIS。
我們把指定腳本編碼的注釋稱為魔法注釋(magic comment)。Ruby 在解釋腳本前,會先讀取魔法注釋來決定腳本編碼。
魔法注釋必須寫在腳本的首行(第 1 行以 `#! ~` 開頭時,則寫在第 2 行)。下面是將腳本編碼指定為 UTF-8 的例子。
~~~
# encoding: utf-8
~~~
> **備注** 在 Unix 中,賦予腳本執行權限后,就可以直接執行腳本。這時,可以在文件開頭以 `#!` 命令的路徑 的形式來指定執行腳本的命令。在本書的例子中,我們經常使用 `>ruby` 腳本名 這樣的形式來表示 在命令行執行腳本的命令為 `ruby`,但若像“`#! /usr/bin/ruby`”這樣,在文件開頭寫上 ruby 命令的路徑的話,那么就能直接以 `>` 腳本名的形式執行腳本了。
此外,為了可以兼容 Emacs、VIM 等編輯器的編碼指定方式,我們也可以像下面這樣寫。
~~~
# -*- coding: utf-8 -*- # 編輯器為Emacs 的時候
# vim:set fileencoding=utf-8: # 編輯器為VIM 的時候
~~~
程序代碼的編碼會嚴格檢查是否與腳本編碼一致。因此,有時候直接寫上日語的字符串后就會產生錯誤 1。
1中文也需要注意同樣的問題。——譯者注
~~~
# encoding: US-ASCII
a = 'こんにちは' #=> invalid multibyte char (US-ASCII)
~~~
由于 US-ASCII 不能表示日語的字符串,因此會產生錯誤。在 Ruby1.9 中,沒有魔法注釋時默認腳本編碼也為 US-ASCII,因此也會產生這個錯誤。
為了使日語字符能正常顯示,必須指定適當的編碼 2。而在 Ruby2.0 中,由于沒有魔法注釋時的默認腳本編碼為 UTF-8,因此如果代碼是以 UTF-8 編碼編寫的話,那么就無須使用魔法注釋了。
2中文字符也一樣。——譯者注
但有時僅使用魔法注釋是不夠的。例如,使用特殊字符 `\u` 創建字符串后,即使腳本編碼不是 UTF-8,其生成的字符串也一定是 UTF-8。
~~~
# encoding: EUC-JP
a = "\u3042\u3044"
puts a #=> "あい"
p a.encoding #=> #<Encoding:UTF-8>
~~~
因此,必須使用 `encode!` 方法明確進行編碼轉換。
~~~
# encoding: EUC-JP
a = "\u3042\u3044"
a.encode!("EUC-JP")
p a.encoding #=> #<Encoding:EUC-JP>
~~~
這樣,變量 `a` 的字符串的編碼也就變為 EUC-JP 了。
### **19.3 Encoding 類**
我們可以用 `String#encoding` 方法來調查字符串的編碼。`String#encoding` 方法返回 `Encoding` 對象。
~~~
p "こんにちは".encoding #=> #<Encoding:UTF-8>
~~~
本例中的“こんにちは”字符串對象的編碼為 UTF-8。
> **備注** 日語 Windows 環境中的字符編碼一般為 Windows-31J。這是 Windows 專用的擴展自 Shift_JIS 的編碼,例如,Shift_JIS 中原本并沒有①。Windows-31J 還有一個別名叫 CP932(Microsoft code page932 的意思),在互聯網上就字符編碼討論時,有時候會用到這個名稱。3
3簡體中文 Windows 環境中使用的字符編碼為 GBK(CP936),向下兼容 GB2312 編碼。——譯者注
在腳本中使用不同的編碼時,需要進行必要的轉換。我們可以用 `String#encode` 方法轉換字符串對象的編碼。
~~~
str = "こんにちは"
p str.encoding #=> #<Encoding:UTF-8>
str2 = str.encode("EUC-JP")
p str2.encoding #=> #<Encoding:EUC-JP>
~~~
在本例中,我們嘗試把 UTF-8 字符串對象轉換為新的 EUC-JP 字符串對象。
在操作字符串時,Ruby 會自動進行檢查。例如,如果要連接不同編碼的字符串則會產生錯誤。
~~~
# encoding: utf-8
str1 = "こんにちは"
p str1.encoding #=> #<Encoding:UTF-8>
str2 = "あいうえお".encode("EUC-JP")
p str2.encoding #=> #<Encoding:EUC-JP>
str3 = str1 + str2 #=> incompatible character encodings: UTF-8
#=> and EUC-JP(Encoding::CompatibilityError)
~~~
為了防止錯誤,在連接字符串前,必須使用 `encode` 方法等把兩者轉換為相同的編碼。
還有,在進行字符串比較時,如果編碼不一樣,即使表面的值相同,程序也會將其判斷為不同的字符串。
~~~
# encoding: utf-8
p "あ" == "あ".encode("Shift_JIS") #=> false
~~~
另外,在本例中,用 `String#encode` 指定編碼時,除了可以使用編碼名的字符串外,還可以直接使用 `Encoding` 對象來指定。
### **Encoding 類的方法**
接下來,我們將會介紹 `Encoding` 類的方法。
-
**`Encoding.compatible?`(*str1, str2*)**
檢查兩個字符串的兼容性。這里所說的兼容性是指兩個字符串是否可以連接。可兼容則返回字符串連接后的編碼,不可兼容則返回 `nil`。
~~~
p Encoding.compatible?("AB".encode("EUC-JP"),
"あ".encode("UTF-8")) #=> #<Encoding:UTF-8>
p Encoding.compatible?("あ".encode("EUC-JP"),
"あ".encode("UTF-8")) #=> nil
~~~
AB 這個字符串的編碼無論是 EUC-JP 還是 UTF-8 都是一樣的,因此,將其轉換為 EUC-JP 后也可以與 UTF-8 字符串連接;而あ這個字符串則無法連接,因此返回 `nil`。
-
**`Encoding.default_external`**
返回默認的外部編碼,這個值會影響 `IO` 類的外部編碼,詳細內容請參考 19.5 節。
-
**`Encoding.default_internal`**
返回默認的內部編碼,這個值會影響 `IO` 類的內部編碼,詳細內容請參考 19.5 節。
-
**`Encoding.find`(*name*)**
返回編碼名 *name* 對應的 `Encoding` 對象。預定義的編碼名由不含空格的英文字母、數字與符號構成。查找編碼的時候不區分 *name* 的大小寫。
~~~
p Encoding.find("Shift_JIS") # => #<Encoding:Shift_JIS>
p Encoding.find("shift_JIS") # => #<Encoding:Shift_JIS>
~~~
表 19.1 為預定義的特殊的編碼名。
**表 19.1 特殊的編碼名**
| 名稱 | 意義 |
|-----|-----|
| `locale` | 根據本地信息決定的編碼 |
| `external` | 默認的外部編碼 |
| `internal` | 默認的內部編碼 |
| `filesystem` | 文件系統的編碼 |
-
**`Encoding.list`
`Encoding.name_list`**
返回 Ruby 支持的編碼一覽表。`list` 方法返回的是 `Encoding` 對象一覽表,Encoding.name_list 返回的是表示編碼名的字符串一覽表,兩者的結果都以數組形式返回。
~~~
p Encoding.list
#=> [#<Encoding:ASCII-8BIT>, #<Encoding:UTF-8>, ...
p Encoding.name_list
#=> ["ASCII-8BIT", "UTF-8", "US-ASCII", "Big5", ...
~~~
-
***enc*.`name`**
返回 `Encoding` 對象 *enc* 的編碼名。
~~~
p Encoding.find("shift_jis").name #=> "Shift_JIS"
~~~
-
***enc*.`names`**
像 EUC-JP、eucJP 這樣,有些編碼有多個名稱。這個方法會返回包含 `Encoding` 對象的名稱一覽表的數組。只要是這個方法中的編碼名稱,都可以在通過 `Encoding.find` 方法檢索時使用。
~~~
enc = Encoding.find("Shift_JIS")
p enc.names #=> ["Shift_JIS", "SJIS"]
~~~
> **專欄**
> **ASCII-8BIT 與字節串**
> ASCII-8BIT 是一個特殊的編碼,被用于表示二進制數據以及字節串。因此有時候我們也稱這個編碼為 BINARY。
> 此外,把字符串對象用字節串形式保存的時候也會用到這個編碼。例如,使用 `Array#pack` 方法將二進制數據生成為字符串時,或者使用 `Marsha1.dump` 方法將對象序列化后的數據生成為字符串時,都會使用該編碼。
> 下面是用 `Array#pack` 方法,把 IP 地址的 4 個數值轉換為 4 個字節的字節串。
~~~
str = [127, 0, 0, 1].pack("C4")
p str #=> "\x7F\x00\x00\x01"
p str.encoding # => #<Encoding:ASCII-8BIT>
~~~
> `pack` 方法的參數為字節串化時使用的模式,C4 表示 4 個 8 位的不帶符號的整數。執行結果為 4 個字節的字節串,編碼為 ASCII-8BIT。
> 此外,在使用 `open-uri` 庫等工具通過網絡獲取文件時,有時候并不知道字符編碼是什么。這時候的編碼也默認使用 ASCII-8BIT。
~~~
# encoding: utf-8
require 'open-uri'
str = open("http://www.example.jp/").read
p str.encoding #=> #<Encoding:ASCII-8BIT>
~~~
> 即使是編碼為 ASCII-8BIT 的字符串,實際上也還是正常的字符串,只要知道字符編碼,就可以使用 `force_encoding` 方法。這個方法并不會改變字符串的值(二進制數據),而只是改變編碼信息。
~~~
# encoding: utf-8
require 'open-uri'
str = open("http://www.example.jp/").read
str.force_encoding("Windows-31J")
p str.encoding #=> #<Encoding:Windows-31J>
~~~
> 這樣一來,我們就可以把 ASCII-8BIT 的字符串當作 Windows-31J 字符串來處理了。
> 使用 `force_encoding` 方法時,即使指定了不正確的編碼,也不會馬上產生錯誤,而是在對該字符串進行操作的時候才會產生錯誤。檢查編碼是否正確,可以用 `valid_encoding?` 方法,不正確時則返回 `false`。
~~~
str = "こんにちは"
str.force_encoding("US-ASCII") #=> 不會產生錯誤
str.valid_encoding? #=> false
str + "みなさん" #=> Encoding::CompatibilityError
~~~
### **19.4 正則表達式與編碼**
與字符串同樣,正則表達式也有編碼信息。
正則表達式的編碼即其匹配字符串的編碼。例如,用 EUC-JP 的正則表達式對象去匹配 UTF-8 字符串時就會產生錯誤,反之亦然。
~~~
# encoding: EUC-JP
a = "\u3042\u3044"
p /あ/ =~ a #=> incompatible encoding regexp match
#=> (EUC-JP regexp with UTF-8 string)
#=> (Encoding::CompatibilityError)
~~~
通常情況下,正則表達式字面量的編碼與代碼的編碼是一樣的。指定其他編碼的時候,可使用 `Regexp` 類的 `new` 方法。在這個方法中,表示模式第 1 個參數的字符串編碼,就是該正則表達式的編碼。
~~~
str = "模式".encode("EUC-JP")
re = Regexp.new(str)
p re.encoding # => #<Encoding:EUC-JP>
~~~
### **19.5 IO 類與編碼**
使用 `IO` 類進行輸入 / 輸出操作時編碼也非常重要。接下來,我們就向大家介紹一下 `IO` 與編碼的相關內容。
### **19.5.1 外部編碼與內部編碼**
每個 `IO` 對象都包含有外部編碼與內部編碼兩種編碼信息。外部編碼指的是作為輸入 / 輸出對象的文件、控制臺等的編碼,內部編碼指的是 Ruby 腳本中的編碼。`IO` 對象的編碼的相關方法如表 19.2 所示。
**表 19.2 與 IO 類編碼相關的方法**
<table border="1" data-line-num="256 257 258 259 260 261" width="90%"><thead><tr><th> <p class="表頭單元格">方法名</p> </th> <th> <p class="表頭單元格">意義</p> </th> </tr></thead><tbody><tr><td> <p class="表格單元格"><code>IO#external_encoding</code></p> </td> <td> <p class="表格單元格">返回 <code>IO</code> 的外部編碼</p> </td> </tr><tr><td> <p class="表格單元格"><code>IO#internal_encoding</code></p> </td> <td> <p class="表格單元格">返回 <code>IO</code> 的內部編碼</p> </td> </tr><tr><td> <p class="表格單元格"><code>IO#set_encoding</code></p> </td> <td> <p class="表格單元格">設定 <code>IO</code> 的編碼</p> </td> </tr></tbody></table>
沒有明確指定編碼時,`IO` 對象的外部編碼與內部編碼各自使用其默認值 `Encoding.default_external`、`Encoding.default_internal`。默認情況下,外部編碼會基于各個系統的本地信息設定,內部編碼不設定。Windows 環境下的編碼信息如下所示。
~~~
p Encoding.default_external #=> #<Encoding:Windows-31J>
p Encoding.default_internal #=> nil
File.open("foo.txt") do |f|
p f.external_encoding #=> #<Encoding:Windows-31J>
p f.internal_encoding #=> nil
end
~~~
### **19.5.2 編碼的設定**
在剛才的例子中我們打開了文本文件(foo.txt),但 `IO` 對象(`File` 對象)的編碼與文件的實際內容其實是沒關系的。因為編碼原本就只是用來說明如何處理字符的信息,因此對文本文件以外的文件并沒有多大作用。
在 17.3 節中說明如何按字節操作文件時,我們介紹了 `IO#seek` 方法與 `IO#read`(*size*)方法,這些方法都不受編碼影響,對任何數據都可以進行讀寫操作。`IO#read`(*size*)方法讀取的字符串的編碼為表示二進制數據的 ASCII-8BIT。
設定 `IO` 對象的編碼信息,可以通過使用 `IO#set_encoding` 方法,或者在 `File.open` 方法的參數中指定編碼來進行。
-
***io*.`set_encoding`(*encoding*)**
`IO#set_encoding` 方法以 " 外部編碼名 : 內部編碼名 " 的形式指定字符串 *encoding*。把外部編碼設置為 Shift_JIS,內部編碼設置為 UTF-8 的時候,可以像下面那樣設定。
~~~
$stdin.set_encoding("Shift_JIS:UTF-8")
p $stdin.external_encoding #=> #<Encoding:Shift_JIS>
p $stdin.internal_encoding #=> #<Encoding:UTF-8>
~~~
-
**`File.open`(*file,* "*mode:encoding*")**
為了在打開文件 *file* 時通過 `File.open` 方法指定編碼 *encoding*,可以在第二個參數中指定 *mode* 的后面用冒號(`:`)分割,并按順序指定外部編碼以及內部編碼(內部編碼可省略)。
~~~
# 指定外部編碼為UTF-8
File.open("foo.txt", "w:UTF-8")
# 指定外部編碼為Shift_JIS
# 指定內部編碼為UTF-8
File.open("foo.txt", "r:Shift_JIS:UTF-8")
~~~
### **19.5.3 編碼的作用**
接下來,我們來看看 `IO` 對象中設定的編碼信息是如何工作的。
-
**輸出時編碼的作用**
外部編碼影響 `IO` 的寫入(輸出)。在輸出的時候,會基于每個字符串的原有編碼和 `IO` 對象的外部編碼進行編碼的轉換(因此輸出用的 `IO` 對象不需要指定內部編碼)。
如果沒有設置外部編碼,或者字符串的編碼與外部編碼一致,則不會進行編碼的轉換。在需要進行轉換的時候,如果輸出的字符串的編碼不正確(比如實際上是日語字符串,但編碼卻是中文),或者是無法互相轉換的編碼組合(例如用于日語與中文的編碼),這時程序就會拋出異常。

**圖 19.1 輸出時與編碼相關的行為**
-
**輸入時編碼的作用**
`IO` 的讀取(輸入)會稍微復雜一點。首先,如果外部編碼沒有設置,則會使用 `Encoding.default_external` 的值作為外部編碼。
設定了外部編碼,但內部編碼沒設定的時候,則會將讀取的字符串的編碼設置為 `IO` 對象的外部編碼。這種情況下并不會進行編碼的轉換,而是將文件、控制臺輸入的數據原封不動地保存為 `String` 對象。
最后,外部編碼和內部編碼都設定的時候,則會執行由外部編碼轉換為內部編碼的處理。輸入與輸出的情況一樣,在編碼轉換的過程中如果數據格式或者編碼組合不正確,程序都會拋出異常。
大家或許會感覺有點復雜,其實只要使用的環境與實際使用的數據的編碼一致,我們就不需要考慮編碼的轉換。另外一方面,如果執行環境與數據的編碼不一致,那么我們就需要在程序里有意識地處理編碼問題。

**圖 19.2 輸入時與編碼相關的行為**
> **專欄**
> **UTF8-MAC 編碼**
> 在 Mac OS X 中,文件名中如果使用了濁點或者半濁點字符4,有時候就會產生一些奇怪的現象。
> 例如,創建文件 `ルビー.txt` 并執行下面的程序,可以發現,預計執行結果應該為 `found.`,但實際結果卻是 `not found.`。
> **代碼清單 utf8mac.rb**
~~~
# encoding: utf-8
Dir.glob("*.txt") do |filename|
if filename == "ルビー.txt"
puts "found."; exit
end
end
puts "not found."
~~~
> > **執行示例**
~~~
> touch ルビー.txt
> ruby utf8mac.rb
not found.
~~~
> 另一方面,執行以下腳本,這次會輸出 `found.`。
> **代碼清單 utf8mac_fix.rb**
~~~
# encoding: utf-8
Dir.glob("*.txt") do |filename|
if filename.encode("UTF8-MAC") == "ルビー.txt".encode("UTF8-MAC")
puts "found."; exit
end
end
puts "not found."
~~~
> > **執行示例**
~~~
> touch ルビー.txt
> ruby utf8mac_fix.rb
found.
~~~
> 這是由于 Mac OS X 中的文件系統使用的編碼不是 UTF-8,而是一種名為 UTF8-MAC(或者叫 UTF-8-MAC)的編碼的緣故。
> 那么,UTF8-MAC 是什么樣的編碼呢。我們通過下面的例子來看一下。
> **代碼清單 utf8mac_str.rb**
~~~
# encoding: utf-8
str = "ビ"
puts "size: #{str.size}"
p str.each_byte.map{|b| b.to_s(16)}
puts "size: #{str.encode("UTF8-MAC").size}"
p str.encode("UTF8-MAC").each_byte.map{|b| b.to_s(16)}
~~~
> > **執行示例**
~~~
> ruby utf8mac_str.rb
size: 1
["e3", "83", "93"]
size: 2
["e3", "83", "92", "e3", "82", "99"]
~~~
> 本例表示的是在 UTF-8 和 UTF8-MAC 這兩種編碼方式的情況下,分別以 16 進制的形式輸出字符串 " ビ " 的長度以及各個字節的值。從結果中我們可以看出,UTF-8 時的值為“`ec,83,93`”,UTF8-MAC 時則是“`e3,83,92,e3,82,99`”。而轉換為 UTF8-MAC 后,字符串的長度也變為了兩個字符。
> 在 UTF8-MAC 中,字符ビ(Unicode 中為 U+30D3)會分解為字符匕(U+30D2)與濁點字符(U+3099)兩個字符。用 UTF-8 表示則為 `8392E3` 與 `8299E3` 兩個字節串,因此就得到了之前的結果。
> 像這樣,如果把 Mac OS X 的文件系統當作是普通的 UTF-8 看待,往往就會有意料之外的事情發生。在操作日語文件、目錄時務必注意這個問題 5。
4即日語字母右上角的兩個小點和小圓圈。——譯者注
5除了日語字母外,一些“含附加符號”的字母也會做同樣的處理。例如,字符“ü”會拆分為字符“u”與字符“¨”的組合。雖然中文字符中一般很少有“附加符號”的情況,但在處理中文字符以外的字符時需要小心這個“陷阱”。——譯者注
### **練習題**
1. 定義 `to_utf8(str_gbk, str_gb2312)` 方法,連接 GBK 字符串 `str_gbk` 以及 GB2312 字符串 `str_gb2312`,并將連接后的字符串的編碼轉換為 UTF-8 后返回。
2. 創建編碼為 GBK 的文本文件,文件內容為“你好”,定義一個腳本,讀取該文件并將其按 UTF-8 編碼方式輸出。
3. 請找出 UTF-8 字符串 `str`,要求其 `str.encode("GB18030")` 與 `str.encode("GBK")` 的結果不一樣。
4. 在剛才的專欄的第二個腳本中,比較時雙方都轉換為了 UTF8-MAC。請修改 `if` 語句的條件,使 `ルビー.txt` 在比較時可以保持 UTF-8 編碼的形式。
> 參考答案:請到圖靈社區本書的“隨書下載”處下載([http://www.ituring.com.cn/book/1237](http://www.ituring.com.cn/book/1237))。
- 推薦序
- 譯者序
- 前言
- 本書的讀者對象
- 第 1 部分 Ruby 初體驗
- 第 1 章 Ruby 初探
- 第 2 章 便利的對象
- 第 3 章 創建命令
- 第 2 部分 Ruby 的基礎
- 第 4 章 對象、變量和常量
- 第 5 章 條件判斷
- 第 6 章 循環
- 第 7 章 方法
- 第 8 章 類和模塊
- 第 9 章 運算符
- 第 10 章 錯誤處理與異常
- 第 11 章 塊
- 第 3 部分 Ruby 的類
- 第 12 章 數值類
- 第 13 章 數組類
- 第 14 章 字符串類
- 第 15 章 散列類
- 第 16 章 正則表達式類
- 第 17 章 IO 類
- 第 18 章 File 類與 Dir 類
- 第 19 章 Encoding 類
- 第 20 章 Time 類與 Date 類
- 第 21 章 Proc 類
- 第 4 部分 動手制作工具
- 第 22 章 文本處理
- 第 23 章 檢索郵政編碼
- 附錄
- 附錄 A Ruby 運行環境的構建
- 附錄 B Ruby 參考集
- 后記
- 謝辭