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's, но вроде все подтасовывает. Вы уверены, что 'U*'отменяет действие 'C*'?
Jordan Feldstein
Нет, это не так :) Я просто использовал это в веб-краулере, где меня волнует, что сторонние библиотеки не дают сбоев, больше, чем предложения здесь и там.
Marc Seeger

Ответы:

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
RubenLaguna
источник
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 с the_string.force_encoding («ISO8859-1») и просто работать с этой строкой в ​​ее реальной кодировке.
RubenLaguna
3
Этот трюк с двойным кодированием только что спас моего Бэкона! Интересно, зачем это вообще нужно?
johnf
1
Где мне поставить эти строки?
Lefsler
5
Я думаю, что двойное преобразование работает, потому что оно вызывает преобразование кодировки (а вместе с ним и проверку на недопустимые символы). Если исходная строка уже закодирована в UTF-8, тогда просто вызов .encode('UTF-8')не выполняется, и никакие проверки не выполняются. Документация Ruby Core для кодирования . Однако преобразование его в UTF-16 сначала принудительно запускает все проверки на недопустимые последовательности байтов, а замены выполняются по мере необходимости.
Jo Hund
79

Принятый ответ или другой ответ у меня работают. Я нашел этот пост, в котором предлагалось

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

Это устранило проблему для меня.

Амир Раминфар
источник
1
Это устранило проблему для меня, и мне нравится использовать нерекомендуемые методы (сейчас у меня Ruby 2.0).
La-comadreja
1
Это единственный, который работает! Я пробовал все вышеперечисленные решения, ни одно из них не работает Строка, которая использовалась при тестировании "fdsfdsf dfsf sfds fs sdf <div> hello <p> fooo ??? {! @ # $% ^ & * () _ +} < / p> </div> \ xEF \ xBF \ xBD \ xef \ xbf \ x9c <div> \ xc2 \ x90 </div> \ xc2 \ x90 "
Чихунг Ю 07
1
Для чего нужен второй аргумент «двоичный»?
Хенли Чиу
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
Ранджиткумар Рави
источник
Лучший ответ на мой случай! Спасибо
Aldo
4

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

Разобрать HTML не так просто, как может показаться.

Браузеры анализируют недопустимые последовательности UTF-8 в HTML-документах UTF-8, просто помещая символ « ». Итак, как только недопустимая последовательность UTF-8 в HTML будет проанализирована, результирующий текст станет допустимой строкой.

Даже внутри значений атрибутов вам нужно декодировать объекты HTML, такие как amp

Вот отличный вопрос, который подводит итог, почему нельзя надежно анализировать HTML с помощью регулярного выражения: RegEx сопоставляет открытые теги, за исключением автономных тегов XHTML.

Эдуардо
источник
2
Я бы хотел сохранить регулярное выражение, так как оно примерно в 10 раз быстрее, и я действительно не хочу правильно разбирать html, а просто хочу извлекать ссылки. Я смогу заменить недопустимые части в рубине, просто выполнив: ok_string = bad_string.encode ("UTF-8", {: invalid =>: replace,: undef =>: replace}), но это не похоже работа :(
Marc Seeger
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
Spajus
источник
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
источник
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
Nakilon
источник
1

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

boulder_ruby
источник
Опять же, я просто хочу отметить, что, хотя исправление Накилона (и других) было для кириллических символов, происходящих из (ха-ха) Кириллии, этот вывод является стандартным для 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, а кодировка foo 1.9 выходит из строя
Марк Сигер
@Marc Seeger: Что вы имеете в виду под «моим вкладом»? Стандартный ввод, URL-адрес или тело страницы?
Адриан
HTML можно закодировать в UTF-8: en.wikipedia.org/wiki/Character_encodings_in_HTML
Эдуардо
мой ввод = тело страницы @ Эдуардо: Я знаю. Моя проблема в том, что данные, поступающие из net / http, время от времени, кажется, имеют неправильную кодировку
Марк Сигер,
Веб-страницы нередко имеют плохую кодировку. Заголовок ответа может сказать, что это одна кодировка, но на самом деле обслуживает другую кодировку.
sunkencity
-1

Если вас не «заботят» данные, вы можете просто сделать что-то вроде:

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

Я просто valid_encoding?проходил мимо. У меня поле поиска, и я снова и снова обнаруживал одну и ту же странность, поэтому использовал что-то вроде: просто чтобы система не сломалась. Поскольку я не контролирую пользовательский интерфейс для автоматической проверки перед отправкой этой информации (например, автоматическая обратная связь, чтобы сказать «пустышка!»), Я могу просто взять ее, вырезать и вернуть пустые результаты.

пижамер
источник