Подтвердить что ты не робот

Как я могу глобально игнорировать неверные последовательности байтов в строках UTF-8?

У меня есть приложение Rails, оставшееся от миграции, поскольку Rails версии 1 и я хотел бы игнорировать неработающие байтовые последовательности all, чтобы сохранить обратную совместимость.

Я не могу знать кодировку ввода.

Пример:

> "- Men\xFC -".split("n")
ArgumentError: invalid byte sequence in UTF-8
    from (irb):4:in `split'
    from (irb):4
    from /home/fotanus/.rvm/rubies/ruby-2.0.0-rc2/bin/irb:16:in `<main>'

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

> "- Men\xFC -".unpack("C*").pack("U*").split("n")
 => ["- Me", "ü -"] 

Однако я хотел бы всегда игнорировать недопустимые байтовые последовательности и отключать эти ошибки. На самом Ruby или Rails.

4b9b3361

Ответ 1

Я не думаю, что вы можете полностью отключить проверку UTF-8 без особых трудностей. Вместо этого я хотел бы сосредоточиться на исправлении всех строк, которые входят в ваше приложение, на границе, где они входят (например, когда вы запрашиваете базу данных или получаете HTTP-запросы).

Предположим, что входящие строки имеют BINARY (кодировка ASCII-8BIT a.k.a.). Это можно моделировать следующим образом:

s = "Men\xFC".force_encoding('BINARY')  # => "Men\xFC"

Затем мы можем преобразовать их в UTF-8 с помощью String # encode и заменить любые символы undefined символом замены UTF-8:

s = s.encode("UTF-8", invalid: :replace, undef: :replace)  # => "Men\uFFFD"
s.valid_encoding?  # => true

К сожалению, описанные выше шаги приведут к искажению множества кодовых точек UTF-8, потому что байты в них не будут распознаны. Если у вас есть трехбайтовые символы UTF-8, такие как "\ uFFFD", это будет интерпретироваться как три отдельных байта, и каждый из них будет преобразован в заменяющий символ. Возможно, вы могли бы сделать что-то вроде этого:

def to_utf8(str)
  str = str.force_encoding("UTF-8")
  return str if str.valid_encoding?
  str = str.force_encoding("BINARY")
  str.encode("UTF-8", invalid: :replace, undef: :replace)
end

Это лучшее, что я мог придумать. К сожалению, я не знаю отличный способ сказать Ruby рассматривать строку как UTF-8 и просто заменить все недопустимые байты.

Ответ 2

В ruby ​​2.0 вы можете использовать метод String # b, который является коротким псевдонимом для String # force_encoding ( "BINARY" )

Ответ 3

Если вы просто хотите работать с необработанными байтами, вы можете попробовать кодировать его как ASCII-8BIT/BINARY.

str.force_encoding("BINARY").split("n")

Это не приведет к возврату вашего ü, так как ваша исходная строка в этом случае - ISO-8859-1 (или что-то вроде этого):

"- Men\xFC -".force_encoding("ISO-8859-1").encode("UTF-8")
 => "- Menü -"

Если вы хотите получить многобайтовые символы, вам нужно знать, что такое кодировка источника. Как только вы force_encoding перейдете к BINARY, вы буквально просто получите необработанные байты, поэтому многобайтовые символы не будут интерпретироваться соответственно.

Если данные взяты из вашей базы данных, вы можете изменить свой механизм соединения для использования кодировки ASCII-8BIT или BINARY; Таким образом, Ruby должен соответствующим образом обозначить их. В качестве альтернативы вы можете monkeypatch драйвер базы данных принудительно кодировать все строки, прочитанные из него. Это массивный молот, хотя и может быть абсолютно неправильным.

Правильный ответ - исправить ваши строковые кодировки. Это может потребовать исправления базы данных, исправления кодирования соединения с драйвером базы данных или их комбинации. Все байты все еще существуют, но если вы имеете дело с данной кодировкой, вы должны, если это вообще возможно, позволить Ruby знать, что вы ожидаете, что ваши данные будут в этой кодировке. Общей ошибкой является использование драйвера mysql2 для подключения к базе данных MySQL, которая имеет данные в кодировках latin1, но для указания кодировки utf-8 для соединения. Это приводит к тому, что Rails принимает данные latin1 из БД и интерпретирует его как utf-8, вместо того, чтобы интерпретировать его как latin1, который затем можно преобразовать в UTF-8.

Если вы можете уточнить, откуда берутся строки, может возникнуть более полный ответ. Вы также можете проверить этот ответ для возможного глобального (-ish) решения Rails для строковых кодировок по умолчанию.

Ответ 4

Если вы можете настроить свою базу данных/страницу/все, чтобы передать вам строки в ASCII-8BIT, это даст вам реальную кодировку.

Использовать библиотеку угадывания кодировки Ruby stdlib. Передайте все свои строки через что-то вроде этого:

require 'nkf'
str = "- Men\xFC -"
str.force_encoding(NKF.guess(str))

Библиотека NKF угадывает кодировку (обычно успешно) и принудительно кодирует эту строку. Если вам не хочется полностью доверять библиотеке NKF, также создайте эту защиту вокруг операций с строкой:

begin
  str.split
rescue ArgumentError
  str.force_encoding('BINARY')
  retry
end

Это будет отказ от BINARY, если NKF не догадался правильно. Вы можете превратить это в оболочку метода:

def str_op(s)
  begin
    yield s
  rescue ArgumentError
    s.force_encoding('BINARY')
    retry
  end
end

Ответ 5

Кодирование в Ruby 1.9 и 2.0 кажется немного сложным. \xFC - это код для специального символа ü в ISO-8859-1, но код FC также встречается в UTF-8 для ü U+00FC = \u0252 (и в UTF-16). Это может быть артефакт функции Ruby pack/unpack. Упаковка и распаковка символов Юникода с строкой шаблона U * для Unicode не проблематичны:

>> "- Menü -".unpack('U*').pack("U*")
=> "- Menü -"

Вы можете создать "неправильную" строку, то есть строку с неверной кодировкой, если вы сначала распакуете символы Unicode UTF-8 (U), а затем упакуете неподписанные символы (C):

>> "- Menü -".unpack('U*').pack("C*")
=> "- Men\xFC -"

Эта строка больше не имеет действительной кодировки. По-видимому, процесс преобразования можно отменить, применяя противоположный порядок (немного похожий на операторов в квантовой физике):

>> "- Menü -".unpack('U*').pack("C*").unpack("C*").pack("U*")
=> "- Menü -"

В этом случае также можно "исправить" сломанную строку, сначала преобразуя ее в ISO-8859-1, а затем в UTF-8, но я не уверен, что это работает случайно, потому что код содержится в этот набор символов

>> "- Men\xFC -".force_encoding("ISO-8859-1").encode("UTF-8")
=> "- Menü -"
>> "- Men\xFC -".encode("UTF-8", 'ISO-8859-1')
=> "- Menü -"