ruby 1.9: sequência de bytes inválida em UTF-8


109

Estou escrevendo um rastreador em Ruby (1.9) que consome muito HTML de muitos sites aleatórios.
Ao tentar extrair links, decidi usar apenas .scan(/href="(.*?)"/i)nokogiri / hpricot (maior aceleração). O problema é que agora recebo muitos invalid byte sequence in UTF-8erros " ".
Pelo que entendi, a net/httpbiblioteca não tem opções específicas de codificação e o material que vem basicamente não está devidamente marcado.
Qual seria a melhor maneira de realmente trabalhar com os dados recebidos? Tentei .encodecom o conjunto de opções substituir e inválido, mas sem sucesso até agora ...


algo que pode quebrar caracteres, mas mantém a string válida para outras bibliotecas: valid_string = untrusted_string.unpack ('C *'). pack ('U *')
Marc Seeger

Tendo o problema exato, tentei as mesmas outras soluções. Sem amor. Tentei o de Marc, mas parece bagunçar tudo. Tem certeza que 'U*'desfaz 'C*'?
Jordan Feldstein

Não, não funciona :) Acabei de usar isso em um webcrawler onde me preocupo com bibliotecas de terceiros não travando mais do que com uma frase aqui e ali.
Marc Seeger

Respostas:


172

No Ruby 1.9.3, é possível usar String.encode para "ignorar" as sequências UTF-8 inválidas. Aqui está um snippet que funcionará em 1.8 ( iconv ) e 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

ou se você tiver realmente problemas de entrada, pode fazer uma conversão dupla de UTF-8 para UTF-16 e de volta para 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
Com alguma entrada problemática, também uso uma conversão dupla de UTF-8 para UTF-16 e, em seguida, de volta para UTF-8 file_contents.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '') file_contents.encode!('UTF-8', 'UTF-16')
RubenLaguna

7
Também existe a opção de force_encoding. Se você leu um ISO8859-1 como um UTF-8 (e, portanto, essa string contém UTF-8 inválido), então você pode "reinterpretar" como ISO8859-1 com the_string.force_encoding ("ISO8859-1") e apenas trabalhar com essa string em sua codificação real.
RubenLaguna de

3
Esse truque de codificação dupla acabou de salvar meu Bacon! Eu me pergunto por que isso é necessário?
johnf

1
Onde devo colocar essas linhas?
Lefsler

5
Acho que a dupla conversão funciona porque força uma conversão de codificação (e com ela a verificação de caracteres inválidos). Se a string de origem já estiver codificada em UTF-8, apenas chamar .encode('UTF-8')é um ambiente autônomo e nenhuma verificação é executada. Documentação do núcleo do Ruby para codificação . No entanto, convertê-lo em UTF-16 primeiro força a execução de todas as verificações de sequências de bytes inválidas e as substituições são feitas conforme necessário.
Jo Hund

79

A resposta aceita nem a outra resposta funcionam para mim. Eu encontrei esta postagem que sugeria

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

Isso resolveu o problema para mim.


1
Isso resolveu o problema para mim e gosto de usar métodos não obsoletos (agora tenho Ruby 2.0).
La-comadreja de

1
Este é o único que funciona! Eu tentei todas as soluções acima, nenhuma delas funciona. String usada no teste "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
Qual é o segundo argumento 'binário' para?
Henley Chiu

24

Minha solução atual é executar:

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

Isso vai pelo menos me livrar das exceções que eram o meu principal problema


3
Estou usando esse método em combinação com o valid_encoding?que parece detectar quando algo está errado. val.unpack('C*').pack('U*') if !val.valid_encoding?.
Aaron Gibralter

Este funcionou para mim. Converte com sucesso minhas \xB0costas em símbolos de graus. Mesmo a valid_encoding?volta verdade, mas eu ainda verificar se isso não acontecer e retirar os personagens ofender usando a resposta de Amir acima: string.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: ''). Eu também tentei a force_encodingrota, mas falhou.
Hamstar

Isso é ótimo. Obrigado.
d_ethier

8

Experimente isto:

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

Melhor resposta para o meu caso! Obrigado
Aldo

4

Eu recomendo que você use um analisador HTML. Basta encontrar o mais rápido.

Analisar HTML não é tão fácil quanto pode parecer.

Os navegadores analisam sequências UTF-8 inválidas, em documentos HTML UTF-8, apenas colocando o símbolo " ". Portanto, uma vez que a sequência UTF-8 inválida no HTML é analisada, o texto resultante é uma string válida.

Mesmo dentro dos valores de atributo, você deve decodificar entidades HTML como amp

Esta é uma grande pergunta que resume por que você não pode analisar HTML de forma confiável com uma expressão regular: RegEx corresponde a tags abertas, exceto tags XHTML independentes


2
Adoraria manter o regexp, pois é cerca de 10 vezes mais rápido e realmente não quero analisar o html corretamente, mas apenas extrair os links. Devo ser capaz de substituir as partes inválidas no ruby ​​apenas fazendo: ok_string = bad_string.encode ("UTF-8", {: invalid =>: replace,: undef =>: replace}), mas isso não parece trabalho :(
Marc Seeger

3

Isso parece funcionar:

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

3
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

2

Eu encontrei string, que tinha combinações de inglês, russo e alguns outros alfabetos, o que causou exceção. Preciso apenas de russo e inglês, e isso atualmente funciona para mim:

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

Embora a solução de Nakilon funcione, pelo menos no que diz respeito a superar o erro, no meu caso, eu tinha esse caractere estranho f-ed up originário do Microsoft Excel convertido para CSV que estava sendo registrado em ruby ​​como um (veja só) cirílico K que em ruby era um K. em negrito. Para corrigir isso, usei 'iso-8859-1' viz. CSV.parse(f, :encoding => "iso-8859-1"), que transformou meu K cirílico esquisito em muito mais gerenciável /\xCA/, que eu poderia remover comstring.gsub!(/\xCA/, '')


Novamente, eu só quero observar que, embora a correção de Nakilon (e outros) fosse para caracteres cirílicos originados de (haha) Cyrillia, esta saída é a saída padrão para um csv que foi convertido de xls!
boulder_ruby

0

Antes de usar scan, certifique-se de que o Content-Typecabeçalho da página solicitada seja text/html, uma vez que pode haver links para coisas como imagens que não estão codificadas em UTF-8. A página também pode ser não-html se você selecionar um hrefem algo como um <link>elemento. A forma de verificar isso varia de acordo com a biblioteca HTTP que você está usando. Em seguida, certifique-se de que o resultado seja apenas ascii com String#ascii_only?(não UTF-8 porque o HTML deve usar apenas ascii, as entidades podem ser usadas de outra forma). Se ambos os testes passarem, é seguro usar scan.


obrigado, mas esse não é o meu problema :) Eu só extraio a parte do host da URL e acesso apenas a página inicial. Meu problema é que minha entrada aparentemente não é UTF-8 e o foo de codificação 1.9 fica descontrolado
Marc Seeger

@Marc Seeger: O que você quer dizer com "minha opinião"? Stdin, o URL ou o corpo da página?
Adrian

HTML pode ser codificado em UTF-8: en.wikipedia.org/wiki/Character_encodings_in_HTML
Eduardo

minha entrada = corpo da página @Eduardo: Eu sei. Meu problema é que os dados vindos de net / http parecem ter uma codificação ruim de vez em quando
Marc Seeger

Não é incomum que páginas da web tenham uma codificação ruim de verdade. O cabeçalho da resposta pode dizer que é uma codificação, mas na verdade serve outra codificação.
afundamento em

-1

Se você não "se importa" com os dados, pode simplesmente fazer algo como:

search_params = params[:search].valid_encoding? ? params[:search].gsub(/\W+/, '') : "nothing"

Eu costumava valid_encoding?passar por ele. O meu é um campo de busca, então eu estava encontrando a mesma estranheza repetidamente, então usei algo como: apenas para que o sistema não quebrasse. Como eu não controlo a experiência do usuário para autovalidar antes de enviar essas informações (como feedback automático para dizer "simulado!"), Posso simplesmente pegar, retirar e retornar resultados em branco.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.