SQL - UTF-8 到 varchar/nvarchar 编码问题
Posted
技术标签:
【中文标题】SQL - UTF-8 到 varchar/nvarchar 编码问题【英文标题】:SQL - UTF-8 to varchar/nvarchar Encoding issue 【发布时间】:2019-05-16 22:58:23 【问题描述】:背景 - 我正在接收来自网站的响应数据,格式为 UTF-8 编码的 json 格式。 json 的 body 属性具有 base64binary 类型的值,我将其作为 nvarchar 类型存储在 ms sql server 上。
当我将该 base64binary 数据转换为 varchar 或 nvarchar 时,我看到有趣的字符(代替双引号)表明存在编码问题 - 这就是我问这个问题的原因。
请参阅下面的剖析代码和底部的可运行示例以及我的担忧。
在转换过程中注意有趣的字符。
例如。代表 IRB Holding Corp(——公司“)
以下查询修复了上述问题 - 我看到 quotes 应该出现,但是它在包含 '&' 的行上失败,这是 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 特殊字符添加更多替换语句 - < , >
?
【问题讨论】:
Convert text value in SQL Server from UTF8 to ISO 8859-1的可能重复 @GSerg,如果性能不那么重要,这是一个很好的解决方法。谢谢链接!我用给定的字符串尝试了函数并返回了DALLAS, TX – May 7, 2019 – Covey & Park Energy Holdings LLC (“Covey Park”
,这看起来很有说服力。
@Shnugo 没有一种解决方法(它们都是)将是高效的。最快的解决方法应该将varbinary(max)
传递给CLR 函数并在其上调用Utf8.GetString
。原则上最快的应该是 SQL Server 2019 中的cast(cast(varbinary as varchar(max)) collate LATIN1_GENERAL_100_CI_AS_SC_UTF8 as nvarchar(max))
。
@GSerg True... 但最快的是,知道所有这些,因此完全避免在数据库存储中使用 UTF-8。我刚刚在我的答案中添加了几行来反映这一点。
GSerg 和 Snugo,感谢您的解释。也学到了一些新东西。添加 cdata 块正确转换了我的表中的所有记录 - 现在 xml 的陷阱不在我的路径中,当我继续提取更多数据时会看到它是如何进行的,是的,我需要阅读 cdata 的工作原理。跨度>
【参考方案1】:
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 声明元素中的字符与 latin1 中的字符具有相同的 UTF-8 表示,因此将它们转换为 varbinary
会得到有效的 UTF-8
<![CDATA]]>
块。
请注意,它仍然只是一个 hack。一旦涉及 XML,您就会受到 XML 限制,如果您的字符串包含characters not representable in XML,那么这种类型的 XML 转换将会失败
XML 解析:第 1 行,字符 54,非法 xml 字符
【讨论】:
这太棒了,我这边+1。我在我使用这种方法的答案中添加了一个更新部分。多年来一直告诉人们,SQL-Server 无法读取 utf-8 似乎是错误的 :-) @Shnugo 它仍然是一个 hack。有 not representable in XML 的字符,即使在 CDATA 部分中也是如此。如果字符串包含例如,此代码将失败。char(8)
或 char(11)
.
是的,我知道还有某些字符……但我认为,所有 utf-8 字符串的 99.9% 都可以使用。顺便说一句:I once placed an answer 如何将这些无效字符包含到 XML 中……但这太学术了……
@Shnugo 显然这也与空格混淆(将多个空格压缩为一个,将 CrLf 替换为 Lf 等)。将xml:space="preserve"
应用于root
似乎并不能解决它,因为我认为问题发生在内部CAST('' as xml).value(...)
。
您需要将]]>
字符串替换为]]]]><![CDATA[>
,否则这将无法通用【参考方案2】:
更新:我刚刚学到了一些新东西,那就是 - 嗯 - 很棒 :-)
试试这个功能
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-byte encoded code point for the en dash。 SQL-Server 不会帮你解决这个问题...它会将其解释为 3 个字符,每个字符 1 个字节...
恐怕你倒霉了……
SQL-Server 不支持utf-8
字符串。 BCP / BULK
对启用来自文件系统的输入的支持有限,但字符串 within T-SQL
必须是两个受支持的选项之一:
(var)char
,这是扩展的 ASCII。它严格每个字符一个字节,并且需要一个排序规则来处理一组有限的外来字符。
n(var)char
,即 UCS-2(非常类似于 UTF-16
)。它严格每个字符两个字节,并且会以双倍内存大小为代价对(几乎)任何已知字符进行编码。
UTF-8
与 (var)char
兼容,只要我们坚持 plain latin 和 one-byte-codes。但是任何高于 127 的 ASCII 码都会导致麻烦(可能使用正确的排序规则)。但是 - 这是你的情况 - 你的字符串使用 multi-byte-code-points。 UTF-8
将使用两个甚至更多字节(最多 4 个!)为单个字符编码大量字符。
你能做什么
您将不得不使用一些能够处理 UTF-8 的引擎
一个 CLR 函数 使用有限支持导出到文件并重新导入(需要 v2014 SP2 或更高版本) 使用外部工具(PowerShell、C#、任何您知道的编程语言)还有 - 感谢@GSerg - 还有两个选项:
等待 v2019。将有 special collations 允许在 T-SQL 字符串中原生支持utf-8
This answer 提供了一个 UDF,可以将 UTF8 转换为 NVARCHAR。它不会很快,但很有效。
一般说明
数据库可以按原样保存存储数据或工作数据,您希望以一种或另一种方式使用。将图片存储为VARBINARY(MAX)
只是一小部分。您不会尝试使用 SQL-Server 来执行图像识别。
这与文本数据相同。如果您只存储一大块文本,那么您如何执行此操作并不重要。但是如果你想使用这个文本进行过滤、搜索或者如果你想使用 SQL-Server 来显示这个文本,你必须考虑格式和对性能的需求。
具有可变字节长度的编码将不允许简单的SUBSTRING('blahblah',2,3)
。使用固定长度,引擎可以将字符串作为数组,跳转到第二个索引并选择接下来的三个字符。但是对于可变字节,如果可能存在任何多字节代码点,引擎必须通过检查所有字符来计算索引。这会极大地减慢很多字符串方法的速度......
最好不要以某种格式存储数据,SQL-Server 无法处理(好吧)...
【讨论】:
4.等待 SQL Server 2019 而不是supports UTF-8 in varchars。 @GSerg 是的,但是您必须为此功能使用特殊的排序规则。相当的缺点......而且在任何过滤器或搜索操作中都会非常慢。最好的办法是:尽量避免在数据库中使用 utf8... 我对您的最新编辑不太满意。substring
以字符而不是字节工作,这不会随着 UTF8 而改变。 UTF16 已经支持了很长时间,它也是一种可变长度编码(每个字符 2 到 4 个字节),并且也没有造成问题。
@GSerg 我很确定,在后台,substring
正在从数组中读取数据。如果这是 plain data,它将快速处理内存偏移(可能是指针算法)。但是对于 UTF16 或 UTF8,引擎必须将其转换为 NVARCHAR
,并带有一些 fancy extras 或某种内存中的字符集合(可能是一个链表)。你知道substring()
的实际实现细节吗?会很有趣...
你可能是对的。对于采用两个 UTF-16 代码单元(四个字节)的 declare @s nvarchar(20) = N'?'
,substring(@s, 1, 1)
返回第一个代理项,而不是整个字符。但这是它多年来一直存在的行为,因此使用 UTF-8 不会变得更糟。【参考方案3】:
如果您有 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 - UTF-8 到 varchar/nvarchar 编码问题的主要内容,如果未能解决你的问题,请参考以下文章
将数据批量加载到使用 BCP 从 SQL Server 导出的 Snowflake 时出现 UTF-8 错误