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

J S*_*dhu 3 xml sql-server encoding json utf-8

背景 - 我从网站接收以 UTF-8 编码的 json 格式的响应数据。json 的 body 属性具有 base64binary 类型的值,我将其存储为 ms sql server 上的 nvarchar 类型。

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

请参阅下面的剖析代码和底部的可运行示例,了解我的担忧。

在转换过程中注意有趣的字符。

例如。代表IRB控股公司(的“公司“)

以下查询修复了上述问题 - 我看到引号应该出现,但随后在包含'&' 的行上失败,这是 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)')))
Run Code Online (Sandbox Code Playgroud)

以下查询通过使用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
Run Code Online (Sandbox Code Playgroud)

问题- 我是否必须为其他 xml 特殊字符添加更多替换语句 - < , >

Shn*_*ugo 5

更新:我刚刚学到了一些新东西,那就是 - 嗯 - 太棒了:-)

\n

试试这个功能

\n
CREATE FUNCTION dbo.Convert_utf8(@utf8 VARBINARY(MAX))\nRETURNS NVARCHAR(MAX)\nAS\nBEGIN\n    DECLARE @rslt NVARCHAR(MAX);\n\n    SELECT @rslt=\n    CAST(\n          --\'<?xml version="1.0" encoding="UTF-8"?><![CDATA[\'\n          0x3C3F786D6C2076657273696F6E3D22312E302220656E636F64696E673D225554462D38223F3E3C215B43444154415B\n          --the content goes within CDATA\n        + @utf8\n        --\']]>\'\n        + 0x5D5D3E\n    AS XML).value(\'.\', \'nvarchar(max)\');\n\n    RETURN @rslt;\nEND\nGO\n
Run Code Online (Sandbox Code Playgroud)\n

并这样称呼它

\n
SELECT *\n      ,dbo.Convert_utf8(CAST(t.body AS XML).value(\'.\',\'varbinary(max)\'))\nFROM @t t;\n
Run Code Online (Sandbox Code Playgroud)\n

结果是

\n
DALLAS, TX \xe2\x80\x93 May 7, 2019 \xe2\x80\x93 Covey & Park Energy Holdings LLC (\xe2\x80\x9cCovey Park\xe2\x80\x9d \n
Run Code Online (Sandbox Code Playgroud)\n

GSerg,非常感谢你!下面为您解答。我尝试并简化了它以在 UDF 中工作。

\n

看起来好像 avarbinary(max)到 XML 的转换完全在 CLR 环境中完成,其中考虑了 XML 的编码声明。这似乎也适用于其他编码,但我现在没有时间来一般性地测试它。

\n

现在剩下的答案

\n

因为它包含一些有关字符串编码的背景知识,可能值得阅读。

\n

我简化了你的代码:

\n
declare @t table ( [body] nvarchar(max) ) \n\ninsert into @t(body) \nselect \'REFMTEFTLCBUWCDigJMgTWF5IDcsIDIwMTkg4oCTIENvdmV5ICYgUGFyayBFbmVyZ3kgSG9sZGluZ3MgTExDICjigJxDb3ZleSBQYXJr4oCdIA==\';\n\nSELECT  CAST(t.body AS XML).value(\'.\',\'varbinary(max)\')\n       ,CAST(CAST(t.body AS XML).value(\'.\',\'varbinary(max)\') AS VARCHAR(MAX))\nFROM @t t;\n
Run Code Online (Sandbox Code Playgroud)\n

你会看到这个结果

\n
0x44414C4C41532C20545820E28093204D617920372C203230313920E2809320436F7665792026205061726B20456E6572677920486F6C64696E6773204C4C432028E2809C436F766579205061726BE2809D20  \nDALLAS, TX \xc3\xa2\xe2\x82\xac\xe2\x80\x9c May 7, 2019 \xc3\xa2\xe2\x82\xac\xe2\x80\x9c Covey & Park Energy Holdings LLC (\xc3\xa2\xe2\x82\xac\xc5\x93Covey Park\xc3\xa2\xe2\x82\xac \n
Run Code Online (Sandbox Code Playgroud)\n

我会将第一个字符放置得对读者更友好

\n
0x44414C4C41532C20545820E28093  \n   D A L L A S ,   T X   \xc3\xa2 \xe2\x82\xac \xe2\x80\x9c \n
Run Code Online (Sandbox Code Playgroud)\n

The0x44是 the D,两倍 the0x4C是两倍LL,在空格之后0x20我们得到E28093。这是短划线的 3 字节编码代码点。SQL-Server 不会帮助你解决这个问题...它会将其解释为 3 个字符,每个字符 1 个字节...

\n

恐怕你运气不好......

\n

SQL-Server 不支持utf-8字符串。对启用来自文件系统的输入的支持有限,但其中的BCP / BULK字符串必须是两个受支持的选项之一: T-SQL

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

UTF-8与 兼容(var)char,只要我们坚持使用纯拉丁语一字节代码。但是任何高于 127 的 ASCII 代码都会导致麻烦(可能会使用正确的排序规则)。但是 - 这是您的情况 - 您的字符串使用 multi-byte-code-pointsUTF-8将为一个字符使用两个或什至更多字节(最多 4 个!)来编码大量字符。

\n

你可以做什么

\n

你必须使用一些能够处理 UTF-8 的引擎

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

并且 - 感谢@GSerg - 还有两个选项:

\n
    \n
  • 等待v2019。将有特殊的排序规则允许utf-8T-SQL 字符串的本机支持
  • \n
  • 这个答案提供了一个UDF,它可以将UTF8转换为NVARCHAR。它不会很快,但它有效。
  • \n
\n

一般说明

\n

数据库可以按原样保存存储数据工作数据,您想以一种或另一种方式使用。将图片存储为VARBINARY(MAX)只是一大块位。您不会尝试使用 SQL-Server 来执行图像识别。

\n

这与文本数据相同。如果您只存储一大块文本,那么如何执行此操作并不重要。但如果你想使用这个文本进行过滤、搜索或者如果你想使用SQL-Server来显示这个文本,你必须考虑格式和性能需求。

\n

具有可变字节长度的编码不允许简单的SUBSTRING(\'blahblah\',2,3). 对于固定长度,引擎可以将字符串作为数组,跳转到第二个索引并选择接下来的三个字符。但是对于可变字节,如果可能存在任何多字节代码点,引擎将必须通过检查之前的所有字符来计算索引。这会极大地减慢很多字符串方法的速度......

\n

最好的是,不要以 SQL-Server 无法(很好)处理的格式存储数据......

\n


GSe*_*erg 5

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;
Run Code Online (Sandbox Code Playgroud)

这里的重要部分是:

  • 所述不存在N在字符串文字的前面
  • encoding="UTF-8"
  • 我们知道 XML 声明元素中的字符与 latin1 中的字符具有相同的 UTF-8 表示,因此将它们转换varbinary为有效的 UTF-8
  • <![CDATA]]>块。

请注意,它仍然只是一个黑客攻击。一旦涉及 XML,就会受到 XML 的限制,并且如果您的字符串包含无法在 XML 中表示的字符,则该类型的 XML 转换将失败

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