ruby 1.9: urutan byte tidak valid di UTF-8

109

Saya menulis crawler di Ruby (1.9) yang mengkonsumsi banyak HTML dari banyak situs acak.
Ketika mencoba mengekstrak tautan, saya memutuskan untuk menggunakan saja .scan(/href="(.*?)"/i)daripada nokogiri / hpricot (percepatan besar). Masalahnya adalah sekarang saya menerima banyak " invalid byte sequence in UTF-8" kesalahan.
Dari apa yang saya pahami, net/httpperpustakaan tidak memiliki opsi khusus pengkodean dan hal-hal yang masuk pada dasarnya tidak diberi tag dengan benar.
Apa cara terbaik untuk benar-benar bekerja dengan data yang masuk itu? Saya mencoba .encodedengan set opsi ganti dan tidak valid, tetapi sejauh ini tidak berhasil ...

Marc Seeger
sumber
sesuatu yang mungkin merusak karakter, tetapi menjaga string tetap valid untuk perpustakaan lain: valid_string = untrusted_string.unpack ('C *'). pack ('U *')
Marc Seeger
Memiliki masalah yang sebenarnya, coba solusi lain yang sama. Tidak ada cinta. Mencoba Marc, tetapi tampaknya mengacaukan segalanya. Apakah Anda yakin 'U*'membatalkan 'C*'?
Jordan Feldstein
Tidak, tidak :) Saya hanya menggunakannya di webcrawler di mana saya peduli dengan perpustakaan pihak ketiga agar tidak mogok lebih dari yang saya lakukan tentang kalimat di sana-sini.
Marc Seeger

Jawaban:

172

Di Ruby 1.9.3, Anda dapat menggunakan String.encode untuk "mengabaikan" urutan UTF-8 yang tidak valid. Berikut ini cuplikan yang akan berfungsi baik di 1.8 ( iconv ) dan 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

atau jika Anda memiliki masukan yang sangat merepotkan, Anda dapat melakukan konversi ganda dari UTF-8 ke UTF-16 dan kembali ke 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
sumber
3
Dengan beberapa masukan bermasalah, saya juga menggunakan konversi ganda dari UTF-8 ke UTF-16 dan kemudian kembali ke UTF-8 file_contents.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '') file_contents.encode!('UTF-8', 'UTF-16')
RubenLaguna
7
Ada juga opsi force_encoding. Jika Anda telah membaca ISO8859-1 sebagai UTF-8 (dan karena itu string tersebut berisi UTF-8 yang tidak valid) maka Anda dapat "menafsirkan ulang" sebagai ISO8859-1 dengan the_string.force_encoding ("ISO8859-1") dan langsung bekerja dengan string itu dalam pengkodean aslinya.
RubenLaguna
3
Trik penyandian ganda itu baru saja menyelamatkan Bacon saya! Saya bertanya-tanya mengapa itu diperlukan?
johnf
1
Di mana saya harus meletakkan garis-garis itu?
Lefsler
5
Saya pikir konversi ganda berfungsi karena memaksa konversi pengkodean (dan dengan itu memeriksa karakter yang tidak valid). Jika string sumber sudah dienkode dalam UTF-8, maka pemanggilan saja .encode('UTF-8')sudah tidak ada, dan tidak ada pemeriksaan yang dijalankan. Ruby Core Documentation untuk encode . Namun, mengonversinya ke UTF-16 terlebih dahulu memaksa semua pemeriksaan untuk urutan byte yang tidak valid dijalankan, dan penggantian dilakukan sesuai kebutuhan.
Jo Hund
79

Jawaban yang diterima atau jawaban lainnya cocok untuk saya. Saya menemukan posting ini yang menyarankan

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

Ini memperbaiki masalah saya.

Amir Raminfar
sumber
1
Ini memperbaiki masalah bagi saya dan saya suka menggunakan metode yang tidak digunakan lagi (saya memiliki Ruby 2.0 sekarang).
La-comadreja
1
Yang ini satu-satunya yang berhasil! Saya telah mencoba semua solusi di atas, tidak ada yang berfungsi String yang digunakan dalam pengujian "fdsfdsf dfsf sfds fs sdf <div> halo <p> fooo ??? {! @ # $% ^ & * () _ +} < / p> </div> \ xEF \ xBF \ xBD \ xef \ xbf \ x9c <div> \ xc2 \ x90 </div> \ xc2 \ x90 "
Chihung Yu
1
Untuk apa argumen kedua 'biner'?
Henley Chiu
24

Solusi saya saat ini adalah menjalankan:

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

Ini setidaknya akan menghilangkan pengecualian yang merupakan masalah utama saya

Marc Seeger
sumber
3
Saya menggunakan metode ini dalam kombinasi valid_encoding?yang sepertinya mendeteksi ketika ada sesuatu yang salah. val.unpack('C*').pack('U*') if !val.valid_encoding?.
Aaron Gibraltar
Yang ini berhasil untuk saya. Berhasil mengubah \xB0simbol punggung saya ke derajat. Bahkan valid_encoding?datang kembali benar tapi aku masih memeriksa apakah itu tidak dan menghapus karakter menyinggung menggunakan jawaban Amir di atas: string.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: ''). Saya juga telah mencoba force_encodingrute tersebut tetapi gagal.
hamstar
Ini bagus. Terima kasih.
d_ethier
8

Coba ini:

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
sumber
Jawaban terbaik untuk kasus saya! Terima kasih
Aldo
4

Saya menyarankan Anda untuk menggunakan parser HTML. Temukan saja yang tercepat.

Parsing HTML tidak semudah kelihatannya.

Browser mengurai urutan UTF-8 yang tidak valid, dalam dokumen HTML UTF-8, cukup dengan meletakkan simbol " ". Jadi, setelah urutan UTF-8 yang tidak valid di HTML diurai, teks yang dihasilkan adalah string yang valid.

Bahkan di dalam nilai atribut Anda harus mendekode entitas HTML seperti amp

Berikut adalah pertanyaan bagus yang merangkum mengapa Anda tidak dapat mengurai HTML dengan ekspresi reguler secara andal: RegEx mencocokkan tag terbuka kecuali tag mandiri XHTML

Eduardo
sumber
2
Saya ingin menyimpan regexp karena ini 10 kali lebih cepat dan saya benar-benar tidak ingin mengurai html dengan benar tetapi hanya ingin mengekstrak tautan. Saya seharusnya dapat mengganti bagian yang tidak valid di ruby ​​hanya dengan melakukan: ok_string = bad_string.encode ("UTF-8", {: invalid =>: replace,: undef =>: replace}) tetapi sepertinya tidak kerja :(
Marc Seeger
3

Ini sepertinya berhasil:

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
sumber
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
sumber
2

Saya menemukan string, yang memiliki campuran bahasa Inggris, Rusia, dan beberapa abjad lainnya, yang menyebabkan pengecualian. Saya hanya perlu bahasa Rusia dan Inggris, dan saat ini ini berfungsi untuk saya:

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
sumber
1

Sementara solusi Nakilon berfungsi, setidaknya sejauh melewati kesalahan, dalam kasus saya, saya memiliki karakter aneh yang berasal dari Microsoft Excel yang dikonversi ke CSV yang mendaftar di ruby ​​sebagai (dapatkan ini) cyrillic K yang di ruby adalah K. yang tebal. Untuk memperbaikinya, saya menggunakan 'iso-8859-1' yaitu. CSV.parse(f, :encoding => "iso-8859-1"), yang mengubah Cyrillic K saya yang aneh menjadi jauh lebih mudah diatur /\xCA/, yang kemudian bisa saya hapus denganstring.gsub!(/\xCA/, '')

boulder_ruby
sumber
Sekali lagi, saya hanya ingin mencatat bahwa sementara perbaikan Nakilon (dan lainnya) adalah untuk karakter Cyrillic yang berasal dari (haha) Cyrillia, keluaran ini adalah keluaran standar untuk csv yang diubah dari xls!
boulder_ruby
0

Sebelum Anda menggunakan scan, pastikan bahwa Content-Typeheader halaman yang diminta adalah text/html, karena mungkin ada link ke hal-hal seperti gambar yang tidak dikodekan dalam UTF-8. Halaman tersebut juga bisa non-html jika Anda mengambil hrefsesuatu seperti <link>elemen. Cara memeriksanya bervariasi pada pustaka HTTP apa yang Anda gunakan. Kemudian, pastikan hasilnya hanya ascii dengan String#ascii_only?(bukan UTF-8 karena HTML seharusnya hanya menggunakan ascii, entitas dapat digunakan sebaliknya). Jika kedua tes tersebut lulus, maka aman digunakan scan.

Adrian
sumber
terima kasih, tapi itu bukan masalah saya :) Saya hanya mengekstrak bagian host dari URL dan hanya menekan halaman depan. Masalah saya adalah bahwa masukan saya tampaknya bukan UTF-8 dan encoding 1.9 foo rusak
Marc Seeger
@Marc Seeger: Apa yang Anda maksud dengan "masukan saya"? Stdin, URL, atau badan halaman?
Adrian
HTML dapat dikodekan dalam UTF-8: en.wikipedia.org/wiki/Character_encodings_in_HTML
Eduardo
masukan saya = badan halaman @Eduardo: Saya tahu. Masalah saya adalah bahwa data yang berasal dari net / http tampaknya memiliki pengkodean yang buruk dari waktu ke waktu
Marc Seeger
Tidak jarang laman web benar-benar memiliki pengkodean yang buruk. Header respons mungkin mengatakan itu satu pengkodean tetapi kemudian benar-benar melayani pengkodean lain.
sunkencity
-1

Jika Anda tidak "peduli" dengan data, Anda dapat melakukan sesuatu seperti:

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

Saya hanya biasa valid_encoding?melewatinya. Milik saya adalah bidang pencarian, jadi saya menemukan keanehan yang sama berulang kali jadi saya menggunakan sesuatu seperti: hanya agar sistem tidak rusak. Karena saya tidak mengontrol pengalaman pengguna untuk melakukan validasi otomatis sebelum mengirim info ini (seperti umpan balik otomatis untuk mengatakan "dummy up!") Saya hanya dapat menerimanya, menghapusnya, dan mengembalikan hasil kosong.

pjammer.dll
sumber