ruby 1.9: nieprawidłowa sekwencja bajtów w UTF-8

109

Piszę robota w języku Ruby (1.9), który zużywa dużo kodu HTML z wielu przypadkowych witryn.
Próbując wyodrębnić linki, zdecydowałem się po prostu użyć .scan(/href="(.*?)"/i)zamiast nokogiri / hpricot (duże przyspieszenie). Problem w tym, że teraz otrzymuję dużo " invalid byte sequence in UTF-8" błędów.
Z tego, co zrozumiałem, net/httpbiblioteka nie ma żadnych konkretnych opcji kodowania, a rzeczy, które wchodzą, są w zasadzie nieprawidłowo oznaczone.
Jaki byłby najlepszy sposób pracy z przychodzącymi danymi? Próbowałem .encodez ustawionymi zamiennikami i nieprawidłowymi opcjami, ale jak dotąd bez powodzenia ...

Marc Seeger
źródło
coś, co może zepsuć znaki, ale utrzymuje ciąg prawidłowy dla innych bibliotek: valid_string = niezaufany_string.unpack ('C *'). pack ('U *')
Marc Seeger
Mając dokładny problem, wypróbowałem te same inne rozwiązania. Bez miłości. Próbowałem Marca, ale wydaje się, że wszystko psuje. Czy na pewno 'U*'cofa 'C*'?
Jordan Feldstein,
Nie, nie działa :) Po prostu użyłem tego w webcrawlerze, w którym zależy mi na tym, aby biblioteki innych firm nie rozwiały się bardziej niż o zdanie tu i tam.
Marc Seeger

Odpowiedzi:

172

W Ruby 1.9.3 możliwe jest użycie String.encode do "zignorowania" nieprawidłowych sekwencji UTF-8. Oto fragment, który będzie działał zarówno w wersji 1.8 ( iconv ), jak i 1.9 ( kodowanie String # ):

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

lub jeśli masz naprawdę kłopotliwe dane wejściowe, możesz wykonać podwójną konwersję z UTF-8 na UTF-16 iz powrotem na 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
źródło
3
Z pewnymi problematycznymi danymi wejściowymi używam również podwójnej konwersji z UTF-8 na UTF-16, a następnie z powrotem do UTF-8 file_contents.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '') file_contents.encode!('UTF-8', 'UTF-16')
RubenLaguna
7
Istnieje również opcja force_encoding. Jeśli odczytałeś ISO8859-1 jako UTF-8 (a więc ten ciąg zawiera nieprawidłowy UTF-8), możesz go "reinterpretować" jako ISO8859-1 z the_string.force_encoding ("ISO8859-1") i po prostu działać z tym ciągiem w jego prawdziwym kodowaniu.
RubenLaguna
3
Ta sztuczka z podwójnym kodowaniem uratowała mój boczek! Zastanawiam się, dlaczego jest to wymagane?
johnf
1
Gdzie mam umieścić te linie?
Lefsler
5
Myślę, że podwójna konwersja działa, ponieważ wymusza konwersję kodowania (a wraz z nią sprawdzenie nieprawidłowych znaków). Jeśli ciąg źródłowy jest już zakodowany w UTF-8, to samo wywołanie nie .encode('UTF-8')działa i nie są wykonywane żadne testy. Dokumentacja Ruby Core do kodowania . Jednak przekonwertowanie go na UTF-16 wymusza najpierw przeprowadzenie wszystkich kontroli nieprawidłowych sekwencji bajtów, a zastępowanie jest wykonywane w razie potrzeby.
Jo Hund,
79

Zaakceptowana odpowiedź ani inna odpowiedź mi nie pasuje. Znalazłem ten post, który zasugerował

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

To rozwiązało problem.

Amir Raminfar
źródło
1
To rozwiązało problem i lubię używać metod, które nie są przestarzałe (mam teraz Ruby 2.0).
La-comadreja
1
Tylko ten działa! Wypróbowałem wszystkie powyższe rozwiązania, żadne z nich nie działa. Ciąg używany do testowania "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
Do czego służy drugi argument „binarny”?
Henley Chiu
24

Moje obecne rozwiązanie polega na uruchomieniu:

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

Pozwoli to przynajmniej pozbyć się wyjątków, które były moim głównym problemem

Marc Seeger
źródło
3
Używam tej metody w połączeniu z valid_encoding?którą wydaje się wykrywać, kiedy coś jest nie tak. val.unpack('C*').pack('U*') if !val.valid_encoding?.
Aaron Gibralter
Ten pracował dla mnie. Pomyślnie konwertuje moje \xB0plecy do symboli stopni. Nawet valid_encoding?wraca prawda, ale ja jeszcze sprawdzić, czy nie ma i rozebrać się znaki się naruszeń za pomocą odpowiedź Amir jest powyżej: string.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: ''). Próbowałem też force_encodingtrasy, ale to się nie udało.
hamstar
To jest świetne. Dzięki.
d_ethier
8

Spróbuj tego:

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
źródło
Najlepsza odpowiedź w moim przypadku! Dzięki
Aldo
4

Zalecam użycie parsera HTML. Po prostu znajdź najszybszy.

Przetwarzanie kodu HTML nie jest tak łatwe, jak mogłoby się wydawać.

Przeglądarki analizują nieprawidłowe sekwencje UTF-8 w dokumentach HTML UTF-8, po prostu umieszczając symbol „ ”. Zatem po przeanalizowaniu nieprawidłowej sekwencji UTF-8 w kodzie HTML otrzymany tekst jest prawidłowym ciągiem.

Nawet wewnątrz wartości atrybutów musisz dekodować jednostki HTML, takie jak amp

Oto świetne pytanie, które podsumowuje, dlaczego nie można wiarygodnie przeanalizować HTML za pomocą wyrażenia regularnego: RegEx pasuje do otwartych tagów z wyjątkiem tagów niezależnych XHTML

Eduardo
źródło
2
Chciałbym zachować to wyrażenie regularne, ponieważ jest około 10 razy szybsze i naprawdę nie chcę poprawnie analizować kodu HTML, ale chcę po prostu wyodrębnić linki. Powinienem być w stanie zastąpić nieprawidłowe części w Rubim, wykonując po prostu: ok_string = bad_string.encode ("UTF-8", {: invalid =>: replace,: undef =>: replace}), ale to nie wygląda praca :(
Marc Seeger
3

To wydaje się działać:

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
źródło
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
źródło
2

Napotkałem ciąg znaków, który zawierał mieszankę angielskiego, rosyjskiego i kilku innych alfabetów, co spowodowało wyjątek. Potrzebuję tylko języka rosyjskiego i angielskiego, a to obecnie działa dla mnie:

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
źródło
1

Podczas gdy rozwiązanie Nakilona działa, przynajmniej jeśli chodzi o omijanie błędu, w moim przypadku miałem ten dziwny, przerobiony znak pochodzący z Microsoft Excela przekonwertowany na CSV, który rejestrował się w ruby ​​jako (pobierz to) cyrylicę K, która w ruby był pogrubionym K. Aby to naprawić, użyłem „iso-8859-1”, a mianowicie. CSV.parse(f, :encoding => "iso-8859-1"), co zmieniło moje dziwaczne, deaky cyrillic K w znacznie łatwiejsze w zarządzaniu /\xCA/, które mogłem następnie usunąć za pomocąstring.gsub!(/\xCA/, '')

boulder_ruby
źródło
Ponownie chcę tylko zauważyć, że chociaż poprawka Nakilona (i innych) dotyczyła znaków cyrylicy pochodzących z (haha) Cyrillia, to wyjście jest standardowym wyjściem dla pliku csv, który został przekonwertowany z xls!
boulder_ruby
0

Przed użyciem scanupewnij się, że Content-Typenagłówek żądanej strony to text/html, ponieważ mogą istnieć linki do rzeczy, takich jak obrazy, które nie są zakodowane w UTF-8. Strona może być również hrefw formacie innym niż HTML, jeśli wybrałeś element w rodzaju <link>elementu. Sposób sprawdzenia tego zależy od używanej biblioteki HTTP. Następnie upewnij się, że wynikiem jest tylko ascii z String#ascii_only?(nie UTF-8, ponieważ HTML powinien używać tylko ascii, encje mogą być używane w inny sposób). Jeśli oba te testy przejdą pomyślnie, można go bezpiecznie używać scan.

Adrian
źródło
dzięki, ale to nie mój problem :) I tak wyodrębniam tylko część adresu URL hosta i trafiam tylko na pierwszą stronę. Mój problem polega na tym, że moje dane wejściowe najwyraźniej nie są UTF-8, a foo z kodowaniem 1.9 szaleje
Marc Seeger
@Marc Seeger: Co rozumiesz przez „mój wkład”? Stdin, adres URL czy treść strony?
Adrian
HTML można zakodować w UTF-8: en.wikipedia.org/wiki/Character_encodings_in_HTML
Eduardo
moje dane wejściowe = treść strony @Eduardo: Wiem. Mój problem polega na tym, że dane pochodzące z net / http wydają się od czasu do czasu źle kodować
Marc Seeger,
Nierzadko zdarza się, że strony internetowe mają w rzeczywistości złe kodowanie. Nagłówek odpowiedzi może informować, że jest to jedno kodowanie, ale w rzeczywistości obsługuje inne kodowanie.
sunkencity
-1

Jeśli nie „przejmujesz się” danymi, możesz po prostu zrobić coś takiego:

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

Po prostu mi to valid_encoding?zdawało. Moje jest polem wyszukiwania, więc w kółko znajdowałem tę samą dziwność, więc użyłem czegoś w stylu: po prostu nie psuj systemu. Ponieważ nie kontroluję doświadczenia użytkownika, aby przeprowadzić automatyczną weryfikację przed wysłaniem tych informacji (np. Automatyczna informacja zwrotna z napisem „dummy up!”), Mogę po prostu wziąć to, usunąć i zwrócić puste wyniki.

pjammer
źródło