多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
# **第 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))。