ruby 1.9: secuencia de bytes no válida en UTF-8

109

Estoy escribiendo un rastreador en Ruby (1.9) que consume mucho HTML de muchos sitios aleatorios.
Al intentar extraer enlaces, decidí usar simplemente en .scan(/href="(.*?)"/i)lugar de nokogiri / hpricot (mayor aceleración). El problema es que ahora recibo muchos " invalid byte sequence in UTF-8" errores.
Por lo que entendí, la net/httpbiblioteca no tiene opciones específicas de codificación y las cosas que vienen, básicamente, no están etiquetadas correctamente.
¿Cuál sería la mejor manera de trabajar realmente con esos datos entrantes? Intenté .encodecon el conjunto de opciones de reemplazo y no válidas, pero no tuve éxito hasta ahora ...

Marc Seeger
fuente
algo que puede romper caracteres, pero mantiene la cadena válida para otras bibliotecas: valid_string = untrusted_string.unpack ('C *'). pack ('U *')
Marc Seeger
Teniendo el problema exacto, probé las mismas otras soluciones. Sin amor. Probé el de Marc, pero parece distorsionarlo todo. ¿Seguro que 'U*'deshace 'C*'?
Jordan Feldstein
No, no lo hace :) Solo lo usé en un webcrawler en el que me preocupo por que las bibliotecas de terceros no se bloqueen más de lo que lo hago con una oración aquí y allá.
Marc Seeger

Respuestas:

172

En Ruby 1.9.3 es posible usar String.encode para "ignorar" las secuencias UTF-8 inválidas. Aquí hay un fragmento que funcionará tanto en 1.8 ( iconv ) como en 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

o si tiene una entrada realmente problemática, puede hacer una doble conversión de UTF-8 a UTF-16 y volver a 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
RubenLaguna
fuente
3
Con alguna entrada problemática, también uso una doble conversión de UTF-8 a UTF-16 y luego de regreso a UTF-8 file_contents.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '') file_contents.encode!('UTF-8', 'UTF-16')
RubenLaguna
7
También existe la opción de force_encoding. Si ha leído un ISO8859-1 como un UTF-8 (y, por lo tanto, esa cadena contiene UTF-8 no válido), puede "reinterpretarlo" como ISO8859-1 con the_string.force_encoding ("ISO8859-1") y simplemente trabajar con esa cadena en su codificación real.
RubenLaguna
3
¡Ese truco de doble codificación acaba de salvar mi tocino! Sin embargo, me pregunto por qué se requiere.
johnf
1
¿Dónde debería poner esas líneas?
Lefsler
5
Creo que la doble conversión funciona porque fuerza una conversión de codificación (y con ella la verificación de caracteres no válidos). Si la cadena de origen ya está codificada en UTF-8, entonces simplemente llamar no .encode('UTF-8')es una operación y no se ejecutan verificaciones. Documentación de Ruby Core para codificar . Sin embargo, convertirlo a UTF-16 primero obliga a que se ejecuten todas las comprobaciones de secuencias de bytes no válidas, y los reemplazos se realizan según sea necesario.
Jo Hund
79

La respuesta aceptada ni la otra respuesta funcionan para mí. Encontré esta publicación que sugirió

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

Esto me solucionó el problema.

Amir Raminfar
fuente
1
Esto me solucionó el problema y me gusta usar métodos no obsoletos (ahora tengo Ruby 2.0).
La-comadreja
1
¡Este es el único que funciona! Probé todas las soluciones anteriores, ninguna de ellas funciona Cadena que se usó para probar "fdsfdsf dfsf sfds fs sdf <div> hola <p> fooo ??? {! @ # $% ^ & * () _ +} < / p> </div> \ xEF \ xBF \ xBD \ xef \ xbf \ x9c <div> \ xc2 \ x90 </div> \ xc2 \ x90 "
Chihung Yu
1
¿Para qué sirve el segundo argumento 'binario'?
Henley Chiu
24

Mi solución actual es ejecutar:

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

Esto al menos eliminará las excepciones, que era mi principal problema.

Marc Seeger
fuente
3
Estoy usando este método en combinación con el valid_encoding?que parece detectar cuando algo anda mal. val.unpack('C*').pack('U*') if !val.valid_encoding?.
Aaron Gibralter
Este funcionó para mí. Convierte con éxito mi \xB0espalda en símbolos de grados. Incluso la valid_encoding?vuelve verdadera pero todavía comprobar si no es así y la tira a cabo los caracteres ofensivos usando la respuesta de Amir arriba: string.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: ''). También había probado la force_encodingruta pero falló.
hamstar
Esto es genial. Gracias.
d_ethier
8

Prueba esto:

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
Ranjithkumar Ravi
fuente
¡La mejor respuesta para mi caso! Gracias
Aldo
4

Te recomiendo que uses un analizador HTML. Solo encuentra el más rápido.

Analizar HTML no es tan fácil como parece.

Los navegadores analizan secuencias UTF-8 no válidas, en documentos HTML UTF-8, simplemente poniendo el símbolo " ". Entonces, una vez que se analiza la secuencia UTF-8 no válida en el HTML, el texto resultante es una cadena válida.

Incluso dentro de los valores de los atributos, debe decodificar entidades HTML como amp

Aquí hay una gran pregunta que resume por qué no puede analizar HTML de manera confiable con una expresión regular: RegEx coincide con las etiquetas abiertas, excepto las etiquetas autocontenidas XHTML

Eduardo
fuente
2
Me encantaría mantener la expresión regular, ya que es aproximadamente 10 veces más rápido y realmente no quiero analizar el html correctamente, solo quiero extraer enlaces. Debería poder reemplazar las partes inválidas en ruby ​​simplemente haciendo: ok_string = bad_string.encode ("UTF-8", {: invalid =>: replace,: undef =>: replace}) pero eso no parece trabajo :(
Marc Seeger
3

Esto 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
Spajus
fuente
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
rusllonrails
fuente
2

Encontré cadenas, que tenían mezclas de inglés, ruso y algunos otros alfabetos, lo que causó una excepción. Solo necesito ruso e inglés, y esto actualmente me funciona:

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
Nakilon
fuente
1

Si bien la solución de Nakilon funciona, al menos en lo que respecta a superar el error, en mi caso, tuve este extraño carácter f-ed up que se originó en Microsoft Excel convertido a CSV que se registraba en ruby ​​como (obtenga esto) cirílico K que en ruby era una K en negrita. Para arreglar esto, usé 'iso-8859-1' a saber. CSV.parse(f, :encoding => "iso-8859-1"), que convirtió mis extrañas K cirílicas deaky en una mucho más manejable /\xCA/, que luego pude eliminar constring.gsub!(/\xCA/, '')

boulder_ruby
fuente
Nuevamente, solo quiero señalar que mientras que la corrección de Nakilon (y otros) fue para caracteres cirílicos que se originaron en (jaja) Cyrillia, esta salida es una salida estándar para un csv que se convirtió de xls.
boulder_ruby
0

Antes de usar scan, asegúrese de que el Content-Typeencabezado de la página solicitada sea text/html, ya que puede haber enlaces a cosas como imágenes que no están codificadas en UTF-8. La página también podría ser no html si seleccionó hrefalgo como un <link>elemento. La forma de comprobar esto varía según la biblioteca HTTP que esté utilizando. Luego, asegúrese de que el resultado sea solo ascii con String#ascii_only?(no UTF-8 porque se supone que HTML solo usa ascii, las entidades se pueden usar de lo contrario). Si ambas pruebas pasan, es seguro de usar scan.

Adrian
fuente
gracias, pero ese no es mi problema :) De todos modos, solo extraigo la parte del host de la URL y solo accedo a la página principal. Mi problema es que mi entrada aparentemente no es UTF-8 y la codificación 1.9 foo se vuelve loca
Marc Seeger
@Marc Seeger: ¿Qué quieres decir con "mi opinión"? ¿Stdin, la URL o el cuerpo de la página?
Adrian
HTML se puede codificar en UTF-8: en.wikipedia.org/wiki/Character_encodings_in_HTML
Eduardo
mi entrada = el cuerpo de la página @Eduardo: Lo sé. Mi problema es que los datos que vienen de net / http parecen tener una codificación incorrecta de vez en cuando
Marc Seeger
No es raro que las páginas web tengan realmente una mala codificación. El encabezado de la respuesta puede decir que es una codificación pero luego sirve otra codificación.
sunkencity
-1

Si no le "importan" los datos, puede hacer algo como:

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

Solía valid_encoding?pasarlo. El mío es un campo de búsqueda, por lo que estaba encontrando la misma rareza una y otra vez, así que usé algo como: solo para que el sistema no se rompa. Dado que no controlo la experiencia del usuario para autovalidar antes de enviar esta información (como retroalimentación automática para decir "¡tonto!"), Puedo simplemente asimilarlo, eliminarlo y devolver resultados en blanco.

pjammer
fuente