如何全局忽略UTF-8字符串中的无效字节序列?

时间:2013-06-07 15:19:49

标签: ruby-on-rails ruby encoding

自Rails版本1以来,我有一个Rails应用程序从迁移中幸存下来,我想忽略其上的所有无效字节序列,以保持向后兼容性。

我无法知道输入编码

例如:

> "- Men\xFC -".split("n")
ArgumentError: invalid byte sequence in UTF-8
    from (irb):4:in `split'
    from (irb):4
    from /home/fotanus/.rvm/rubies/ruby-2.0.0-rc2/bin/irb:16:in `<main>'

我可以通过使用以下内容在一行中克服此问题,例如:

> "- Men\xFC -".unpack("C*").pack("U*").split("n")
 => ["- Me", "ü -"] 

但是,我想始终忽略无效的字节序列并禁用此错误。在Ruby本身或Rails中。

5 个答案:

答案 0 :(得分:16)

我认为你不能在没有太大困难的情况下全局关闭UTF-8检查。我会专注于修复进入应用程序的所有字符串,在它们进入的边界(例如,当您查询数据库或接收HTTP请求时)。

让我们假设进来的字符串有BINARY(a.k.a. ASCII-8BIT编码)。这可以这样模拟:

s = "Men\xFC".force_encoding('BINARY')  # => "Men\xFC"

然后我们可以使用String#encode将它们转换为UTF-8,并用UTF-8替换字符替换任何未定义的字符:

s = s.encode("UTF-8", invalid: :replace, undef: :replace)  # => "Men\uFFFD"
s.valid_encoding?  # => true

不幸的是,上面的步骤最终会破坏很多UTF-8代码点,因为它们中的字节不会被识别。如果您有一个三字节的UTF-8字符,如“\ uFFFD”,它将被解释为三个单独的字节,每个字节将被转换为替换字符。也许你可以这样做:

def to_utf8(str)
  str = str.force_encoding("UTF-8")
  return str if str.valid_encoding?
  str = str.force_encoding("BINARY")
  str.encode("UTF-8", invalid: :replace, undef: :replace)
end

这是我能想到的最好的。不幸的是,我不知道告诉Ruby将字符串视为UTF-8并只替换所有无效字节的好方法。

答案 1 :(得分:6)

在ruby 2.0中你可以使用String#b方法,这是String#force_encoding(“BINARY”)的简短别名

答案 2 :(得分:3)

如果您只想对原始字节进行操作,可以尝试将其编码为ASCII-8BIT / BINARY。

str.force_encoding("BINARY").split("n")

这不会让你的ü回来,因为你的源字符串在这种情况下是ISO-8859-1(或类似的东西):

"- Men\xFC -".force_encoding("ISO-8859-1").encode("UTF-8")
 => "- Menü -"

如果要获取多字节字符, 可以知道源字符集是什么。 一旦force_encoding到BINARY,你就会得到原始字节,因此不会相应地解释多字节字符。

如果数据来自您的数据库,您可以更改连接机制以使用ASCII-8BIT或BINARY编码; Ruby 应该相应地标记它们。或者,您可以对数据库驱动程序进行monkeypatch,以强制对从中读取的所有字符串进行编码。但这是一个巨大的锤子,可能是绝对错误的事情。

正确的答案是修复你的字符串编码。这可能需要数据库修复,数据库驱动程序连接编码修复或其某种组合。所有字节仍然存在,但是如果你正在处理一个给定的字符集,你应该尽可能地让Ruby知道你希望你的数据在那个编码中。一个常见的错误是使用mysql2驱动程序连接到具有latin1编码数据的MySQL数据库,但为连接指定utf-8字符集。这会导致Rails从DB获取latin1数据并将其解释为utf-8,而不是将其解释为latin1,然后您可以将其转换为UTF-8。

如果您可以详细说明字符串的来源,可能会有更完整的答案。您也可以查看this answer以获取默认字符串编码的可能全局(-ish)Rails解决方案。

答案 3 :(得分:2)

如果您可以配置数据库/页面/以及在ASCII-8BIT中为您提供字符串的任何内容,这将为您提供真正的编码。

使用Ruby的stdlib编码猜测库。通过以下内容传递所有字符串:

require 'nkf'
str = "- Men\xFC -"
str.force_encoding(NKF.guess(str))

NKF库将猜测编码(通常是成功的),并强制对字符串进行编码。如果你不想完全信任NKF库,那么围绕字符串操作构建这个安全措施:

begin
  str.split
rescue ArgumentError
  str.force_encoding('BINARY')
  retry
end

如果NKF没有正确猜测,这将在BINARY上回落。您可以将其转换为方法包装器:

def str_op(s)
  begin
    yield s
  rescue ArgumentError
    s.force_encoding('BINARY')
    retry
  end
end

答案 4 :(得分:1)

Ruby 1.9和2.0中的编码似乎有点棘手。 \ xFC是ISO-8859-1中特殊字符ü的代码,但代码FC也以UTF-8出现,用于üU+00FC = \u0252(以及UTF-16)。它可能是Ruby pack/unpack functions的工件。使用Unicode的U *模板字符串打包和解包Unicode字符不成问题:

>> "- Menü -".unpack('U*').pack("U*")
=> "- Menü -"

如果首先解压缩Unicode UTF-8字符(U),然后打包无符号字符(C),则可以创建“错误”字符串,即具有无效编码的字符串:

>> "- Menü -".unpack('U*').pack("C*")
=> "- Men\xFC -"

此字符串不再是有效编码。显然,转换过程可以通过应用相反的顺序(有点像量子物理中的运算符)来反转:

>> "- Menü -".unpack('U*').pack("C*").unpack("C*").pack("U*")
=> "- Menü -"

在这种情况下,也可以通过首先将其转换为ISO-8859-1,然后转换为UTF-8来“修复”损坏的字符串,但我不确定这是否意外地工作,因为代码包含在这个字符集

>> "- Men\xFC -".force_encoding("ISO-8859-1").encode("UTF-8")
=> "- Menü -"
>> "- Men\xFC -".encode("UTF-8", 'ISO-8859-1')
=> "- Menü -"