使用 T-SQL Xquery 从 XML 中提取数据,其中低 ASCII 表示为“”?或者只使用 T-SQL?

问题描述 投票:0回答:3

我在 XML 中有包含低位 ASCII 的字符串。具体来说,EDI 包含特殊字符,如 char(28)、char(29)、char(30)(也称为文件分隔符/组分隔符/记录分隔符)。开发人员将其编码为 XML 字符串的方式将这些字符更改为“”,其中 x1C 是十六进制表示(char(28),文件分隔符)。然后我们使用 CLR 将其转换回原始文本并存储在表中,其中包含低位 ascii 字符(我们不存储 XML,仅存储其中的数据,作为字段)

但是,我需要在不支持 CLR 的 Azure SQL DB 中执行此操作。

我不确定最好/最快的方法是编写 XQuery 以正确的形式提取它,还是只使用 XQuery“值”并使用 T-SQL 对其进行后处理:嵌套替换、交叉/外部APPLY 替换,甚至可能是 STUFF(TRANSLATE 将无法工作,因为它是多个字符)。

在网上看,我可以看到其他版本的 XQuery 在哪里支持 fn:replace 或 bin:decode-string,但似乎 SQL Server 的 XQuery 不支持这些方法,所以我想弄清楚如何才能做到这一点(“替换价值”,也许吧?)。

DECLARE @xml_edi XML  = '<Bundle><RawData>ABCDE&amp;#x1E;&amp;#x1C;FGHIJK&amp;#x1D;LMNOP</RawData></Bundle>' 
SELECT x.y.value('./RawData[1]','varchar(max)')
from @xml_edi.nodes('/Bundle')x(y)

预期结果:

ABCDEFGHIJKLMNOP  --note that this shows all 3 as the same character, that's a limitation of the browser/editor

(ABCDE,然后是 char(30) & char(28),然后是 FGHIJK,然后是低位 ascii 字符 29,然后是 LMNOP)

我在写这篇文章时发现的一些链接:

sql-server xml xquery xquery-sql
3个回答
1
投票

这可能远非高效,并且它还假设没有其他转义序列可以出现在从XML获得的

varchar
值中,例如由于具有双重转义符号而具有
&amp;amp;
& 
).但是,如果这是真的,那么我们可能会做一些 varbinary (ab)use。

首先,我从 XML 中获取文本的值。然后我将该字符串拆分为每一行的单个字符,使用

GENERATE_SERIES
SUBSTRING
并通过计算符号(
&
)和分号(
;)的数量来计算字符是否在转义序列中
) 具有不同窗口的字符。

然后我检查那个组中是否有分号;如果有,那就是逃生组织。最后,我重新聚合字符串,省略转义序列中的某些字符,但是,我以二进制形式聚合字符串;将非转义序列转换为二进制值(例如

'41'
'a'
),同时保留转义序列原样。然后我convert那个值到一个实际的
varbinary
然后
CONVERT
它回到一个
varchar
讨厌,但它有效:

DECLARE @xml_edi XML  = '<Bundle><RawData>ABCDE&amp;#x1E;&amp;#x1C;FGHIJK&amp;#x1D;LMNOP</RawData></Bundle>';

WITH EscapedStrings AS(
    SELECT x.b.value('(./RawData/text())[1]','varchar(max)') AS EscapedString
    FROM @xml_edi.nodes('/Bundle')x(b)),
Groups AS (
    SELECT ES.EscapedString,
           SS.C,
           GS.Value,
           COUNT(CASE SS.C WHEN '&' THEN 1 END) OVER (PARTITION BY ES.EscapedString ORDER BY GS.Value
                                                      ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) + 
           COUNT(CASE SS.C WHEN ';' THEN 1 END) OVER (PARTITION BY ES.EscapedString ORDER BY GS.Value
                                                      ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS Grp
    FROM EscapedStrings ES
         CROSS APPLY GENERATE_SERIES(CONVERT(bigint,1),LEN(ES.EscapedString),CONVERT(bigint,1)) GS
         CROSS APPLY (VALUES(CONVERT(char(1),SUBSTRING(ES.EscapedString,GS.value,1))))SS(C)),
EscapeGroups AS (
    SELECT G.EscapedString,
           G.C,
           G.Grp,
           G.Value,
           COUNT(CASE G.C WHEN ';' THEN 1 END) OVER (PARTITION BY G.EscapedString, G.Grp) AS EscapeGroup
    FROM Groups G)
SELECT CONVERT(varchar(MAX),CONVERT(varbinary(MAX),STRING_AGG(CASE EG.EscapeGroup WHEN 1 THEN EG.C
                                                                                  ELSE CONVERT(varchar(2),CONVERT(varbinary(1),EG.C),2)
                                                              END,'') WITHIN GROUP (ORDER BY EG.Value),2))
FROM EscapeGroups EG
WHERE NOT (EG.EscapeGroup = 1 AND EG.C IN ('&','#','x',';'))
GROUP BY EG.EscapedString;

我有意对

RawData
元素的值进行分区和分组(有效),以便如果需要针对表中的列使用它,它可以缩放。但是,如果 2 个(或更多)元素具有相同的值,则需要使用不同的(唯一的)标识符。


0
投票

字符 28、29 和 30 在 XML 1.0 中无效,无论是直接表示还是作为字符引用(

&#28;
等)都是如此。

在 XML 1.1 中,您可以使用这些字符,前提是您将它们编写为字符引用。但是支持 XML 1.1 的人并不多。

所以你的问题是你认为是 XML 的数据实际上不是,这使得它很难处理。


0
投票

正如 Larnu 所建议的,这是我编辑中的解决方案作为单独的答案发布。

DECLARE @xml_edi XML  = '<Bundle><RawData>ABCDE&amp;#x1E;&amp;#x1C;FGHIJK&amp;#x1D;LMNOP</RawData></Bundle>' 
--nested replace
SELECT REPLACE(REPLACE(REPLACE(REPLACE(x.y.value('./RawData[1]','nvarchar(max)'),'&#x1C;',CHAR(28)),'&#x1D;',CHAR(29)),'&#x1E;', char(30)),'&#x1F;',CHAR(31))
from @xml_edi.nodes('/Bundle')x(y)

--nested replace; cleaner? (uses https://bertwagner.com/posts/how-to-eliminate-ugly-nested-replace-functions/)
SELECT 
    s.Colors
FROM
    (SELECT x.y.value('./RawData[1]','nvarchar(max)') AS Colors FROM @xml_edi.nodes('/Bundle')x(y) ) c
    CROSS APPLY (SELECT REPLACE(c.Colors,'&#x1C;',CHAR(28)) AS Colors) r
    CROSS APPLY (SELECT REPLACE(r.Colors,'&#x1D;',CHAR(29)) AS Colors) g
    CROSS APPLY (SELECT REPLACE(g.Colors,'&#x1E;',CHAR(30)) AS Colors) b
    CROSS APPLY (SELECT REPLACE(b.Colors,'&#x1F;',CHAR(31)) AS Colors) s
© www.soinside.com 2019 - 2024. All rights reserved.