我在 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&#x1E;&#x1C;FGHIJK&#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)
我在写这篇文章时发现的一些链接:
这可能远非高效,并且它还假设没有其他转义序列可以出现在从XML获得的
varchar
值中,例如由于具有双重转义符号而具有&amp;
(&
).但是,如果这是真的,那么我们可能会做一些 varbinary (ab)use。
首先,我从 XML 中获取文本的值。然后我将该字符串拆分为每一行的单个字符,使用
GENERATE_SERIES
和 SUBSTRING
并通过计算符号(&
)和分号(;
)的数量来计算字符是否在转义序列中
) 具有不同窗口的字符。
然后我检查那个组中是否有分号;如果有,那就是逃生组织。最后,我重新聚合字符串,省略转义序列中的某些字符,但是,我以二进制形式聚合字符串;将非转义序列转换为二进制值(例如
'41'
为 'a'
),同时保留转义序列原样。然后我convert那个值到一个实际的varbinary
然后CONVERT
它回到一个varchar
。 讨厌,但它有效:
DECLARE @xml_edi XML = '<Bundle><RawData>ABCDE&#x1E;&#x1C;FGHIJK&#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 个(或更多)元素具有相同的值,则需要使用不同的(唯一的)标识符。
字符 28、29 和 30 在 XML 1.0 中无效,无论是直接表示还是作为字符引用(

等)都是如此。
在 XML 1.1 中,您可以使用这些字符,前提是您将它们编写为字符引用。但是支持 XML 1.1 的人并不多。
所以你的问题是你认为是 XML 的数据实际上不是,这使得它很难处理。
正如 Larnu 所建议的,这是我编辑中的解决方案作为单独的答案发布。
DECLARE @xml_edi XML = '<Bundle><RawData>ABCDE&#x1E;&#x1C;FGHIJK&#x1D;LMNOP</RawData></Bundle>'
--nested replace
SELECT REPLACE(REPLACE(REPLACE(REPLACE(x.y.value('./RawData[1]','nvarchar(max)'),'',CHAR(28)),'',CHAR(29)),'', char(30)),'',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,'',CHAR(28)) AS Colors) r
CROSS APPLY (SELECT REPLACE(r.Colors,'',CHAR(29)) AS Colors) g
CROSS APPLY (SELECT REPLACE(g.Colors,'',CHAR(30)) AS Colors) b
CROSS APPLY (SELECT REPLACE(b.Colors,'',CHAR(31)) AS Colors) s