2012-04-17 59 views
25

我有一个字符串,我从某种输入中读取。ruby​​ 1.9,force_encoding,但是检查

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

string.force_encoding("utf8") 

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

通常情况下,force_encoding(“utf8”)会遇到这样的字节?我认为它不会。

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

但我没有做#encode,我正在做一个#force_encoding。它没有这样的选择。

会是有意义的

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

得到一个例外,对吗?正常编码 utf8 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,然后用别的东西来代替他们的 - 转换为不同的编码时。但我不想想要转换为不同的编码,我想留在utf8 - 但我可能想提出如果有一个无效的字节在那里,或者我可能想用替换字符替换无效的字节。

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

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

回答

16

(更新:看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,它只是没有文档,很少有人知道它(或许很少有人会说英语)。尽管我在某处的博客上看到了这种用法,所以别人也知道这一点!

+0

使用Ruby 1.9.3-p484,这错误地将ISO-8859-1文件中的\ xc0字节标记为不正确的编码。我发现,对于我的少数测试用例,编码('binary',:undef =>:replace)似乎工作:iso-8859-1通过,但是序列不正确的UTF-8文件是抓住。 – 2014-02-10 19:56:06

+0

查看[这个新答案](http://stackoverflow.com/a/21686992/238886)的代码,不会遇到我上面提到的问题。 – 2014-02-10 20:46:22

0

关于我能想到的唯一的事情就是转码的东西和背部,不会在往返损坏字符串:

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

似乎有点浪费,但。

+0

啊。除了浪费之外,它还要求你确定你知道什么编码可以循环而不丢失任何东西。我想要一个能够处理任意输入编码的通用解决方案 - ruby​​知道如何在实际转码时使用任何编码来做到这一点,为什么它不能为我做到这一点?烦人。 – jrochkind 2012-04-18 01:42:09

+2

您可以随时在任何UTF之间往返;无论您如何表示Unicode,Unicode都是Unicode。只有当你摆脱Unicode时,你才会在翻译中失去一些东西。 – 2012-04-19 00:29:43

+0

对,我想要一个不假设unicode的解决方案。 – jrochkind 2012-04-19 12:14:41

4

# 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 

更多的咆哮确保您的脚本文件本身被保存为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 
+0

我不想将编码更改为ISO-8859-1。我想保留原始编码。现在你会说“好吧,然后转码到8859 1,然后再回来。”我想要一个适用于任何编码的解决方案;您无法将其转码为8859,并且不会丢失任何编码。 – jrochkind 2012-04-19 12:15:31

+0

好吧,只是编辑我的答案 – peter 2012-04-19 15:57:40

+0

谢谢。我独立地得到了类似的东西,但是你能解释一下它的作用:'c.encode!(Encoding.locale_charmap,:invalid =>:replace)'?这是一个转码吗?我不希望对字符串进行转码(更改编码),无论它开始使用什么编码以及我的默认区域设置编码是什么。但是我想我已经到达了你最终会考虑到这一点的地方,在这个问题上看到我的自我回答。 – jrochkind 2012-04-20 15:13:56

0

如果你是一个“真实”的用例这样做 - 例如用于分析用户输入的不同的字符串,而不是只为的是能够“解码”完全随机的文件所赋可以按照你的意愿编写尽可能多的编码,那么我想你至少可以假设每个字符串的所有字符都具有相同的编码。

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

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 
} 

我不是红宝石“亲”以任何方式,所以请原谅,如果我的解决方法是错误的,甚至有点幼稚..

我只是想给回我所能,这是我来到的,而当我(我仍然)正在为这个小解析器编写任意编码的字符串时,我正在为一个研究项目做些什么。

虽然我发布这个,但我必须承认,我还没有完全测试它..我只是得到了一些“积极”的结果,但我感到非常兴奋,可能已经找到我是什么努力寻找(并一直在阅读关于这个的所有时间......),我只是觉得有必要尽快分享它,希望它能帮助人们节省一些时间给那些一直在寻找它的人只要我一直... ..如果它按预期工作:)

+0

这就是我最终做的:https://github.com/jrochkind/ensure_valid_encoding/ blob/master/lib/ensure_valid_encoding.rb 关键是我知道字符串_supposed_被编码为,但它可能有坏字节。你的解决方案更多地试图猜测编码是“真的”,这是一个不同的问题。 – jrochkind 2013-03-12 01:42:56

+0

回顾一下:1)你要么有不好的编码字符或数据损坏,(从你的github的理由你认为这两件事情可能是问题的原因),2)你似乎不关心错误的编码,因为你只想保持有效的UTF-8字符(你不要检查坏数据是否对不同的编码有效) - 人们建议转换为另一种编码作为检查非有效字节的手段,但是你害怕丢失一些数据。如果你没有首先验证假定编码的有效性,那么有什么意义?(如此可能会丢失数据?) – 2013-03-24 18:01:49

+0

谢谢你的回答,试图说服我做我需要做的事情是愚蠢的,但显然许多人不同意,因为红宝石将其添加到stdlib与在ruby 2.1中的字符串#擦洗!事实上,我明白自己在做什么,并且在很多情况下这样做是有意义的(在这种情况下,你是否尝试过检查vim或其他最喜欢的编辑器?),但是这张票不是为了说服你这个事实。 – jrochkind 2014-11-06 20:25:50

0

一个简单的方法挑起例外似乎是:

untrusted_string.match /./

+1

如果你只是想要一个无效字符串的异常,你可以简单地这样做:'引发Exception.new,除非string.valid_encoding?'它用替换字符替换坏字节更具挑战性。 – jrochkind 2013-11-10 14:59:53

3

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

# 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 
0

这里有两个常见的情况,以及如何在红宝石2.1+对付他们。我知道,这个问题涉及Ruby v1.9,但也许这有助于其他人通过Google找到此问题。

情况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: äöüß" 

注意

  • 上面的代码片段假定红宝石编码您的所有字符串在UTF-8 。尽管这几乎总是如此,但您可以通过# encoding: UTF-8开始脚本来确保这一点。

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

  • 即使UTF-8已经成为在网络,ISO-8859-1Latin1味道仍然在西方国家非常流行,特别是在北美地区的默认编码越来越受欢迎。请注意,有几个单字节编码非常相似,但与ISO-8859-1略有不同。实例:CP1252(又名Windows-1252),ISO-8859-15

+0

虽然我不会传递一个参数来清理,但是我会将显示为unicode替换字符( )的坏字节全部删除。我认为默认是正确的适当的默认行为。 – jrochkind 2016-02-18 21:25:14

+0

@jrochkind:我同意为不同的应用程序你想有不同的行为。如果一个人会查看转换后的字符串,那么很可能你想用缺省替换字符( )替换坏字节。但是,也有其他情况。举一个例子:我来自哪里,我们使用千兆字节的数据流进行不可靠的编码。我们只想过滤某些信息。要正常工作,我们需要有效的UTF-8字符串,但我们不关心坏字节。在这种情况下,我建议删除坏字节。 – 2016-02-19 23:19:50

+0

我确定有些情况是合适的,但它们是特殊用途的。无论有多少千兆字节的数据,我都不认为我会想要一个不适当编码的Macapá(巴西的一个城市)变成Macap(印度尼西亚的一个地方)而不是Macap 。作为不知道某人的特殊用例的一般默认建议,使用unicode替换字符的例程默认是合适的 - 那些unicode人员知道他们在做什么。 – jrochkind 2016-02-20 02:26:04