Предыстория. Я получаю данные ответа с веб-сайта в формате 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)'))),'&','&'))
from @t
Вопрос — Должен ли я добавлять дополнительные операторы замены для других специальных символов xml — < , >?
@GSerg, это отличный обходной путь, если производительность не имеет большого значения. Спасибо за ссылку! Я попробовал функцию с заданной строкой и получил DALLAS, TX – May 7, 2019 – Covey & Park Energy Holdings LLC (“Covey Park” , что выглядит довольно убедительно.
@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 Верно ... Но самым быстрым было узнать все это и, следовательно, полностью избежать UTF-8 в хранилище базы данных. Я только что добавил несколько строк в свой ответ, чтобы отразить это.
GSerg и Snugo, спасибо за объяснение. Тоже узнал кое-что новое. Добавление блока cdata преобразовало все записи в мою таблицу правильно - пока подводных камней xml нет на моем пути, посмотрим, как пойдет, когда я продолжу извлекать больше данных, да, мне нужно прочитать о том, как работает cdata.

Попробуйте эту функцию
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.
И - спасибо @GSerg - еще два варианта:
utf-8 в строках T-SQL.База данных может содержать только данные хранения как есть или рабочие данные, которые вы хотите использовать тем или иным способом. Сохранение изображения как VARBINARY(MAX) — это всего лишь кусок битов. Вы бы не пытались использовать SQL-Server для распознавания изображений.
То же самое и с текстовыми данными. Если вы просто сохраните кусок текста, не имеет значения, как вы это сделаете. Но если вы хотите использовать этот текст для фильтрации, поиска или если вы хотите использовать SQL-Server для отображения этого текста, вы должны подумать о формате и требованиях к производительности.
Кодирование с переменной длиной байта не позволит использовать простой SUBSTRING('blahblah',2,3). При фиксированной длине движок может просто взять строку как массив, перейти ко второму индексу и выбрать следующие три символа. Но с переменными байтами механизм должен будет вычислить индекс, проверив все символы перед тем, может ли быть какая-либо многобайтовая кодовая точка. Это сильно замедлит работу многих строковых методов...
Лучше всего было не хранить данные в формате, который SQL-сервер не может обрабатывать (хорошо)...
4. Дождитесь SQL Server 2019, чем поддерживает UTF-8 в varchars.
@GSerg Да, но для этой функции вам придется использовать специальные сопоставления. Недостаток... И он будет очень медленным при любом фильтре или поисковом действии. Лучше всего будет: Старайтесь избегать utf8 в базах данных...
Я не очень доволен вашим последним редактированием. substring работает с символами, а не с байтами, и это не изменится с UTF8. UTF16, которая поддерживается уже давно, также является кодировкой переменной длины (от 2 до 4 байтов на символ), и это тоже не вызывает проблем.
@GSerg Я почти уверен, что под капотом substring читается из массива. Если это простые данные, это будет молниеносно работать со смещениями памяти (вероятно, арифметика указателя). Но с UTF16 или UTF8 движку придется преобразовать это либо в NVARCHAR с некоторым модные дополнения, либо в какую-то коллекцию символов в памяти (возможно, связанный список). Знаете ли вы подробности о фактической реализации substring()? Было бы интересно...
Вы, вероятно, правы в этом. С declare @s nvarchar(20) = N'?', который принимает две единицы кода UTF-16 (четыре байта), substring(@s, 1, 1) возвращает первый суррогат, а не весь символ. Но это поведение, которое было у него целую вечность, поэтому с UTF-8 оно не станет хуже.
@GSert, нет, хуже не станет, но вернется с неожиданными результатами. Или это станет медленным, если следующий SQL-сервер изменит это поведение из-за сортировки.
Трюк с 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"varbinary дает действительный UTF-8<![CDATA]]>.Обратите внимание, что это все еще не более чем взлом. Как только вы задействуете XML, на вас распространяются ограничения XML, и если ваша строка содержит символы, не представленные в XML, этот тип преобразования XML не удастся с
XML parsing: line 1, character 54, illegal xml character
Это здорово, +1 с моей стороны. Я добавил раздел обновления в свой ответ, где я использую этот подход. Говорить людям годами, что SQL-Server не может читать utf-8, кажется неправильным :-)
@Shnugo Это все еще взлом. Есть символы не представляется в XML даже в разделах CDATA. Этот код завершится ошибкой, если строка содержит, например, char(8) или char(11).
Да, я знаю, что остались определенные символы... Но я думаю, что 99,9% всех строк utf-8 будут работать. Кстати: Я когда-то разместил ответ, как вы можете включить эти недопустимые символы в XML... Но это довольно академично...
@Shnugo Очевидно, это также портит пробелы (сжимает несколько пробелов в один, заменяет CrLf на Lf и т. д.). Применение xml:space = "preserve" к root, похоже, не решает проблему, поскольку проблема, как мне кажется, возникает во внутреннем CAST('' as xml).value(...).
вам нужно заменить строку ]]> на ]]]]><![CDATA[>, иначе это не будет работать универсально
Если у вас есть 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».
Возможный дубликат Преобразование текстового значения в SQL Server из UTF8 в ISO 8859-1