Ruby 字符编码怎么判断与转换?Encoding 类有哪些核心方法?
【免费下载链接】rubyThe Ruby Programming Language项目地址: https://gitcode.com/GitHub_Trending/ru/ruby
手头有一段来源不明的字节序列,不确定它该按哪种编码解释,或者需要把文件从一种编码读到另一种编码——这类问题在 Ruby 里统一由Encoding类和String上的编码方法解决。本文基于 Ruby 仓库的语言文档 encodings.rdoc 和String方法文档,给出"判断当前编码 → 确认能否拼接 → 重新标注编码 → 真正转码 → 读写流时自动转换"这条完整路径,所有示例代码均可直接在ruby交互环境或脚本中运行验证。
第一步:判断字符串当前的编码与有效性
每个String对象都带有一个Encoding实例,用String#encoding读取。注意:拿到编码名只说明字符串"自称"是什么编码,还要用String#valid_encoding?确认字节序列在该编码下确实合法(参见 valid_encoding_p.rdoc):
s = 'Straße' s.valid_encoding? # => true s.encoding # => #<Encoding:UTF-8> s.force_encoding(Encoding::ASCII).valid_encoding? # => false文档给出的另一个常用查询是String#ascii_only?,配合valid_encoding?可以判断一段文本是否只是纯 ASCII:
s = "abc".force_encoding(Encoding::UTF_8) # => "abc" s.ascii_only? # => true s = "abc\u{6666}".force_encoding(Encoding::UTF_8) # => "abc晦" s.ascii_only? # => false s = "\xc2\xa1".force_encoding(Encoding::UTF_8) # => "¡" s.valid_encoding? # => true s = "\xc2".force_encoding(Encoding::UTF_8) # => "\xC2" s.valid_encoding? # => false需要区分两种"编码不对":一种是编码标签错误(字节合法但被标成了别的编码),用force_encoding改标签即可;另一种是字节本身在该编码下非法(valid_encoding?返回false),必须走转码或清洗路径,后文会讲。
Symbol和Regexp内部存的字符串也有编码,同样可用Symbol#encoding/Regexp#encoding读取;两者的默认编码规则是:全部字符为 US-ASCII 时是 US-ASCII,否则是脚本编码。
第二步:枚举、查找和别名解析
Encoding类的常量就是编码对象,每个常量只有一个实例。核心查询方法都在 encodings.rdoc 的 "Names and Aliases" 一节:
Encoding.list.size # => 103 Encoding.list.first.class # => Encoding Encoding.list.take(3) # => [#<Encoding:ASCII-8BIT>, #<Encoding:UTF-8>, #<Encoding:US-ASCII>]上面三行输出是文档示例,Encoding.list.size的具体数值随环境注册的编码数量变化,不必作为固定校验值。
按名称(或别名)找编码对象用Encoding.find;name/names/aliases/name_list用于反查一个编码的名字和全部别名:
Encoding::ASCII_8BIT.name # => "ASCII-8BIT" Encoding::ASCII_8BIT.names # => ["ASCII-8BIT", "BINARY"] Encoding::WINDOWS_31J.names # => ["Windows-31J", "CP932", "csWindows31J", "SJIS", "PCK"] Encoding.find("US-ASCII") # => #<Encoding:US-ASCII>Encoding.name_list返回的名字总数多于Encoding.list,因为它把别名也算进去了(文档示例:Encoding.name_list.size # => 175,同样是示例数值)。
第三步:确认两个字符串能否直接拼接
拼接前用Encoding.compatible?检查两个对象是否编码兼容;它返回拼接结果将采用的Encoding,不兼容时返回nil:
rus = "\u{442 435 441 442}" eng = 'text' Encoding.compatible?(rus, eng) # => #<Encoding:UTF-8> s0 = "\xa1\xa1".force_encoding(Encoding::ISO_8859_1) # => "\xA1\xA1" s1 = "\xa1\xa1".force_encoding(Encoding::EUCJP) # => "\x{A1A1}" Encoding.compatible?(s0, s1) # => nil返回nil意味着这两个字符串不能直接+拼接,需要先统一编码,也就是进入下一步。
第四步:force_encoding 与 encode 的区别——只改标签,还是真正转码
这是最容易用错的一对方法。
String#force_encoding(参见 force_encoding.rdoc)不改变底层字节,只把编码标签换掉,因此即使新标签与内容不匹配也照改不误:
s = 'łał' s.bytes # => [197, 130, 97, 197, 130] s.encoding # => #<Encoding:UTF-8> s.force_encoding('ascii') # => "\xC5\x82a\xC5\x82" s.encoding # => #<Encoding:US-ASCII> s.valid_encoding? # => false s.bytes # => [197, 130, 97, 197, 130]也就是说,force_encoding适合"这段字节本来就是 ISO-8859-1,只是被错标了"的场景;如果字节在新编码下不合法,valid_encoding?会立刻给出false,这是一个可以直接执行的检查点。
String#encode才是真转码(参见 encode.rdoc):它按给定编码重新解释字节并输出新字符串,字节内容通常会变。三种调用形式:
# 只给目标编码 s = "Ruby\x99".force_encoding('Windows-1252') t = s.encode('UTF-8') # => "Ruby™" t.encoding # => #<Encoding:UTF-8> # 同时给目标和源编码:按 src_encoding 解释 self,再编码为 dst_encoding s = "Ruby\x99" t = s.encode('UTF-8', 'Windows-1252') # => "Ruby™" # 不带参数:Encoding.default_internal 为 nil 时保持原编码 Encoding.default_internal # => nil s = "Ruby\x99".force_encoding('Windows-1252') t = s.encode # 编码与字节都不变两条需要注意的行为都写进了文档:
- 默认情况下,
self含非法字节、或含目标编码未定义的字符时,encode会抛异常; - 同一编码到同一编码的转换是 no-op——即使字符串里有非法字节,只要没给
invalid: :replace,s.encode('UTF-8')(对 UTF-8 字符串)只是原样拷贝,不会抛错。想靠encode做"同编码校验"是行不通的。
第五步:转码失败时,用编码选项决定替换策略
encodings.rdoc 的 "Encoding Options" 一节列出了encode等核心方法接受的关键词参数,默认行为都是"抛异常",可以通过选项改为替换:
invalid: :replace——非法字节序列替换为替换字符串;undef: :replace——目标编码未定义的字符替换为替换字符串;:replace(默认nil)——指定替换串;不指定时,Unicode 编码默认用"\uFFFD"("�"),其他编码默认用'?';:fallback——可用 hash、方法或 proc 按"每个不可转换的字符"定制替换结果;:xml: :text/:xml: :attr——按 XML 文本或属性值的规则转义未定义字符。
文档示例:
s = "\x80foo\x80" s.encode(Encoding::ISO_8859_3) # Raises Encoding::InvalidByteSequenceError. s.encode(Encoding::ISO_8859_3, invalid: :replace) # => "?foo?" s = "\xA5foo\xA5" options = {:undef => :replace, :replace => 'xyzzy'} s.encode(Encoding::UTF_8, Encoding::ISO_8859_3, **options) # => "xyzzyfooxyzzy"fallback的 proc 形式(文档示例):
s = "\u3042foo\u3043" proc = Proc.new {|x| x == "\u3042" ? 'xyzzy' : 'XYZZY' } s.encode('ASCII', fallback: proc) # => "XYZZYfooXYZZY"如果只是想"把非法字节洗掉再使用"而不做编码转换,用String#scrub(参见 scrub.rdoc):
"foo\x81\x81bar".scrub # => "foo��bar" "foo\x81\x81bar".force_encoding('US-ASCII').scrub # => "foo??bar" "foo\x81\x81bar".scrub('xyzzy') # => "fooxyzzyxyzzybar" "foo\x81\x81bar".scrub {|sequence| p sequence; 'XYZZY' } # => "fooXYZZYXYZZYbar" # 块中每次收到的无效序列是 "\x81"(文档示例输出)同族方法共六个,均会生成新字符串或原地修改:String#encode、String#encode!(原地)、String#scrub、String#scrub!(原地)、String#unicode_normalize、String#unicode_normalize!(原地)。其中unicode_normalize接受:nfc/:nfd/:nfkc/:nfkd四种形式(参见 unicode_normalize.rdoc),且文档明确列出了它支持的编码范围:Encoding::UTF_8、Encoding::UTF_16BE/LE、Encoding::UTF_32BE/LE、Encoding::GB18030、Encoding::UCS_2BE、Encoding::UCS_4BE。
"a\u0300".unicode_normalize # => "à" "a\u0300".unicode_normalize(:nfd) # => "à"第六步:读写文件时自动转码(external/internal 双编码)
IO、File、ARGF、StringIO这类流对象同时带两个编码:
- external encoding:说明流里的字节按什么编码解释。文本流默认 UTF-8,二进制流默认 ASCII-8BIT;
- internal encoding:若非
nil,读入的字符串统一转换为该编码。默认是nil(不转换)。
可以在创建流时用 open 选项external_encoding/internal_encoding(或简写encoding)指定,也可以用set_encoding方法(支持 BOM 的还有set_encoding_by_bom)事后设置。全局默认值可通过命令行-E(--external_encoding/--internal_encoding)或Encoding.default_external=/Encoding.default_internal=设置——文档特别提醒:运行时用方法改全局默认值可能出问题,因为改动前后创建的字符串编码可能不同,优先用命令行选项。
文档给出的完整示例:把字符串按 ISO-8859-1 写入文件,再分别按"原始字节"和"转码后"两种方式读回。注意这段代码会向当前工作目录写入临时文件t.tmp,运行前请确认目录可写:
s = "R\u00E9sum\u00E9" path = 't.tmp' ext_enc = Encoding::ISO_8859_1 int_enc = Encoding::UTF_8 File.write(path, s, external_encoding: ext_enc) raw_text = File.binread(path) transcoded_text = File.read(path, external_encoding: ext_enc, internal_encoding: int_enc) p raw_text p transcoded_text文档示例输出:
"R\xE9sum\xE9" "Résumé"第一行是binread得到的原始字节(ISO-8859-1),第二行是按 internal encoding 转码后的 UTF-8 字符串——这就是"同一份文件内容,两种编码视图"的直接验证方式。
第七步:确认脚本自身的编码
Ruby 脚本有自己的脚本编码,用__ENCODING__读取,默认 UTF-8。可在源文件第一行(有 shebang 时第二行)用 magic comment 指定,注释必须包含coding或encoding加冒号、空格和编码名或别名:
# encoding: ISO-8859-1 __ENCODING__ #=> #<Encoding:ISO-8859-1>字符串字面量默认就是脚本编码,而String.new的规则不同:无参数时是 ASCII-8BIT;给字符串参数时继承其编码;用encoding:关键词可显式指定(参见 new.rdoc)。注意String.new('foo', encoding: 'bar')这种不存在的编码名会抛ArgumentError,但"编码合法而内容不合法"的组合不会报错,valid_encoding?才会暴露它。
边界与易错点小结
force_encoding只改标签不改字节;字节真的变了才用encode。用valid_encoding?可以立刻判断改标签后内容是否仍合法。encode同编码到同编码是 no-op,不校验非法字节;需要校验/清洗时用valid_encoding?或scrub。unicode_normalize只支持文档列出的 UTF-16/32 各端序、UTF-8、GB18030、UCS-2BE/4BE,其他编码不适用。- 运行时修改
Encoding.default_external=/Encoding.default_internal=会让改动前后创建的字符串编码不一致,文档建议用-E命令行选项设置全局默认。 Encoding.compatible?返回nil时两个字符串不可直接拼接,先统一编码再操作。
完成上面步骤后,你可以按"encoding查标签 →valid_encoding?查合法性 →compatible?查可拼接性 →encode/scrub处理字节 → 流上用 external/internal 双编码"这个顺序处理绝大多数字符串编码问题;更多选项细节(如:cr_newline/:crlf_newline/:universal_newline换行转换)见 encodings.rdoc。
【免费下载链接】rubyThe Ruby Programming Language项目地址: https://gitcode.com/GitHub_Trending/ru/ruby
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考