# **第 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` 对象不需要指定内部编码)。
如果没有设置外部编码,或者字符串的编码与外部编码一致,则不会进行编码的转换。在需要进行转换的时候,如果输出的字符串的编码不正确(比如实际上是日语字符串,但编码却是中文),或者是无法互相转换的编码组合(例如用于日语与中文的编码),这时程序就会抛出异常。
![{%}](https://box.kancloud.cn/2015-10-26_562e0206f36dc.png)
**图 19.1 输出时与编码相关的行为**
-
**输入时编码的作用**
`IO` 的读取(输入)会稍微复杂一点。首先,如果外部编码没有设置,则会使用 `Encoding.default_external` 的值作为外部编码。
设定了外部编码,但内部编码没设定的时候,则会将读取的字符串的编码设置为 `IO` 对象的外部编码。这种情况下并不会进行编码的转换,而是将文件、控制台输入的数据原封不动地保存为 `String` 对象。
最后,外部编码和内部编码都设定的时候,则会执行由外部编码转换为内部编码的处理。输入与输出的情况一样,在编码转换的过程中如果数据格式或者编码组合不正确,程序都会抛出异常。
大家或许会感觉有点复杂,其实只要使用的环境与实际使用的数据的编码一致,我们就不需要考虑编码的转换。另外一方面,如果执行环境与数据的编码不一致,那么我们就需要在程序里有意识地处理编码问题。
![{%}](https://box.kancloud.cn/2015-10-26_562e020718d80.png)
**图 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 参考集
- 后记
- 谢辞