SQL - проблема кодировки UTF-8 в varchar / nvarchar - PullRequest
1 голос
/ 17 мая 2019

Немного предыстории - я получаю ответные данные с сайта в json в UTF-8. Атрибут body json имеет значения в виде base64binary, которые я храню как тип nvarchar на сервере ms sql.

Когда я конвертирую эти base64binary данные в varchar или nvarchar, я вижу забавные символы (вместо двойных кавычек), указывающие на наличие проблемы кодирования, которая является причиной этого вопроса.

Ниже я объясняю рассеченный код, а внизу вы видите работающий пример и проблемы.

Плохое преобразование, обратите внимание на забавных персонажей.

например. От имени IRB Holding Corp (компания 1011 *)

Пример запроса ниже устраняет вышеуказанную проблему. Он преобразует данные в читаемый текст в формате xml, и я вижу кавычки в том виде, в котором они должны отображаться, но теперь он не работает в строках, содержащих «&», потому что это специальный символ в xml.

select    convert(xml,  '<?xml version="1.0" encoding="UTF-8"?>' + convert(varchar(max),cast('' as xml).value('xs:base64Binary(sql:column("body"))','varbinary(max)')))

Наконец, в приведенном ниже запросе я исправляю этот оператор замены, и я могу полностью увидеть все строки, как и ожидалось. Но это решение будет обрабатывать только «&». Боюсь, что код сломается, если в строках есть другие специальные символы в xml, такие как <,> и т. Д.

Вопрос: добавление дополнительных операторов замены - единственный выход из этой ситуации.

Пример кода для запуска:

    declare @t table ( [body] nvarchar(max) ) 

    insert into @t(body) 
    select 'REFMTEFTLCBUWCDigJMgTWF5IDcsIDIwMTkg4oCTIENvdmV5ICYgUGFyayBFbmVyZ3kgSG9sZGluZ3MgTExDICjigJxDb3ZleSBQYXJr4oCdIA=='

    select convert(varchar(max),cast('' as xml).value('xs:base64Binary(sql:column("body"))','varbinary(max)'))
        , convert(xml, '<?xml version="1.0" encoding="UTF-8"?>'+ replace(convert(varchar(max),convert(varchar(max),cast('' as xml).value('xs:base64Binary(sql:column("body"))','varbinary(max)'))),'&','&amp;')) 
from @t

Ответы [ 2 ]

3 голосов
/ 17 мая 2019

Трюк с XML работает отлично, просто позвольте механизму XML обрабатывать символьные объекты:

declare @t table ([body] nvarchar(max));

insert into @t(body) 
values ('REFMTEFTLCBUWCDigJMgTWF5IDcsIDIwMTkg4oCTIENvdmV5ICYgUGFyayBFbmVyZ3kgSG9sZGluZ3MgTExDICjigJxDb3ZleSBQYXJr4oCdIA==');

select
    cast(
        cast('<?xml version="1.0" encoding="UTF-8"?><root><![CDATA[' as varbinary(max))
        +
        CAST('' as xml).value('xs:base64Binary(sql:column("body"))', 'VARBINARY(MAX)')
        +
        cast(']]></root>' as varbinary(max))
    as xml).value('.', 'nvarchar(max)')
from
@t;

Важными частями здесь являются:

  • отсутствие из N перед строковыми литералами
  • encoding="UTF-8"
  • Тот факт, что мы знаем, что символы из элемента объявления XML имеют такое же представление UTF-8, как и в latin1, поэтому приведение их к varbinary дает действительный UTF-8
  • Блок <![CDATA]]>.

Обратите внимание, что это все еще не более, чем взломать. Как только вы задействуете XML, на вас распространяются ограничения XML, и если ваша строка содержит символов, которые не представлены в XML , преобразование этого типа XML завершится неудачно с

Синтаксический анализ XML: строка 1, символ 54, недопустимый символ xml

1 голос
/ 17 мая 2019

ОБНОВЛЕНИЕ: я только изучаю что-то новое, что - хм - здорово: -)

Попробуйте эту функцию

CREATE FUNCTION dbo.Convert_utf8(@utf8 VARBINARY(MAX))
RETURNS NVARCHAR(MAX)
AS
BEGIN
    DECLARE @rslt NVARCHAR(MAX);

    SELECT @rslt=
    CAST(
          --'<?xml version="1.0" encoding="UTF-8"?><![CDATA['
          0x3C3F786D6C2076657273696F6E3D22312E302220656E636F64696E673D225554462D38223F3E3C215B43444154415B
          --the content goes within CDATA
        + @utf8
        --']]>'
        + 0x5D5D3E
    AS XML).value('.', 'nvarchar(max)');

    RETURN @rslt;
END
GO

И назовите это так

SELECT *
      ,dbo.Convert_utf8(CAST(t.body AS XML).value('.','varbinary(max)'))
FROM @t t;

Результат

DALLAS, TX – May 7, 2019 – Covey & Park Energy Holdings LLC (“Covey Park” 

GSerg, большое спасибо! за ответ ниже. Я попытался и упростил это, чтобы работать в UDF.

Похоже, что приведение varbinary(max) к XML полностью выполнено в среде CLR, где учитывается объявление кодировки XML. Похоже, это работает и с другими кодировками, но у меня сейчас нет времени, чтобы проверить это в общем.

Теперь остальная часть ответа

Поскольку он содержит некоторые сведения о строковом кодировании, которые, возможно, стоит прочитать.

Я немного упростил ваш код:

declare @t table ( [body] nvarchar(max) ) 

insert into @t(body) 
select 'REFMTEFTLCBUWCDigJMgTWF5IDcsIDIwMTkg4oCTIENvdmV5ICYgUGFyayBFbmVyZ3kgSG9sZGluZ3MgTExDICjigJxDb3ZleSBQYXJr4oCdIA==';

SELECT  CAST(t.body AS XML).value('.','varbinary(max)')
       ,CAST(CAST(t.body AS XML).value('.','varbinary(max)') AS VARCHAR(MAX))
FROM @t t;

Вы увидите этот результат

0x44414C4C41532C20545820E28093204D617920372C203230313920E2809320436F7665792026205061726B20456E6572677920486F6C64696E6773204C4C432028E2809C436F766579205061726BE2809D20  
DALLAS, TX – May 7, 2019 – Covey & Park Energy Holdings LLC (“Covey Park†

Я сделаю первые символы более удобными для чтения

0x44414C4C41532C20545820E28093  
   D A L L A S ,   T X   â € “ 

0x44 - это D, вдвое больше 0x4C - удвоенное LL, и после пробела 0x20 мы получаем E28093. Это 3-байтовая кодированная точка для en dash . SQL-сервер не поможет вам в этом ... Он будет интерпретировать это как 3 символа по 1 байту каждый ...

Боюсь, тебе не повезло ...

SQL-сервер не поддерживает utf-8 строки. BCP / BULK имеет ограниченную поддержку для включения ввода из файловой системы, но строка в T-SQL должна быть одной из двух поддерживаемых опций:

  • (var)char, то есть расширенный ASCII . Он строго один байт на символ и будет нуждаться в сопоставлении, чтобы иметь дело с ограниченным набором иностранных символов.
  • n(var)char, то есть UCS-2 (очень похоже на UTF-16). Он строго два байта на символ и будет кодировать (почти) любой известный символ по цене удвоенного размера в памяти.

UTF-8 совместим с (var)char, если мы придерживаемся обычного латинского и однобайтовых кодов . Но любой код ASCII выше 127 приведет к проблемам (может работать с правильным сопоставлением). Но - это ваш случай здесь - ваша строка использует многобайтовых кодовых точек . UTF-8 будет кодировать множество символов двумя или более байтами (до 4!) Для одного символа.

Что вы можете сделать

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

  • CLR-функция
  • Экспорт в файл и повторный импорт с использованием ограниченной поддержки (требуется версия v2014 SP2 или выше)
  • Используйте внешний инструмент (PowerShell, C #, любой язык программирования, который вы знаете)

И - спасибо @GSerg - еще два варианта:

Общее замечание

База данных может хранить данные хранения просто , как , или рабочие данные, которые вы хотите использовать тем или иным способом. Хранение картинки как VARBINARY(MAX) - это просто кусок битов. Вы не пытаетесь использовать SQL-Server для распознавания изображений.

То же самое с текстовыми данными. Если вы просто храните кусок текста, не имеет значения, как вы это сделаете. Но если вы хотите использовать этот текст для фильтрации, поиска или если вы хотите использовать SQL-Server для отображения этого текста, вы должны подумать о формате и потребностях в производительности.

Кодирование с переменной длиной байта не допускает простого SUBSTRING('blahblah',2,3). С фиксированной длиной движок может просто взять строку в виде массива, перейти ко второму индексу и выбрать следующие три символа. Но с переменными байтами движок должен будет вычислить индекс, проверив все символы раньше, если может быть какая-либо многобайтовая кодовая точка. Это очень сильно замедлит многие строковые методы ...

Лучше всего было не хранить данные в формате, SQL-сервер не может обработать (хорошо) ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...