Ruby 1.9+ 的字符编码
从 1.9 开始,Ruby 增加了对字符编码的支持。这篇文章基本上是看了 Ruby 2.0 镐头书第
17 章 Character Encoding
做的笔记,并补充了一些自己通过实验得到的结论。
Ruby 代码文件的编码
Ruby 源文件的默认编码:
你需要告诉 Ruby 你的 Ruby 代码文件使用的是什么编码,因为 Ruby 中的字符串字面量、 Symbol 字面量以及正则表达式字面量的字符编码在多数时候取决于定义他们的源文件的字 符编码。
Ruby 1.9 默认 Ruby 源文件的编码为 US-ASCII
1 | #!/usr/bin/env ruby |
1 | rvm use 1.9 ruby default_source_encoding.rb # 输出: US-ASCII |
Ruby 2.0 默认 Ruby 源文件的编码为 UTF-8
1 | rvm use 2.0 ruby default_source_encoding.rb # 输出: UTF-8 |
指定 Ruby 源文件的字符编码
在 Ruby 1.9 中,如果代码文件中包含了非 ASCII 字符,或者在 Ruby 2.0 中代码文件中 包含了非 UTF-8 字符,那么就需要在代码文件中声明该代码文件的字符编码:
1 | #!/usr/bin/env ruby |
1 | rvm use 1.9 ruby non_ascii.rb # 输出: non_ascii.rb:2: invalid multibyte char (US-ASCII) non_ascii.rb:2: invalid multibyte char (US-ASCII) |
Ruby 使用一个看似神奇实则很简单的标记规则来指定代码文件的字符编码:如果一个文件的第一行(如果第一行是 UNIX shebang
#!
,那么就是第二行)是注释行,Ruby 会使用 coding:\s*(\S+)
这个正则表达式来对这个注释行进行匹配,如果匹配成功那么
该文件的字符编码就被设置为 $1
的值。所以,可以这样将一个 Ruby 代码文件的字符编码设置为UTF-8:
1 | # coding: utf-8 |
因为 Ruby 只是检索字符串中是否包含 coding:
这个子字符串,所以实际上也可以这样写:
1 | # encoding: utf-8 |
Emacs 用户可能会更喜欢这样写:
1 | # -*- encoding: utf-8 -*- |
另外,如果 Ruby 代码文件包含了 UTF-8 BOM,也就是说代码文件的头三个字节是 \xEF\xBB\xBF
,那么 Ruby 认为这个代码文件
的字符编码是 UTF-8,而不管上述的标记行:
1 | #!/usr/bin/env ruby |
1 | rvm use 2.0 ruby gbk.rb # 输出: GBK ruby -e 'print [0xEF, 0xBB, 0xBF].pack("c*")' > bom.rb cat gbk.rb >> bom.rb ruby bom.rb # 输出: UTF-8 |
查询代码文件的编码:
特殊常量 __ENCODING__
存储了文件的字符编码
字符串(还有 Symbol 和 Regexp)字面量的字符编码
在 Ruby 1.9+ 中,每一个字符串对象、Symbol 对象和正则表达式对象都有自己的字符编码。
1 | #!/usr/bin/env ruby |
1 | rvm use 2.0 ruby show_encoding.rb # 输出: UTF-8 US-ASCII GBK |
字符串对象、Symbol 对象和正则表达式对象的字面量的编码是这样确定的:
字符串字面量总是以定义它的源代码文件的字符编码来编码的。
1 | #!/usr/bin/env ruby |
1 | #!/usr/bin/env ruby |
1 | rvm use 2.0 |
Symbol
和正则表达式有点特别(我猜测可能出于性能方面的考量):如果它们只包含 ASCII 字符 (即所有字节的最高位都为 0),那么它们就以 US-ASCII 编码;否则它们就以定义它们的 源代码文件的字符编码来编码。
1 | #!/usr/bin/env ruby |
1 | rvm use 2.0 ruby sym_regex_encoding.rb # 输出: US-ASCII UTF-8 US-ASCII UTF-8 |
一个例外:
在字符串和正则表达式中,可以使用 \uxxxx
或 \u{x... x... x...}
来创建任意的UNICODE 字符,如果一个字符串字面量或者
正则表达式字面量中包含了 \uxxxx
或 \u{x... x... x...}
标记,且此标记所表示的字符不是 ASCII 字符,那么它的编码将设
置为 UTF-8,而不管定义它的源代码文件的字符编码是什么。
1 | #!/usr/bin/env ruby |
1 | rvm use 2.0 ruby unicode_notation.rb # 输出: GBK GBK GBK UTF-8 |
虚拟编码 ASCII-8BIT
Ruby 支持一个叫做 ASCII-8BIT
的虚拟字符编码。这个虚拟编码更多地是用来处理二进
制数据,或者在不确定 Ruby 代码文件编码时也可以将其指定为 ASCII-8BIT
。
编码转换
可以将字符串从一个编码转换为另外一个编码
1 | #!/usr/bin/env ruby |
1 | rvm use 2.0 ruby transcoding.rb # 输出: UTF-8 ["e4", "b8", "ad"] GBK ["d6", "d0"] LANG=zh_CN.UTF-8 echo -n 中 | od -An -tx1 # 输出: e4 b8 ad LANG=zh_CN.UTF-8 echo -n 中 | iconv -t GBK | od -An -tx1 # 输出: d6 d0 |
改变一个对象的编码
encode
方法实际上是返回一个新的对象,而要改变一个对象的编码,则使用
force_encoding
方法:
1 | #!/usr/bin/env ruby |
1 | rvm use 2.0 ruby force_encoding.rb # 输出: ASCII-8BIT ["e4", "b8", "ad"] UTF-8 ["e4", "b8", "ad"] GBK ["e4", "b8", "ad"] |
可以看到 force_encoding
只是改变了对象的字符编码,并没有改变存储字符的实际字节。
IO 的字符编码
如果将一个某种特定字符编码的字符串输出到外部 IO 对象时,Ruby 将会使用什么编码输
出这个字符串呢?答案取决于这个 IO 对象的编码是什么。每个 IO 对象都有两个和字
符编码相关的属性:外部编码 external_encoding
和 内部编码 internal_encoding
。
输出过程中的编码转换
与输出数据到一个 IO 对象这个过程相关的是 external_encoding
, 输出过程中的字符
编码转换规则为:若此 IO 对象的 external_encoding
为 nil
,则被输出的对象将不
会被转换字符编码而直接输出其内存中的实际字节;否则,被输出的对象将使用
external_encoding
进行编码,编码过程中所使用的源编码为被输出对象的 encoding
属性。
1 | #!/usr/bin/env ruby |
输入过程中的编码转换
当从一个 IO 对象读取数据时,读取的数据的编码和此 IO 对象的 external_encoding
和 internal_encoding
两个属性都有关
系,具体的规则为:若 internal_encoding
为nil,那么外部数据将被不经任何转换地读进内存,在内存中存储此块数据的对象的
encoding
属性被设置为此 IO 对象的 external_encoding
;否则,外部数据被读进内存时将被转换为 internal_encoding
所
标识的字符编码,且存储此块数据的对象的 encoding
属性被设置为 internal_encoding
,编码转换所使用的源编码为
external_encoding
。
1 | echo -n $'\xe4\xb8\xad' > tmp # 向 tmp 文件中输出“中”字经 UTF-8 编码的字节序列 cat tmp #输出: 中 |
1 | #!/usr/bin/env ruby |
设置 IO 对象的字符编码
在使用 IO.new()
创建一个 IO 对象时,可以指定这个对象的 external_encoding
和 internal_encoding
:
1 | #!/usr/bin/env ruby |
1 | rvm use 2.0 ruby open_file.rb # 输出 #<Encoding:GBK> nil #<Encoding:ISO-8859-1> #<Encoding:Windows-31J> |
若要修改一个 IO 对象的 external_encoding
和 internal_encoding
,使用
IO#set_encoding()
方法:
1 | #!/usr/bin/env ruby |
1 | rvm use 2.0 ruby set_enc.rb # 输出 nil nil nil nil #<Encoding:GBK> nil #<Encoding:Windows-31J> #<Encoding:UTF-8> |
IO 默认编码
Ruby 1.9+ 还有一个 IO 默认外部编码 Encoding.default_external
和 IO 默认内部编
码 Encoding.default_internal
的概念,不过通过我在 ruby-2.0.0-p247 上的实践,发
现这个概念真是一团糟。总的来说, 当你创建一个 IO 对象时,如果没有在 mode 参数里
指定内部编码和外部编码,那么这个 IO 对象的内部编码和外部编码会分别设置为这两个默
认编码,但是这需要满足以下规则:
如果
Encoding.default_internal
为 nil, 那么用户创建的 IO 对象的内部编码和外 部编码,与这两个默认编码没有关系,也就是说在 这种情况下,即便是创建 IO 对象时没 有指定内部编码和外部编码,Ruby 也不会用这两个默 认编码的值去设置这个 IO 对象的 内部和外部编码。例外: 如果 IO 对象是以 readonly (如
File.new filename, "r"= )模式打开的,且没有 指定内部编码和外部编码,那么不管 =default_internal
是否为 nil,那么该对象的外部编 码都将被设置为default_external
的值。- 如果
Encoding.default_internal
和Encoding.default_external
的值相同(顺便 提一下,default_external
的值永远不会是 nil),那么如果创建 IO 对象时没有指 定内部编码和外部编码,那么这个 IO 对象的外部编码将被设置为default_external
的值,而 IO 对象的内部编码不会被设置。 - 如果
default_internal
值不为 nil,且与default_external
不相等,创建 IO 对 象时没有指定内部编码和外部编码,那么这个 IO 对象的外部编码将被设置为default_external
的值,内部编码被设置为default_internal
。
另外,当从一个 IO 对象读取数据时,如果该 IO 对象的 external_encoding
和
internal_encoding
都为 nil,那么外部数据将被不经任何转换地读进内存,在内存中存
储此块数据的对象的 encoding
属性被设置为 Encoding.default_external
;
设置默认编码
- 可以通过
Encoding.default_external=()
和Encoding.default_internal=()
来设 置默认外部编码和默认内部编码。 - 也可以通过 ruby 解释器的
-E
选项来指定默 认外部编码和默认内部编码。 - 另外,Ruby 也会从
LANG
环境变量推断默认的外部编码 - 如果没有设置
LANG
环境变量,也没有指定-E
选项,那么默认的外部编码就被设 置为US-ASCII
标准 IO 对象的默认编码
STDIN
、 STDOUT
和 STDERR
这三个标准 IO 对象的外部编码和内部编码的默认值受
Encoding.default_external
和 Encoding.default_internal
控制,其规则和前文所
述的 default_external
、 default_internal
对新建为指定编码的 IO 对象的控制规则
一致。 一个典型的例子:系统的 LANG 环境变量为 zh_CN.UTF-8
,且没有指定 ruby 解释
器的 -E
选项,那么这 3 个标准 IO 对象的内部编码和外部编码分别为:
1 | #!/usr/bin/env ruby |