Ruby 1.9:UTF-8中无效的字节序列

117

我正在使用Ruby(1.9)编写网络爬虫,从许多随机站点消耗大量的HTML。
在尝试提取链接时,我决定仅使用.scan(/href="(.*?)"/i)而不是nokogiri/hpricot(可以大大加快速度)。问题是我现在收到了很多"invalid byte sequence in UTF-8"错误。
据我所知,net/http库没有任何特定于编码的选项,因此传入的数据基本上未被正确标记。
实际处理这些传入数据的最佳方法是什么? 我尝试使用将replace和invalid选项设置的.encode,但是目前没有成功...


有些字符可能会破坏字符串,但可以使其对其他库保持有效:valid_string = untrusted_string.unpack('C*').pack('U*') - Marc Seeger
遇到了完全相同的问题,尝试了其他解决方案,但都无济于事。尝试了Marc的方法,但似乎会使一切变得混乱。你确定'U*'可以撤销'C*'吗? - Jordan Feldstein
不会的 :) 我只是在一个网络爬虫中使用它,我更关心第三方库不崩溃,而不是偶尔出现的一句话。 - Marc Seeger
12个回答

176

Ruby 1.9.3中可以使用String.encode来“忽略”无效的UTF-8序列。以下是一个片段,在1.8(iconv)和1.9(String#encode)中都适用:

require 'iconv' unless String.method_defined?(:encode)
if String.method_defined?(:encode)
  file_contents.encode!('UTF-8', 'UTF-8', :invalid => :replace)
else
  ic = Iconv.new('UTF-8', 'UTF-8//IGNORE')
  file_contents = ic.iconv(file_contents)
end

或者,如果你遇到极为棘手的输入,可以进行从 UTF-8 到 UTF-16 再回到 UTF-8 的双重转换:

require 'iconv' unless String.method_defined?(:encode)
if String.method_defined?(:encode)
  file_contents.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '')
  file_contents.encode!('UTF-8', 'UTF-16')
else
  ic = Iconv.new('UTF-8', 'UTF-8//IGNORE')
  file_contents = ic.iconv(file_contents)
end

3
有些输入可能存在问题,因此我使用了双重转换,从UTF-8转换为UTF-16,然后再转回UTF-8。具体代码如下:file_contents.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '') file_contents.encode!('UTF-8', 'UTF-16') - RubenLaguna
7
还有一个选项是使用force_encoding。如果你把ISO8859-1编码的字符串读入成UTF-8编码(导致该字符串包含了非法的UTF-8字符),那么可以使用the_string.force_encoding("ISO8859-1")将其“重新解释”为ISO8859-1编码,然后用原来的编码方式处理该字符串。 - RubenLaguna
3
那个双重编码技巧真救了我一命!不过我想知道为什么需要这样做? - johnf
1
我应该把这些代码放在哪里? - Lefsler
5
我认为双重转换的原因在于它强制进行编码转换(并通过此方式检查无效字符)。如果源字符串已经用UTF-8编码,则调用.encode('UTF-8')没有任何操作,也不会运行任何检查。但是,首先将其转换为UTF-16会强制运行所有无效字节序列的检查,并根据需要进行替换。 - Jo Hund
显示剩余3条评论

84

对我来说,被接受的答案和其他答案都没用。我发现了这篇文章,其中提到

string.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')

这对我解决了问题。


1
这对我解决了问题,而且我喜欢使用非弃用的方法(我现在有Ruby 2.0)。 - La-comadreja
1
这个是唯一一个有效的!我已经尝试了以上所有解决方案,但都没有起作用。 在测试中使用的字符串: “fdsfdsf dfsf sfds fs sdf <div>hello<p>fooo??? {!@#$%^&*()_+}</p></div> \xEF\xBF\xBD \xef\xbf\x9c <div>\xc2\x90</div> \xc2\x90” - Chihung Yu
1
第二个参数'binary'是用来做什么的? - Henley

25

我的当前解决方案是运行:

my_string.unpack("C*").pack("U*")

这将至少解决我的主要问题,即异常。


3
我将结合valid_encoding?使用这种方法,该方法似乎可以检测出问题所在。如果val不符合编码规范,则使用val.unpack('C*').pack('U*') - Aaron Gibralter
这个方案对我起作用了。成功将我的\xB0转换为度符号。甚至valid_encoding?也返回了true,但我仍然会检查它是否不是,并使用Amir上面的答案剥离有问题的字符:string.encode!('UTF-8','binary',invalid:: replace,undef:replace,replace:'')。我之前也尝试过force_encoding的方法,但失败了。 - hamstar
这太棒了。谢谢。 - d_ethier

8

试试这个:

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

最佳答案适用于我的情况!谢谢。 - Aldo

4
attachment = file.read

begin
   # Try it as UTF-8 directly
   cleaned = attachment.dup.force_encoding('UTF-8')
   unless cleaned.valid_encoding?
     # Some of it might be old Windows code page
     cleaned = attachment.encode( 'UTF-8', 'Windows-1252' )
   end
   attachment = cleaned
 rescue EncodingError
   # Force it to UTF-8, throwing out invalid bits
   attachment = attachment.force_encoding("ISO-8859-1").encode("utf-8", replace: nil)
 end

4
我建议您使用HTML解析器。只需找到最快的一个。
解析HTML并不像看起来那么简单。
在UTF-8 HTML文档中,浏览器会解析无效的UTF-8序列,只是放置“�”符号。因此,一旦在HTML中解析无效的UTF-8序列,结果文本就成为有效字符串。
即使在属性值中,您也必须解码HTML实体,如amp。
这是一个很棒的问题,总结了为什么您无法可靠地使用正则表达式解析HTML: RegEx match open tags except XHTML self-contained tags

2
我很想保留正则表达式,因为它大约快了10倍,而且我真的不想正确解析HTML,只想提取链接。
我应该能够通过以下方式在Ruby中替换无效部分:
ok_string = bad_string.encode("UTF-8", {:invalid => :replace, :undef => :replace})但似乎这并不起作用 :(
- Marc Seeger

3

这似乎可行:

def sanitize_utf8(string)
  return nil if string.nil?
  return string if string.valid_encoding?
  string.chars.select { |c| c.valid_encoding? }.join
end

2

我遇到了一个字符串,其中混杂着英文、俄文和其他一些字母,导致出现了异常。我只需要俄文和英文,目前这个解决方案对我来说可行:

ec1 = Encoding::Converter.new "UTF-8","Windows-1251",:invalid=>:replace,:undef=>:replace,:replace=>""
ec2 = Encoding::Converter.new "Windows-1251","UTF-8",:invalid=>:replace,:undef=>:replace,:replace=>""
t = ec2.convert ec1.convert t

1

虽然Nakilon的解决方案有效,至少可以解决错误问题,但在我的情况下,我有一个来自Microsoft Excel转换为CSV的怪异字符,在ruby中注册为(听好了)西里尔字母K,而在ruby中则是加粗的K。为了解决这个问题,我使用了“iso-8859-1”,即CSV.parse(f, :encoding => "iso-8859-1"),这将我的怪异的西里尔字母K变成了更易管理的/\xCA/,然后我可以用string.gsub!(/\xCA/, '')删除它们。


再次强调一下,尽管Nakilon(和其他人)的修复是针对来自Cyrillia的西里尔字符,但这个输出是从xls转换而来的csv的标准输出! - boulder_ruby

0

还有scrub方法来过滤无效字节。

string.scrub('')

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接