Ruby Ruby 1.9+ 的字符编码

lululau · January 18, 2014 · Last by boshrc replied at July 25, 2014 · 19082 hits
Topic has been selected as the excellent topic by the admin.

从 1.9 开始,Ruby 增加了对字符编码的支持。这篇文章基本上是看了 Ruby 2.0 镐头书第 17 章 Character Encoding 做的笔记,并补充了一些自己通过实验得到的结论。

Ruby 代码文件的编码

你需要告诉 Ruby 你的 Ruby 代码文件使用的是什么编码,因为 Ruby 中的字符串字面量、Symbol 字面量以及正则表达式字面量的字符编码在多数时候取决于定义他们的源文件的字符编码。

(1). 如果你没有声明代码文件的字符编码,那么 Ruby 源文件的默认编码是这样确定的:

  • Ruby 1.9 默认 Ruby 源文件的编码为 US-ASCII

default_source_encoding.rb:

#!/usr/bin/env ruby
puts __ENCODING__   # 通过 __ENCODING__ 来查询当前文件的字符编码

Shell Commands

rvm use 1.9 
ruby default_source_encoding.rb
# 输出:
US-ASCII
  • Ruby 2.0 默认 Ruby 源文件的编码为 UTF-8

Shell Commands

rvm use 2.0
ruby default_source_encoding.rb
# 输出:
UTF-8

(2). 指定 Ruby 源文件的字符编码 在 Ruby 1.9 中,如果代码文件中包含了非 ASCII 字符,或者在 Ruby 2.0 中代码文件中包含了非 UTF-8 字符,那么就需要在代码文件中声明该代码文件的字符编码:

non_ascii.rb:

#!/usr/bin/env ruby
puts '中文'

Shell Commands:

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:

# coding: utf-8

因为 Ruby 只是检索字符串中是否包含 coding: 这个子字符串,所以实际上也可以这样写:

# encoding: utf-8

Emacs 用户可能会更喜欢这样写:

# -*- encoding: utf-8 -*-

另外,如果 Ruby 代码文件包含了 UTF-8 BOM,也就是说代码文件的头三个字节是 \xEF\xBB\xBF,那么 Ruby 认为这个代码文件的字符编码是 UTF-8,而不管上述的标记行:

gbk.rb:

#!/usr/bin/env ruby
# coding: GBK
puts __ENCODING__

Shell Commands:

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

(3). 查询代码文件的编码:
特殊常量 __ENCODING__ 存储了文件的字符编码

字符串(还有 Symbol 和 Regexp)字面量的字符编码

在 Ruby 1.9+ 中,每一个字符串对象、Symbol 对象和正则表达式对象都有自己的字符编码。

show_encoding.rb:

#!/usr/bin/env ruby
# coding: utf-8
str = "中文"
sym = :name
regex = Regexp.new(str.encode("GBK"))
puts str.encoding
puts sym.encoding
puts regex.encoding

Shell Commands:

rvm use 2.0
ruby show_encoding.rb
# 输出:
UTF-8
US-ASCII
GBK

字符串对象、Symbol 对象和正则表达式对象的字面量的编码是这样确定的:

(1). 字符串字面量总是以定义它的源代码文件的字符编码来编码的。

utf8.rb:

#!/usr/bin/env ruby
# coding: utf-8
puts "abc".encoding
puts "中文".encoding

gbk.rb:

#!/usr/bin/env ruby
# coding: GBK
puts "abc".encoding
puts "中文".encoding

Shell Commands:

rvm use 2.0
ruby utf8.rb
ruby gbk.rb
# 输出:
UTF-8
UTF-8
GBK
GBK

(2). Symbol 和正则表达式有点特别(我猜测可能出于性能方面的考量):如果它们只包含 ASCII 字符(即所有字节的最高位都为 0),那么它们就以 US-ASCII 编码;否则它们就以定义它们的源代码文件的字符编码来编码。

sym_regex_encoding.rb:

#!/usr/bin/env ruby
# coding: UTF-8
a = :name
b = :名字
x = /hello/
y = /你好/
puts a.encoding
puts b.encoding
puts x.encoding
puts y.encoding

Shell Commands:

rvm use 2.0
ruby sym_regex_encoding.rb
# 输出:
US-ASCII
UTF-8
US-ASCII
UTF-8

(3). 一个例外:

在字符串和正则表达式中,可以使用 \uxxxx\u{x... x... x...} 来创建任意的 UNICODE 字符,如果一个字符串字面量或者正则表达式字面量中包含了 \uxxxx\u{x... x... x...} 标记,且此标记所表示的字符不是 ASCII 字符,那么它的编码将设置为 UTF-8,而不管定义它的源代码文件的字符编码是什么。

unicode_notation.rb:

#!/usr/bin/env ruby
# coding: GBK
a = "a"
b = "中"
x = "\u0061"
y = "\u2d4e"
puts a.encoding
puts b.encoding
puts x.encoding
puts y.encoding

Shell Commands:

rvm use 2.0
ruby unicode_notation.rb
# 输出:
GBK
GBK
GBK
UTF-8

虚拟编码 ASCII-8BIT

Ruby 支持一个叫做 ASCII-8BIT 的虚拟字符编码。这个虚拟编码更多地是用来处理二进制数据,或者在不确定 Ruby 代码文件编码时也可以将其指定为 ASCII-8BIT

编码转换

(1). 可以将字符串从一个编码转换为另外一个编码

transcoding.rb:

#!/usr/bin/env ruby
# coding: UTF-8
a = "中"
puts a.encoding
p a.bytes.map { |e| e.to_s(16) }
b = a.encode("GBK")
puts b.encoding
p b.bytes.map { |e| e.to_s(16) }

Shell Commands:

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

(2). 改变一个对象的编码 encode 方法实际上是返回一个新的对象,而要改变一个对象的编码,则使用 force_encoding 方法:

force_encoding.rb:

#!/usr/bin/env ruby
# coding: ASCII-8BIT
a = "中"
puts a.encoding
p a.bytes.map { |e| e.to_s(16) }
a.force_encoding("UTF-8")
puts a.encoding
p a.bytes.map { |e| e.to_s(16) }
a.force_encoding("GBK")
puts a.encoding
p a.bytes.map { |e| e.to_s(16) }

Shell Commands:

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

(1). 输出过程中的编码转换
与输出数据到一个 IO 对象这个过程相关的是 external_encoding,输出过程中的字符编码转换规则为:若此 IO 对象的 external_encodingnil ,则被输出的对象将不会被转换字符编码而直接输出其内存中的实际字节;否则,被输出的对象将使用 external_encoding 进行编码,编码过程中所使用的源编码为被输出对象的 encoding 属性。

output_transcoding.rb:

#!/usr/bin/env ruby
# coding: UTF-8
s_utf8 = "中"
p s_utf8.bytes.map { |e| e.to_s(16) }   # => ["e4", "b8", "ad"]
s_gbk = s_utf8.encode("GBK")
p s_gbk.bytes.map { |e| e.to_s(16) }    # => ["d6", "d0"]
p s_gbk.encoding    # =>  #<Encoding:GBK>
p STDOUT.external_encoding    # => nil
p STDOUT.internal_encoding    # => nil
puts s_gbk      # => 0xd6  0xd0 , 说明 s_gbk 的字节没有经过编码转换而直接输出
STDOUT.set_encoding("UTF-8:Windows-31J")
p STDOUT.external_encoding     # => #<Encoding:UTF-8>
p STDOUT.internal_encoding     # => #<Encoding:Windows-31J>
puts s_gbk      # => 0xe4  0xb8  0xad ,被正常转换为 UTF-8,说明数据输出过程中的编码转换和 internal_encoding <Windows-31J> 无关

(2). 输入过程中的编码转换
当从一个 IO 对象读取数据时,读取的数据的编码和此 IO 对象的 external_encodinginternal_encoding 两个属性都有关系,具体的规则为:若 internal_encoding 为 nil,那么外部数据将被不经任何转换地读进内存,在内存中存储此块数据的对象的 encoding 属性被设置为此 IO 对象的 external_encoding;否则,外部数据被读进内存时将被转换为 internal_encoding 所标识的字符编码,且存储此块数据的对象的 encoding 属性被设置为 internal_encoding,编码转换所使用的源编码为 external_encoding

Shell Commands:

echo -n $'\xe4\xb8\xad' > tmp  # 向 tmp 文件中输出“中”字经 UTF-8 编码的字节序列
cat tmp
#输出:

input_transcoding.rb:

#!/usr/bin/env ruby
File.open "tmp", "r:UTF-8" do |f|
    p f.external_encoding   # => #<Encoding:UTF-8>
    p f.internal_encoding   # => nil
    l = f.gets
    p l.encoding   # => #<Encoding:UTF-8>
    p l.bytes.map { |e| e.to_s(16) }  # => ["e4", "b8", "ad"]
end
File.open "tmp", "r:UTF-8:GBK" do |f|
    p f.external_encoding  # => #<Encoding:UTF-8>
    p f.internal_encoding  # => #<Encoding:GBK>
    l = f.gets
    p l.encoding  # => #<Encoding:GBK>
    p l.bytes.map { |e| e.to_s(16) }  # => ["d6", "d0"]
end

(3). 设置 IO 对象的字符编码
在使用 IO.new()创建一个 IO 对象时,可以指定这个对象的 external_encodinginternal_encoding

open_file.rb:

#!/usr/bin/env ruby
hello = File.new "hello", "w:gbk"
world = File.new "world", "w:ISO8859-1:sjis"
p hello.external_encoding
p hello.internal_encoding
p world.external_encoding
p world.internal_encoding

Shell Commands:

rvm use 2.0
ruby open_file.rb
# 输出
#<Encoding:GBK>
nil
#<Encoding:ISO-8859-1>
#<Encoding:Windows-31J>

若要修改一个 IO 对象的 external_encodinginternal_encoding,使用 IO#set_encoding() 方法:

set_enc.rb:

#!/usr/bin/env ruby
p STDOUT.external_encoding
p STDOUT.internal_encoding
p STDERR.external_encoding
p STDERR.internal_encoding
STDOUT.set_encoding('gbk')
STDERR.set_encoding('sjis:utf-8')
p STDOUT.external_encoding
p STDOUT.internal_encoding
p STDERR.external_encoding
p STDERR.internal_encoding

Shell Commands:

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 对象的内部编码和外部编码会分别设置为这两个默认编码,但是这需要满足以下规则:

  1. 如果 Encoding.default_internal 为 nil,那么用户创建的 IO 对象的内部编码和外部编码,与这两个默认编码没有关系,也就是说在这种情况下,即便是创建 IO 对象时没有指定内部编码和外部编码,Ruby 也不会用这两个默认编码的值去设置这个 IO 对象的内部和外部编码。
    例外:如果 IO 对象是以 readonly (如File.new filename, "r") 模式打开的,且没有指定内部编码和外部编码,那么不管default_internal是否为 nil,那么该对象的外部编码都将被设置为 default_external 的值。
  2. 如果 Encoding.default_internalEncoding.default_external 的值相同(顺便提一下,default_external 的值永远不会是 nil),那么如果创建 IO 对象时没有指定内部编码和外部编码,那么这个 IO 对象的外部编码将被设置为 default_external 的值,而 IO 对象的内部编码不会被设置。
  3. 如果 default_internal 值不为 nil,且与 default_external 不相等,创建 IO 对象时没有指定内部编码和外部编码,那么这个 IO 对象的外部编码将被设置为 default_external 的值,内部编码被设置为 default_internal

另外,当从一个 IO 对象读取数据时,如果该 IO 对象的 external_encodinginternal_encoding 都为 nil,那么外部数据将被不经任何转换地读进内存,在内存中存储此块数据的对象的 encoding 属性被设置为Encoding.default_external

设置默认编码

  1. 可以通过 Encoding.default_external=()Encoding.default_internal=() 来设置默认外部编码和默认内部编码。
  2. 也可以通过 ruby 解释器的 -E 选项来指定默认外部编码和默认内部编码。
  3. 另外,Ruby 也会从 LANG 环境变量推断默认的外部编码
  4. 如果没有设置 LANG 环境变量,也没有指定 -E 选项,那么默认的外部编码就被设置为 US-ASCII

标准 IO 对象的默认编码

STDINSTDOUTSTDERR这三个标准 IO 对象的外部编码和内部编码的默认值受 Encoding.default_externalEncoding.default_internal 控制,其规则和前文所述的 default_externaldefault_internal 对新建为指定编码的 IO 对象的控制规则一致。

一个典型的例子:系统的 LANG 环境变量为 zh_CN.UTF-8,且没有指定 ruby 解释器的 -E 选项,那么这 3 个标准 IO 对象的内部编码和外部编码分别为:

stdio_encoding.rb:

#!/usr/bin/env ruby
puts Encoding.default_external  # => UTF-8
puts Encoding.default_internal  # => nil
puts STDIN.external_encoding  # => UTF-8
puts STDIN.internal_encoding  # => nil
puts STDOUT.external_encoding  # => nil
puts STDOUT.internal_encoding  # => nil
puts STDERR.external_encoding  # => nil
puts STDERR.internal_encoding  # => nil

ruby 2.0 以上版本,改哪个变量能起到像改了文件头部的 #encoding: 注释行一样的效果?

意思就是在 windows 简体中文版像在文件头加了 #encoding: GBK 一样的效果,在其它语言的 windows 也像加了对应语言注释行那种效果?

还没看,不过,跟编码有关的知识,竟然会写这么长?

好久在社区没有看到 如此详细的总结 一个 纯技术专题 的帖子了。

@lgn21st, @huacnlee, @Rei,

管理员同志们,如此好的原创帖,竟然不加精,天理不容呀!!

看到那么多帖子,只不过随便贴个链接,三五个字,就标记为精华,而这样的帖子,两天了,在我回复时,已经有 8 个喜欢了,竟然不加精,是不是你们有点 太凭个人喜好 来标记精华了呢?

我不得不诧异,你们精华的依据到底是什么?

#5 楼 @zw963 因为世界上有太多事比加精一个帖子重要且有趣。

puts 应该改成 p 吧,p 更直观

#6 楼 @Rei

好吧,我理解为这样的帖子,你根本不感兴趣,对吧?

那么加精也应该分版块由感兴趣的人来加精吧?

这样不问不理,会影响楼主的积极性,不觉得这样的帖子越来越少了吗?

#8 楼 @zw963 不加精就会影响积极性吗?如果是我发贴,有内容的交流比管理员认证更让我高兴。

#9 楼 @Rei

那干嘛要加精这个功能呢?取消好了。 这就好像,明明分了等级,却非要说,等级无所谓,有人欣赏你才是最重要的。

#10 楼 @zw963 认真就无趣了,管理员已经说了,他之前无暇处理这个帖子而已。

加精由管理员说了算,本来就是一种主观判定,没有什么必须加或必须不加的依据。除非像高校这样规定判定条件:必须发 5 篇 SCI 才能评正教授。然后大家都向这个判定条件靠拢,而设定这个判定条件的最初目的,就没有人管了。

论坛交流也类似,不管是靠人治或是靠机制,最终都达不到理想中的最佳效果,人来人往,人聚人散,谈得来就多聊几句,谈不来就散,当这里就是一个茶馆好了。

ps: 整理一个技术问题或专题,最主要的是让自己清晰,之余再分享给他人,加不加精是不大要紧。

#11 楼 @xhj6

ps: 整理一个技术问题或专题,最主要的是让自己清晰,之余再分享给他人,加不加精是不大要紧

这句话我很认可呀。

楼主就算不在这里发帖,我相信他自己也一定会做很多总结, 加不加精,肯定不会影响楼主归纳总结的积极性了。 但是这里的 之余再分享给他人 的积极性,就是我说的问题了。

刚好补补编码有关的知识,谢谢分享。

14 Floor has deleted
15 Floor has deleted
16 Floor has deleted
17 Floor has deleted

学习了。 我觉得文本编码一直都是蛋疼的问题,应该算历史遗留问题了吧。貌似文本文件从来就没有统一的编码规范,难得出了个 UTF-8 BOM,还要被 Unix 社区鄙视。若不是,Ruby 是日本人搞的,估计也不会考虑这些文本编码的问题吧。

欢迎拍砖,哈哈

收藏了,多谢楼主的分享

好文,学习了。

You need to Sign in before reply, if you don't have an account, please Sign up first.