SQL-UTF-8到varchar / nvarchar编码问题

时间:2019-05-16 22:58:23

标签: json sql-server xml encoding utf-8

某些背景-我从UTF-8中的json网站接收到响应数据。 json的body属性具有base64binary类型的值,我将其存储为ms sql服务器上的nvarchar类型。

当我将base64binary数据转换为varchar或nvarchar时,我看到有趣的字符(代替双引号),表明存在编码问题,这就是此问题的原因。

下面,我在解释解剖的代码,在底部,您可以看到一个可运行的示例和相关问题。

转换不好,注意有趣的人物。

  

例如代表IRB Holding Corp(公司”)

下面的示例查询解决了上述问题。它将数据转换为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

3 个答案:

答案 0 :(得分:3)

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]]>块。

请注意,它仍然不过是黑客。涉及XML时,您将受到XML的限制,并且如果您的字符串包含characters not representable in XML,则该类型的XML转换将失败

  

XML解析:第1行,字符54,非法的xml字符

答案 1 :(得分:1)

更新:我刚刚学到了一些新东西,这是-嗯-很棒:-)

尝试此功能

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   â € “ 

0x44D,两倍的0x4C是两倍的LL,在空格0x20之后,我们到达E28093 。这是3-byte encoded code point for the en dash。 SQL-Server将无法为您提供帮助...它将解释为每个1个字节的3个字符...

恐怕你运气不好...

SQL-Server不支持utf-8字符串。 BCP / BULK仅支持从文件系统输入,但 之内的字符串T-SQL必须是两个受支持的选项之一:

  • (var)char,是扩展的ASCII 。它严格是每个字符一个字节,并且需要排序规则来处理有限的一组外国字符。
  • n(var)char,它是 UCS-2 (非常类似于UTF-16)。严格来说,它是每个字符两个字节,并且将以几乎两倍的内存大小来编码(几乎)任何已知字符。

UTF-8(var)char兼容,只要我们坚持使用 plain latin 一字节代码即可。但是,任何高于127的ASCII码都会导致麻烦(使用正确的排序规则可能会起作用)。但是-这是您的情况-您的字符串使用多字节代码点UTF-8将为一个字符编码两个或更多字节(最多4个!)的许多字符。

你能做什么

您将不得不使用一些能够处理UTF-8的引擎

  • CLR功能
  • 导出到文件并使用有限支持(需要v2014 SP2或更高版本)重新导入
  • 使用外部工具(PowerShell,C#,任何您知道的编程语言)

然后-thx到@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无法处理(很好)...

答案 2 :(得分:0)

如果你有 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 Server 对其变量和函数返回值使用特定数据库的默认排序规则。您必须将结果存储到 NVARCHARUTF8 整理的 'VARCHAR in your non-UTF8` 数据库中。