ruby 1.9: недійсна послідовність байтів у UTF-8


109

Я пишу сканер в Ruby (1.9), який споживає багато HTML з багатьох випадкових сайтів.
Намагаючись витягнути посилання, я вирішив просто використовувати .scan(/href="(.*?)"/i)замість nokogiri / hpricot (основна швидкість). Проблема полягає в тому, що зараз я отримую багато invalid byte sequence in UTF-8помилок.
З того, що я зрозумів, у net/httpбібліотеці немає специфічних параметрів кодування, і те, що входить, в основному не належним чином позначено.
Який був би найкращий спосіб реально працювати з цими вхідними даними? Я спробував .encodeвстановити заміни та недійсні параметри, але успіху поки що немає ...


щось, що може порушити символи, але зберігає рядок, дійсний для інших бібліотек: valid_string = untrusted_string.unpack ('C *'). pack ('U *')
Marc Seeger

Маючи точну проблему, спробували ті ж самі рішення. Немає любові. Спробував Марка, але, здається, все загрожує. Дійсно 'U*'скасувати 'C*'?
Йордан Фельдштейн

Ні, це не так :) Я просто використав це в веб-сканері, де я дбаю про те, щоб бібліотеки сторонніх організацій не врізалися більше, ніж я про речення тут і там.
Марк Зегер

Відповіді:


172

У Ruby 1.9.3 можна використовувати String.encode, щоб "ігнорувати" недійсні послідовності UTF-8. Ось фрагмент, який буде працювати як у 1.8 ( iconv ), так і 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

або якщо у вас справді клопітка інформація, ви можете зробити подвійне перетворення з UTF-8 в UTF-16 і назад до 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
З деяким проблемним вкладом я також використовую подвійне перетворення з UTF-8 в UTF-16, а потім назад до UTF-8 file_contents.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '') file_contents.encode!('UTF-8', 'UTF-16')
RubenLaguna

7
Також є варіант force_encoding. Якщо ви читаєте ISO8859-1 як UTF-8 (і, таким чином, ця рядок містить недійсний UTF-8), ви можете "повторно інтерпретувати" його як ISO8859-1 за допомогою theringring.force_encoding ("ISO8859-1") і просто працювати з цим рядком у реальному кодуванні.
RubenLaguna

3
Цей трюк з подвійним кодуванням просто врятував мого Бекона! Цікаво, чому це потрібно?
Johnf

1
Де я повинен розмістити ці рядки?
Лефслер

5
Я думаю, що подвійне перетворення працює, тому що воно примушує перетворення кодування (а з ним і перевірку на недійсні символи). Якщо рядок вихідного коду вже закодовано в UTF-8, то просто виклик .encode('UTF-8')не працює, і перевірки не виконуються. Документація Ruby Core для кодування . Однак перетворення його в UTF-16 спочатку змушує запускати всі перевірки на недійсні послідовності байтів, а заміни виконуються за потребою.
Джо Хунд

79

Прийнята відповідь та інша відповідь не працюють для мене. Я знайшов цю посаду, яка запропонувала

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

Це вирішило для мене проблему.


1
Це вирішило проблему для мене, і мені подобається використовувати непридатні методи (у мене зараз Ruby 2.0).
La-comadreja

1
Цей єдиний працює! Я випробував усе вищезазначене рішення, жодне з них не працює String, який використовується при тестуванні "fdsfdsf dfsf sfds fs sdf <div> привіт <p> fooo ??? {! @ # $% ^ & * () _ +} < / p> </div> \ xEF \ xBF \ xBD \ xef \ xbf \ x9c <div> \ xc2 \ x90 </div> \ xc2 \ x90 "
Chihung Yu

1
Для чого другий аргумент "бінарний"?
Henley Chiu

24

Моє поточне рішення: запустити:

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

Це хоча б позбудеться тих винятків, які були моєю основною проблемою


3
Я використовую цей метод у поєднанні, valid_encoding?який, здається, виявляє, коли щось не так. val.unpack('C*').pack('U*') if !val.valid_encoding?.
Аарон Гібралтер

Цей працював на мене. Успішно перетворює \xB0спину на символи градусів. Навіть valid_encoding?повертається правда , але я все ще перевірити , якщо він не робить і вичистити ображають символи , використовуючи відповідь Аміра вище: string.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: ''). Я також спробував force_encodingмаршрут, але це не вдалося.
hamstar

Це чудово. Дякую.
d_ethier

8

Спробуйте це:

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

Найкраща відповідь для моєї справи! Спасибі
Альдо

4

Я рекомендую вам використовувати парний HTML-аналізатор. Просто знайдіть найшвидший.

Розбір HTML не такий простий, як може здатися.

Браузери аналізують недійсні послідовності UTF-8 у документах HTML UTF-8, просто ставлячи символ " ". Отже, як тільки недійсна послідовність UTF-8 в HTML розбирається, отриманий текст є дійсним рядком.

Навіть всередині значень атрибутів ви повинні декодувати HTML-сутності, наприклад, підсилювачі

Ось чудове питання, яке підсумовує, чому ви не можете надійно розібрати HTML з регулярним виразом: RegEx відповідає відкритим тегам, за винятком автономних тегів XHTML


2
Я хотів би тримати regexp, оскільки це приблизно в 10 разів швидше, і я дійсно не хочу правильно аналізувати HTML, а просто хочу витягнути посилання. Я повинен мати можливість замінити недійсні частини в рубіні, просто зробивши: ok_string = bad_string.encode ("UTF-8", {: invalid =>: Замінити,: undef =>: Замінити}), але це не здається робота :(
Марк Зегер

3

Це, здається, працює:

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

Я стикався з рядками, в яких були змішання англійської, російської та деяких інших алфавітів, що спричинило виняток. Мені потрібні лише російська та англійська мови, і зараз це працює для мене:

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

Хоча рішення Nakilon працює, принаймні, що стосується подолання помилки, у моєму випадку у мене був цей дивний персонаж, що вийшов з файлу Microsoft Excel, перетворений у CSV, що реєструвався в рубіні як (отримати це) кирилицею K, яка в Рубін був напівжирним К. Для виправлення цього я використав "iso-8859-1" саме. CSV.parse(f, :encoding => "iso-8859-1"), що перетворило мої химерні глухі кирилиці K у набагато більш керовані /\xCA/, які я міг потім знятиstring.gsub!(/\xCA/, '')


Ще раз хочу зазначити, що в той час, як виправлення Накілона (та інших) було виправлено для символів кирилиці, що походять від (ха-ха) Кирилії, цей вихід є стандартним висновком для csv, який був перетворений з xls!
boulder_ruby

0

Перш ніж використовувати scan, переконайтеся, що Content-Typeзаголовок потрібної сторінки є text/html, оскільки там можуть бути посилання на речі, такі як зображення, які не закодовані в UTF-8. Сторінка також може бути не-html, якщо ви зібрали hrefщось на зразок <link>елемента. Як перевірити це, залежить від бібліотеки HTTP, яку ви використовуєте. Потім переконайтеся, що результатом є лише ascii з String#ascii_only?(а не UTF-8, оскільки HTML повинен використовуватись лише ascii, сутності можна використовувати інакше). Якщо вони пройдуть обидва ці тести, це безпечно для використання scan.


дякую, але це не моя проблема :) Я все одно витягую лише головну частину URL-адреси та натискаю лише на головну сторінку. Моя проблема полягає в тому, що мій внесок, мабуть, не є UTF-8, а кодування 1.9 кодується у сітці
Marc Seeger

@Marc Seeger: Що ви маєте на увазі під "моїм вкладом"? Stdin, URL-адреса чи тіло сторінки?
Адріан

HTML можна закодувати в UTF-8: en.wikipedia.org/wiki/Character_encodings_in_HTML
Eduardo

мій вклад = тіло сторінки @Eduardo: Я знаю. Моя проблема полягає в тому, що дані, що надходять із мережі net / http, час від часу мають погану кодування
Marc Seeger

Не рідкість веб-сторінок насправді має неправильне кодування. Заголовок відповіді може сказати, що це одне кодування, але потім фактично обслуговує інше кодування.
sunkencity

-1

Якщо вас не цікавить дані, ви можете зробити щось на кшталт:

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

Я просто звик valid_encoding?це пройти. Моє поле пошуку, і тому я знаходив однакові дивацтва знову і знову, тому я використовував щось на кшталт: просто щоб система не зламалася. Оскільки я не контролюю, щоб користувальницький досвід користувався автовалізацією перед надсиланням цієї інформації (як-от автоматичний зворотний зв'язок, щоб сказати "манекен!"), Я можу просто взяти її, зняти та повернути порожні результати.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.