ruby 1.9: sequenza di byte non valida in UTF-8


109

Sto scrivendo un crawler in Ruby (1.9) che utilizza molto HTML da molti siti casuali.
Quando ho provato a estrarre i collegamenti, ho deciso di usare solo al .scan(/href="(.*?)"/i)posto di nokogiri / hpricot (maggiore velocità). Il problema è che ora ricevo molti " invalid byte sequence in UTF-8" errori.
Da quello che ho capito, la net/httplibreria non ha opzioni specifiche di codifica e il materiale che arriva non è fondamentalmente etichettato correttamente.
Quale sarebbe il modo migliore per lavorare effettivamente con i dati in arrivo? Ho provato .encodecon le opzioni di sostituzione e non valide impostate, ma finora nessun successo ...


qualcosa che potrebbe rompere i caratteri, ma mantiene la stringa valida per altre librerie: valid_string = untrusted_string.unpack ('C *'). pack ('U *')
Marc Seeger

Avendo il problema esatto, ho provato le stesse altre soluzioni. Nessun amore. Ho provato quello di Marc, ma sembra ingarbugliare tutto. Sei sicuro che 'U*'annulla 'C*'?
Jordan Feldstein

No, non lo fa :) L'ho appena usato in un webcrawler in cui mi interessa che le librerie di terze parti non si blocchino più di me su una frase qua e là.
Marc Seeger

Risposte:


172

In Ruby 1.9.3 è possibile utilizzare String.encode per "ignorare" le sequenze UTF-8 non valide. Ecco uno snippet che funzionerà sia in 1.8 ( iconv ) che 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 se hai un input davvero fastidioso puoi fare una doppia conversione da UTF-8 a UTF-16 e di nuovo 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

3
Con qualche input problematico, uso anche una doppia conversione da UTF-8 a UTF-16 e poi di nuovo a UTF-8 file_contents.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '') file_contents.encode!('UTF-8', 'UTF-16')
RubenLaguna

7
C'è anche la possibilità di force_encoding. Se hai letto un ISO8859-1 come UTF-8 (e quindi quella stringa contiene UTF-8 non valido), puoi "reinterpretarlo" come ISO8859-1 con the_string.force_encoding ("ISO8859-1") e lavorare con quella stringa nella sua codifica reale.
RubenLaguna

3
Quel trucco della doppia codifica ha appena salvato la mia pancetta! Mi chiedo perché sia ​​richiesto però?
johnf

1
Dove devo mettere quelle righe?
Lefsler

5
Penso che la doppia conversione funzioni perché forza una conversione di codifica (e con essa il controllo di caratteri non validi). Se la stringa di origine è già codificata in UTF-8, la semplice chiamata non .encode('UTF-8')è un'operazione necessaria e non vengono eseguiti controlli. Documentazione di Ruby Core per encode . Tuttavia, la conversione in UTF-16 impone prima l'esecuzione di tutti i controlli per le sequenze di byte non valide e le sostituzioni vengono eseguite secondo necessità.
Jo Hund

79

La risposta accettata né l'altra risposta funzionano per me. Ho trovato questo post che ha suggerito

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

Questo ha risolto il problema per me.


1
Questo ha risolto il problema per me e mi piace usare metodi non deprecati (ora ho Ruby 2.0).
La-comadreja

1
Questo è l'unico che funziona! Ho provato tutte le soluzioni precedenti, nessuna di esse funziona con la stringa utilizzata nel test "fdsfdsf dfsf sfds fs sdf <div> ciao <p> fooo ??? {! @ # $% ^ & * () _ +} < / p> </div> \ xEF \ xBF \ xBD \ xef \ xbf \ x9c <div> \ xc2 \ x90 </div> \ xc2 \ x90 "
Chihung Yu

1
A cosa serve il secondo argomento "binario"?
Henley Chiu

24

La mia soluzione attuale è eseguire:

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

Questo almeno eliminerà le eccezioni che erano il mio problema principale


3
Sto usando questo metodo in combinazione con il valid_encoding?quale sembra rilevare quando qualcosa non va. val.unpack('C*').pack('U*') if !val.valid_encoding?.
Aaron Gibralter

Questo ha funzionato per me. Converte con successo la mia \xB0schiena in simboli di gradi. Anche il valid_encoding?ritorna vero, ma io continuo a controllare se non lo fa e togliere i caratteri offendere usando la risposta di Amir sopra: string.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: ''). Avevo anche provato il force_encodingpercorso ma non è riuscito.
hamstar

Questo è fantastico. Grazie.
d_ethier

8

Prova questo:

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

La migliore risposta per il mio caso! Grazie
Aldo

4

Ti consiglio di utilizzare un parser HTML. Trova quello più veloce.

L'analisi dell'HTML non è così facile come potrebbe sembrare.

I browser analizzano le sequenze UTF-8 non valide, nei documenti HTML UTF-8, inserendo semplicemente il simbolo " ". Quindi, una volta che la sequenza UTF-8 non valida nell'HTML viene analizzata, il testo risultante è una stringa valida.

Anche all'interno dei valori degli attributi devi decodificare entità HTML come amp

Ecco una grande domanda che riassume il motivo per cui non è possibile analizzare in modo affidabile l'HTML con un'espressione regolare: RegEx corrisponde ai tag aperti tranne ai tag XHTML autonomi


2
Mi piacerebbe mantenere la regexp poiché è circa 10 volte più veloce e non voglio davvero analizzare correttamente l'html ma voglio solo estrarre i collegamenti. Dovrei essere in grado di sostituire le parti non valide in ruby ​​semplicemente facendo: ok_string = bad_string.encode ("UTF-8", {: invalid =>: replace,: undef =>: replace}) ma non sembra lavoro :(
Marc Seeger

3

Questo sembra funzionare:

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

Ho riscontrato una stringa, che aveva mescolanze di inglese, russo e altri alfabeti, che hanno causato un'eccezione. Ho bisogno solo del russo e dell'inglese, e questo attualmente funziona per me:

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

Mentre la soluzione di Nakilon funziona, almeno per quanto riguarda il superamento dell'errore, nel mio caso, ho avuto questo strano carattere f-ed up originato da Microsoft Excel convertito in CSV che si stava registrando in ruby ​​come (prendilo) cirillico K che in ruby era un K in grassetto. Per risolvere questo problema ho usato "iso-8859-1", vale a dire. CSV.parse(f, :encoding => "iso-8859-1"), che ha trasformato le mie bizzarre K in cirillico in un molto più maneggevole /\xCA/, che ho potuto rimuovere constring.gsub!(/\xCA/, '')


Di nuovo, voglio solo notare che mentre la correzione di Nakilon (e altri) era per i caratteri cirillici originati da (haha) Cyrillia, questo output è l'output standard per un csv che è stato convertito da xls!
boulder_ruby

0

Prima dell'uso scan, assicurati che l' Content-Typeintestazione della pagina richiesta sia text/html, poiché possono esserci collegamenti a cose come immagini che non sono codificate in UTF-8. La pagina potrebbe anche essere non html se hai preso hrefun <link>elemento in qualcosa di simile . La modalità di verifica varia a seconda della libreria HTTP che stai utilizzando. Quindi, assicurati che il risultato sia solo ascii con String#ascii_only?(non UTF-8 perché HTML dovrebbe usare solo ascii, le entità possono essere usate altrimenti). Se entrambi i test vengono superati, è sicuro da usare scan.


grazie, ma non è un problema mio :) Estraggo comunque solo la parte host dell'URL e premo solo la prima pagina. Il mio problema è che il mio input apparentemente non è UTF-8 e la codifica 1.9 foo va in tilt
Marc Seeger

@ Marc Seeger: Cosa intendi con "il mio input"? Stdin, l'URL o il corpo della pagina?
Adrian

HTML può essere codificato in UTF-8: en.wikipedia.org/wiki/Character_encodings_in_HTML
Eduardo

il mio input = il corpo della pagina @Eduardo: Lo so. Il mio problema è che i dati provenienti da rete / http sembrano avere una codifica errata di tanto in tanto
Marc Seeger

Non è raro che le pagine web abbiano effettivamente una cattiva codifica per davvero. L'intestazione della risposta potrebbe dire che si tratta di una codifica, ma in realtà serve un'altra codifica.
Sunkencity

-1

Se non ti "interessano" i dati puoi semplicemente fare qualcosa come:

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

L'ho appena valid_encoding?passato. Il mio è un campo di ricerca, quindi trovavo la stessa stranezza più e più volte, quindi ho usato qualcosa del tipo: solo per evitare che il sistema si rompa. Dal momento che non controllo l'esperienza dell'utente per l'autovalidazione prima di inviare queste informazioni (come il feedback automatico per dire "finto!") Posso semplicemente prenderlo, rimuoverlo e restituire risultati vuoti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.