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 语句处理上述问题,我能够按预期完全查看所有行。但是这个解决方案只会处理'&amp;' 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 特殊字符添加更多替换语句 - &lt; , &gt;

【问题讨论】:

Convert text value in SQL Server from UTF8 to ISO 8859-1的可能重复 @GSerg,如果性能不那么重要,这是一个很好的解决方法。谢谢链接!我用给定的字符串尝试了函数并返回了DALLAS, TX – May 7, 2019 – Covey &amp; 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 &lt;![CDATA]]&gt; 块。

请注意,它仍然只是一个 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(...) 您需要将]]&gt;字符串替换为]]]]&gt;&lt;![CDATA[&gt;,否则这将无法通用【参考方案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   â € “ 

0x44D0x4C 的两倍是 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 latinone-byte-codes。但是任何高于 127 的 ASCII 码都会导致麻烦(可能使用正确的排序规则)。但是 - 这是你的情况 - 你的字符串使用 multi-byte-code-pointsUTF-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 服务器对其变量和函数返回值使用特定数据库的默认排序规则。您必须将结果存储到 NVARCHARUTF8 整理的 'VARCHAR in your non-UTF8` 数据库中。

【讨论】:

以上是关于SQL - UTF-8 到 varchar/nvarchar 编码问题的主要内容,如果未能解决你的问题,请参考以下文章

SQL 链接服务器到 UTF-8 数据库

如何在SQL语句中使用UTF-8?

将数据批量加载到使用 BCP 从 SQL Server 导出的 Snowflake 时出现 UTF-8 错误

UTF-8 字符在 HTTP 响应 pl/sql 中损坏

如何在 SQL Server 数据库中使用 UTF-8 排序规则?

是否可以让 SQL Server 将排序规则转换为 UTF-8 / UTF-16