Как я могу глобально игнорировать неверные последовательности байтов в строках 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.
Ответы
Ответ 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ü -"