【问题标题】:ruby 1.9, force_encoding, but checkruby 1.9,force_encoding,但检查
【发布时间】:2012-04-29 08:13:02
【问题描述】:

我有一个从某种输入中读取的字符串。

据我所知,它是 UTF8。好的:

string.force_encoding("utf8")

但是如果这个字符串中有实际上不是合法 UTF8 的字节,我现在想知道并采取行动。

通常情况下, force_encoding("utf8") 遇到这样的字节会引发吗?我相信不会。

如果我正在执行#encode,我可以从方便的选项中选择如何处理源编码(或目标编码)中无效的字符。

但我不是在做#encode,而是在做#force_encoding。它没有这样的选择。

这样做有意义吗

string.force_encoding("utf8").encode("utf8")

立即获得例外?通常编码 from utf8 to utf8 没有任何意义。但如果有无效字节,也许这是让它立即提升的方法?或者使用:replace 选项等对无效字节做一些不同的事情?

但不,似乎也无法做到这一点。

有人知道吗?

1.9.3-p0 :032 > a = "bad: \xc3\x28 okay".force_encoding("utf-8")
=> "bad: \xC3( okay"
1.9.3-p0 :033 > a.valid_encoding?
=> false

好的,但是如何找到并消除这些坏字节?奇怪的是,这不会引发:

1.9.3-p0 :035 > a.encode("utf-8")
 => "bad: \xC3( okay"

如果我要转换成不同的编码,它会的!

1.9.3-p0 :039 > a.encode("ISO-8859-1")
Encoding::InvalidByteSequenceError: "\xC3" followed by "(" on UTF-8

或者如果我告诉它,它会用“?”替换它=>

1.9.3-p0 :040 > a.encode("ISO-8859-1", :invalid => :replace)
=> "bad: ?( okay"

所以 ruby​​ 很聪明地知道什么是 utf-8 中的坏字节,并在转换为不同的编码时用其他东西替换 em。但我不想想要转换为不同的编码,我想保留 utf8 - 但如果那里有无效字节,我可能想提出,或者我可能想用无效字节替换替换字符。

难道没有办法让 ruby​​ 做到这一点吗?

更新 我相信这最终被添加到 ruby​​ 2.1 中,2.1 预览版中存在 String#scrub 来执行此操作。所以找那个!

【问题讨论】:

    标签: ruby character-encoding


    【解决方案1】:

    关于我唯一能想到的就是在往返过程中转码到不会损坏字符串的东西并返回:

    string.force_encoding("UTF-8").encode("UTF-32LE").encode("UTF-8")
    

    虽然看起来相当浪费。

    【讨论】:

    • 呃。除了浪费之外,它还要求您确保知道哪些编码可以往返而不会丢失任何内容。我想要一个适用于任意输入编码的通用解决方案——ruby 知道如何在实际转码时使用任何编码来做到这一点,为什么它不能为我做呢?烦人。
    • 你总是可以在任何 UTF 之间往返; Unicode 就是 Unicode,不管你如何表示它。只有当你离开 Unicode 时,你可能会在翻译中丢失一些东西。
    • 嗯?您已经假设输入是 UTF-8,这意味着您已经假设 Unicode。我不明白这种反对意见。
    • 我正在编写库代码,我需要能够为任意字符编码“检查和修复编码有效性”。在这种情况下,它是 UTF-8,但它需要是一个库函数,可以处理任意 ruby​​ 1.9 编码作为输入参数。
    【解决方案2】:

    好的,这是我自己想出来的一种非常蹩脚的纯红宝石方法。它可能执行废话。什么鬼,红宝石?现在不选择我自己的答案,希望其他人会出现并给我们更好的东西。

     # Pass in a string, will raise an Encoding::InvalidByteSequenceError
     # if it contains an invalid byte for it's encoding; otherwise
     # returns an equivalent string.
     #
     # OR, like String#encode, pass in option `:invalid => :replace`
     # to replace invalid bytes with a replacement string in the
     # returned string.  Pass in the
     # char you'd like with option `:replace`, or will, like String#encode
     # use the unicode replacement char if it thinks it's a unicode encoding,
     # else ascii '?'.
     #
     # in any case, method will raise, or return a new string
     # that is #valid_encoding?
     def validate_encoding(str, options = {})
       str.chars.collect do |c|
         if c.valid_encoding?
           c
         else
           unless options[:invalid] == :replace
             # it ought to be filled out with all the metadata
             # this exception usually has, but what a pain!
             raise  Encoding::InvalidByteSequenceError.new
           else
             options[:replace] || (
              # surely there's a better way to tell if
              # an encoding is a 'Unicode encoding form'
              # than this? What's wrong with you ruby 1.9?
              str.encoding.name.start_with?('UTF') ?
                 "\uFFFD" :
                 "?" )
           end
         end 
       end.join
     end
    

    http://bibwild.wordpress.com/2012/04/17/checkingfixing-bad-bytes-in-ruby-1-9-char-encoding/ 的更多咆哮

    【讨论】:

      【解决方案3】:

      确保您的脚本文件本身保存为 UTF8 并尝试以下操作

      # encoding: UTF-8
      p [a = "bad: \xc3\x28 okay", a.valid_encoding?]
      p [a.force_encoding("utf-8"), a.valid_encoding?]
      p [a.encode!("ISO-8859-1", :invalid => :replace), a.valid_encoding?]
      

      这在我的 windows7 系统上提供了以下内容

      ["bad: \xC3( okay", false]
      ["bad: \xC3( okay", false]
      ["bad: ?( okay", true]
      

      所以你的坏字符被替换了,你可以马上做如下

      a = "bad: \xc3\x28 okay".encode!("ISO-8859-1", :invalid => :replace)
      => "bad: ?( okay"
      

      编辑:这里有一个适用于任意编码的解决方案,第一个只编码坏字符,第二个只是替换为 ?

      def validate_encoding(str)
        str.chars.collect do |c| 
          (c.valid_encoding?) ? c:c.encode!(Encoding.locale_charmap, :invalid => :replace)
        end.join 
      end
      
      def validate_encoding2(str)
        str.chars.collect do |c| 
          (c.valid_encoding?) ? c:'?'
        end.join 
      end
      
      a = "bad: \xc3\x28 okay"
      
      puts validate_encoding(a)                  #=>bad: ?( okay
      puts validate_encoding(a).valid_encoding?  #=>true
      
      
      puts validate_encoding2(a)                  #=>bad: ?( okay
      puts validate_encoding2(a).valid_encoding?  #=>true
      

      【讨论】:

      • 我不想将编码更改为 ISO-8859-1。我想将其保留为原始编码。现在您会说“好的,然后转码为 8859 1,然后再返回。”我想要一个适用于任意编码的解决方案;对于任何任意编码,您都不能必然地转码为 8859 并返回而不会丢失。
      • 谢谢。我已经独立地得出了类似的结论,但你能解释一下这是做什么的吗:c.encode!(Encoding.locale_charmap, :invalid => :replace)?是转码吗?我不想对字符串进行转码(更改编码),无论它以什么编码开始以及我的默认语言环境编码是什么。但我想我已经到了你最终也会考虑到这一点的地方,请参阅我对这个问题的自我回答。
      • 我不会更改编码,因为它与输入字符串的编码相同,无论如何,你有你的解决方案,这才是最重要的
      【解决方案4】:

      如果您这样做是为了“现实生活”用例 - 例如解析用户输入的不同字符串,而不仅仅是为了能够“解码”一个完全随机的文件,该文件可以由尽可能多的编码,那么我想你至少可以假设每个字符串的所有字符都具有相同的编码。

      那么,在这种情况下,你会怎么看呢?

      strings = [ "UTF-8 string with some utf8 chars \xC3\xB2 \xC3\x93", 
                   "ISO-8859-1 string with some iso-8859-1 chars \xE0 \xE8", "..." ]
      
      strings.each { |s| 
          s.force_encoding "utf-8"
          if s.valid_encoding?
              next
          else
              while s.valid_encoding? == false 
                          s.force_encoding "ISO-8859-1"
                          s.force_encoding "..."
                      end
              s.encode!("utf-8")
          end
      }
      

      无论如何,我都不是 Ruby “专业人士”,所以如果我的解决方案有误甚至有点幼稚,请原谅..

      我只是尽量回馈我能做的,这就是我的目标,而我正在(我仍然)在这个用于任意编码字符串的小解析器上工作,我正在为一个研究项目做这个.

      当我发布这篇文章时,我必须承认我什至没有完全测试过它。我.. 刚刚得到了几个“阳性”结果,但我很兴奋可能找到了我正在努力的东西找到(并且我花了所有时间在 SO.. 上阅读此内容)我只是觉得有必要尽快分享它,希望它可以帮助任何一直在寻找它的人节省一些时间只要我一直...... ..如果它按预期工作:)

      【讨论】:

      • 这就是我最终做的事情:github.com/jrochkind/ensure_valid_encoding/blob/master/lib/… 关键是我知道字符串 supposed 要编码为什么,但它可能包含坏字节.您的解决方案更倾向于猜测“真正”的编码是什么,这是一个不同的问题。
      • 回顾一下:1)您的编码字符错误或数据损坏,(根据您 github 上的基本原理,您认为这两件事都可能是问题的原因),2)您似乎不在乎错误编码,因为您只想保留有效的 utf-8 字符(您不要检查不良数据是否使用不同的编码有效) - 人们建议转换为另一种编码作为检查无效字节的一种手段,但是您害怕丢失一些数据。如果您没有首先验证假定编码的有效性,那有什么意义呢?(所以可能会丢失数据?)
      • 感谢您的回答,试图说服我做我需要做的事情是愚蠢的,但显然许多其他人不同意,因为 ruby​​ 在 ruby​​ 2.1 中使用 String#scrub 将它添加到标准库中!事实上,我明白我在做什么,并且在很多情况下这样做是有意义的(您是否尝试过查看 vim 或您喜欢的其他编辑器在这种情况下做了什么?),但重点这张票不是为了让你相信这个事实。
      • #scrub 适用于您确定字符串编码的情况,否则您将丢失数据。您在上面的第一条评论中说您“知道字符串是 SUPPOSED 要编码为什么”。用英语来说,这并不意味着您对编码 100% 确定,但只是应该如此。所以不要抱怨。如果您不是那个意思,那么您可以从一开始就简单地说您对编码很确定,并且您只想去除坏字节。顺便说一句,在#scrub 之前,您必须手动通过不同的编码来做到这一点。这是我的建议。
      • 如果其中有坏字节,那么您对 ​​100% 确定编码显然是错误的!根据定义,它不是那种编码。在 scrub 进入标准库之前,您必须弄清楚如何在纯 ruby​​ 中自己实现擦洗,这就是我最终要做的——而不是通过一个一个地进行编码,这不是我想要的——磨砂膏的作用是我一直在寻找的。总的来说,你在 stackoverflow 上是否成功地告诉人们他们的要求很愚蠢,他们很愚蠢,他们不应该问他们所问的问题?
      【解决方案5】:

      (更新:见https://github.com/jrochkind/scrub_rb

      所以我在这里编写了一个我需要的解决方案:https://github.com/jrochkind/ensure_valid_encoding/blob/master/lib/ensure_valid_encoding.rb

      但直到最近我才意识到这实际上是内置在 stdlib 中的,您只需要有点违反直觉地将“二进制”作为“源编码”传递:

      a = "bad: \xc3\x28 okay".force_encoding("utf-8")
      a.encode("utf-8", "binary", :undef => :replace)
      => "bad: �( okay"
      

      是的,这正是我想要的。事实证明,这是内置于 1.9 stdlib 中的,它只是没有记录,很少有人知道(或者可能很少有人会说英语?)。虽然我在某处的博客上看到这些论点以这种方式使用,但其他人知道!

      【讨论】:

      • 使用 Ruby 1.9.3-p484,这会错误地将 iso-8859-1 文件中的 \xc0 字节标记为不正确的编码。我发现,对于我的几个测试用例,encode('binary', :undef => :replace) 似乎有效:iso-8859-1 通过,但捕获了一个序列不正确的 UTF-8 文件。
      • 请参阅this new answer 以获取不受上述问题影响的代码。
      【解决方案6】:

      引发异常的简单方法似乎是:

      untrusted_string.match /./

      【讨论】:

      • 如果你只是想要一个无效字符串的例外,你可以简单地做:raise Exception.new unless string.valid_encoding? 它用替换字符替换坏字节,更具挑战性。
      【解决方案7】:

      要检查字符串是否没有无效序列,请尝试将其转换为二进制编码:

      # Returns true if the string has only valid sequences
      def valid_encoding?(string)
        string.encode('binary', :undef => :replace)
        true
      rescue Encoding::InvalidByteSequenceError => e
        false
      end
      
      p valid_encoding?("\xc0".force_encoding('iso-8859-1'))    # true
      p valid_encoding?("\u1111")                               # true
      p valid_encoding?("\xc0".force_encoding('utf-8'))         # false
      

      此代码替换未定义的字符,因为我们不关心是否存在无法用二进制表示的有效序列。我们只关心是否存在无效序列。

      对该代码稍作修改会返回实际错误,其中包含有关不正确编码的有价值信息:

      # Returns the encoding error, or nil if there isn't one.
      
      def encoding_error(string)
        string.encode('binary', :undef => :replace)
        nil
      rescue Encoding::InvalidByteSequenceError => e
        e.to_s
      end
      
      # Returns truthy if the string has only valid sequences
      
      def valid_encoding?(string)
        !encoding_error(string)
      end
      
      puts encoding_error("\xc0".force_encoding('iso-8859-1'))    # nil
      puts encoding_error("\u1111")                               # nil
      puts encoding_error("\xc0".force_encoding('utf-8'))         # "\xC0" on UTF-8
      

      【讨论】:

        【解决方案8】:

        在 ruby​​ 2.1 中,stdlib 最终通过 scrub 支持这一点。

        http://ruby-doc.org/core-2.1.0/String.html#method-i-scrub

        【讨论】:

          【解决方案9】:

          以下是 Ruby 2.1+ 中的 2 种常见情况以及如何处理它们。我知道,这个问题是指 Ruby v1.9,但也许这有助于其他人通过谷歌找到这个问题。

          情况1

          您的 UTF-8 字符串可能包含一些无效字节
          删除无效字节:

          str = "Partly valid\xE4 UTF-8 encoding: äöüß"
          
          str.scrub('')
           # => "Partly valid UTF-8 encoding: äöüß"
          

          情况2

          您的字符串可以采用 UTF-8 或 ISO-8859-1 编码
          检查它是哪种编码并转换为 UTF-8(如果需要):

          str = "String in ISO-8859-1 encoding: \xE4\xF6\xFC\xDF"
          
          unless str.valid_encoding?
            str.encode!( 'UTF-8', 'ISO-8859-1', invalid: :replace, undef: :replace, replace: '?' )
          end #unless
           # => "String in ISO-8859-1 encoding: äöüß"
          

          备注

          • 以上代码 sn-ps 假定 Ruby 默认将所有字符串编码为 UTF-8。尽管情况几乎总是如此,但您可以通过使用 # encoding: UTF-8 开始您的脚本来确保这一点。

          • 如果无效,则可以通过编程检测大多数多字节编码,例如 UTF-8(在 Ruby 中,请参阅:#valid_encoding?)。但是,不能(很容易)以编程方式检测像ISO-8859-1 这样的单字节编码的无效性。因此上面的代码 sn-p 不能反过来工作,即检测字符串是否有效ISO-8859-1 编码。

          • 尽管UTF-8 作为网络默认编码越来越流行,ISO-8859-1 和其他Latin1 风格在西方国家仍然非常流行,尤其是在北美。请注意,有几个单字节编码非常相似,但与 ISO-8859-1 略有不同。示例:CP1252(又名Windows-1252)、ISO-8859-15

          【讨论】:

          • 虽然我不会将参数传递给擦洗,但我希望将坏字节显示为 unicode 替换字符 (�),而不是完全擦除。我认为默认值是正确适当的默认行为。
          • @jrochkind:我同意对于不同的应用程序,您希望有不同的行为。如果有人会查看转换后的字符串,那么您很可能希望用默认替换字符 (�) 替换坏字节。但是,也有其他情况。举个例子:我来自哪里,我们使用不可靠编码的千兆字节数据流。我们只想过滤某些信息。为了正常工作,我们需要有效的 UTF-8 字符串,但我们不关心坏字节。在这种情况下,我建议删除坏字节。
          • 我确信有些情况是合适的,但它们是特殊用途的。不管有多少 GB 的数据,我认为我永远不会想要一个编码不正确的 Macapá(巴西的一个城市)变成 Macap(印度尼西亚的一个地方)而不是 Macap。作为一般默认建议,不知道某人的特殊用例,默认使用 unicode 替换 char 的例程是合适的——那些 unicode 人知道他们在做什么。
          猜你喜欢
          • 2014-04-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-02-15
          • 1970-01-01
          • 2010-11-26
          • 2012-07-08
          • 2023-04-05
          相关资源
          最近更新 更多