SQL — проблема с кодировкой UTF-8 в varchar/nvarchar

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

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

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

Обратите внимание на забавных персонажей во время конвертации.

eg. On behalf of IRB Holding Corp (the “Company")

Следующий запрос исправляет указанную выше проблему: я вижу Цитаты в том виде, в котором они должны отображаться, но затем происходит сбой в строках, содержащих '&' , который является специальным символом в 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)')))

Следующий запрос обрабатывает вышеуказанную проблему с помощью операторов replace, и я могу полностью увидеть все строки, как и ожидалось. Но это решение будет обрабатывать только '&' s.

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

    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

Вопрос — Должен ли я добавлять дополнительные операторы замены для других специальных символов xml — < , >?

@GSerg, это отличный обходной путь, если производительность не имеет большого значения. Спасибо за ссылку! Я попробовал функцию с заданной строкой и получил DALLAS, TX – May 7, 2019 – Covey & Park Energy Holdings LLC (“Covey Park” , что выглядит довольно убедительно.

Shnugo 17.05.2019 10:22

@Shnugo Ни один из обходных путей (а они все есть) не будут эффективными. Самым быстрым из обходных путей должен является передача varbinary(max) функции CLR и вызов Utf8.GetString для нее. Самый быстрый в принципе должен быть cast(cast(varbinary as varchar(max)) collate LATIN1_GENERAL_100_CI_AS_SC_UTF8 as nvarchar(max)) в SQL Server 2019.

GSerg 17.05.2019 10:32

@GSerg Верно ... Но самым быстрым было узнать все это и, следовательно, полностью избежать UTF-8 в хранилище базы данных. Я только что добавил несколько строк в свой ответ, чтобы отразить это.

Shnugo 17.05.2019 10:34

GSerg и Snugo, спасибо за объяснение. Тоже узнал кое-что новое. Добавление блока cdata преобразовало все записи в мою таблицу правильно - пока подводных камней xml нет на моем пути, посмотрим, как пойдет, когда я продолжу извлекать больше данных, да, мне нужно прочитать о том, как работает cdata.

J Sidhu 17.05.2019 18:02
Как сделать HTTP-запрос в Javascript?
Как сделать HTTP-запрос в Javascript?
В JavaScript вы можете сделать HTTP-запрос, используя объект XMLHttpRequest или более новый API fetch. Вот пример для обоих методов:
3
5
9 672
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

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

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-байтовая кодовая точка для в тире. SQL-сервер вам в этом не поможет... Он интерпретирует это в 3 символа по 1 байту...

Боюсь, вам не повезло...

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

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

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

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

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

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

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

  • Ждите v2019. Будет специальные сопоставления, обеспечивающий встроенную поддержку utf-8 в строках T-SQL.
  • Этот ответ предоставляет UDF, который может преобразовывать UTF8 в NVARCHAR. Это будет не быстро, но работает.

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

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

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

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

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

4. Дождитесь SQL Server 2019, чем поддерживает UTF-8 в varchars.

GSerg 17.05.2019 09:51

@GSerg Да, но для этой функции вам придется использовать специальные сопоставления. Недостаток... И он будет очень медленным при любом фильтре или поисковом действии. Лучше всего будет: Старайтесь избегать utf8 в базах данных...

Shnugo 17.05.2019 10:01

Я не очень доволен вашим последним редактированием. substring работает с символами, а не с байтами, и это не изменится с UTF8. UTF16, которая поддерживается уже давно, также является кодировкой переменной длины (от 2 до 4 байтов на символ), и это тоже не вызывает проблем.

GSerg 17.05.2019 10:39

@GSerg Я почти уверен, что под капотом substring читается из массива. Если это простые данные, это будет молниеносно работать со смещениями памяти (вероятно, арифметика указателя). Но с UTF16 или UTF8 движку придется преобразовать это либо в NVARCHAR с некоторым модные дополнения, либо в какую-то коллекцию символов в памяти (возможно, связанный список). Знаете ли вы подробности о фактической реализации substring()? Было бы интересно...

Shnugo 17.05.2019 10:48

Вы, вероятно, правы в этом. С declare @s nvarchar(20) = N'?', который принимает две единицы кода UTF-16 (четыре байта), substring(@s, 1, 1) возвращает первый суррогат, а не весь символ. Но это поведение, которое было у него целую вечность, поэтому с UTF-8 оно не станет хуже.

GSerg 17.05.2019 10:55

@GSert, нет, хуже не станет, но вернется с неожиданными результатами. Или это станет медленным, если следующий SQL-сервер изменит это поведение из-за сортировки.

Shnugo 17.05.2019 12:04
Ответ принят как подходящий

Трюк с 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 parsing: line 1, character 54, illegal xml character

Это здорово, +1 с моей стороны. Я добавил раздел обновления в свой ответ, где я использую этот подход. Говорить людям годами, что SQL-Server не может читать utf-8, кажется неправильным :-)

Shnugo 17.05.2019 11:58

@Shnugo Это все еще взлом. Есть символы не представляется в XML даже в разделах CDATA. Этот код завершится ошибкой, если строка содержит, например, char(8) или char(11).

GSerg 17.05.2019 12:10

Да, я знаю, что остались определенные символы... Но я думаю, что 99,9% всех строк utf-8 будут работать. Кстати: Я когда-то разместил ответ, как вы можете включить эти недопустимые символы в XML... Но это довольно академично...

Shnugo 17.05.2019 12:15

@Shnugo Очевидно, это также портит пробелы (сжимает несколько пробелов в один, заменяет CrLf на Lf и т. д.). Применение xml:space = "preserve" к root, похоже, не решает проблему, поскольку проблема, как мне кажется, возникает во внутреннем CAST('' as xml).value(...).

GSerg 28.10.2020 12:21

вам нужно заменить строку ]]> на ]]]]><![CDATA[>, иначе это не будет работать универсально

andowero 23.05.2021 13:02

Если у вас есть SQL Server 2019, вы можете создать другую базу данных с UTF8 в качестве сопоставления по умолчанию и создать там простую функцию:

USE UTF8_DATABASE
GO

CREATE OR ALTER FUNCTION dbo.VarBinaryToUTF8
  (@UTF8 VARBINARY(MAX))
  RETURNS VARCHAR(MAX)
AS
BEGIN
  RETURN CAST(@UTF8 AS VARCHAR(MAX));
END;

Вы бы позвонили

SELECT
  UTF8_DATABASE.dbo.VarBinaryToUTF8
  (
    CAST('' as xml).value('xs:base64Binary(sql:column("body"))','varbinary(max)')
  )
FROM
  @t

Это работает, потому что SQL-сервер использует сопоставление по умолчанию конкретной базы данных для своих переменных и возвращаемых значений функций. Вы должны сохранить результат в NVARCHAR или UTF8 сопоставленной базе данных «VARCHAR in your non-UTF8».

Другие вопросы по теме