【问题标题】:Converting accented characters in varchar() to XML causing "illegal XML character"将 varchar() 中的重音字符转换为 XML 导致“非法 XML 字符”
【发布时间】:2026-02-16 23:30:01
【问题描述】:

我有一个应用程序写入的表。该字段是 varchar(max)。数据看起来像 xml。

DECLARE @poit VARCHAR(100)
SET @poit = '<?xml version="1.0" encoding="utf-8"?><test>VÍA</test>'
SELECT CONVERT(XML,@poit)

但是(似乎是因为 UTF8;删除它有效),我收到此错误:

XML parsing: line 1, character 46, illegal xml character

有没有办法干净地转换它?

我找到了这个线程,它谈到 varchar 不支持“非 ASCII 字符”,但显然 I 是非 unicode。是的,我可以这样做:

SELECT CONVERT(XML, REPLACE(@poit, 'encoding="utf-8"', ''))

但这是最好的方法吗?

Why does casting a UTF-8 VARCHAR column to XML require converting to NVARCHAR and encoding change?

【问题讨论】:

  • 离我只有几美分。 This 答案很好地解释了为什么您应该始终使用 nvarchar 而不是 varchar。
  • 是和不是。实际上,对我来说,这通常不值得花费(存储空间、备份空间和 IOPS)。是的,它会让生活更简单,但我已经以 40GB/天的速度增长。
  • @gofr1 这个答案过于简单化,而且基于非常错误的神话,即磁盘的唯一成本是金钱。我写了一篇关于这个主题的文章,详细介绍了所有“隐藏”成本:Disk Is Cheap! ORLY?(该站点确实需要免费注册才能查看其内容)。此外,我在下面更新了我的答案,以包含有关XML 的内部优化、类型以及使用它时实际占用的空间比使用VARCHAR(MAX) 更少的信息。

标签: sql-server xml unicode character-encoding type-conversion


【解决方案1】:

如果您只想得到答案而没有完整的解释,请向下滚动到“结论”。但是,你真的应该花点时间阅读解释? ;DR>

这里发生了一些事情:

  1. &lt;xml&gt; 元素的encoding= 属性用于表示如何解释 XML 文档的底层字节。如果字符串文字中的文档是正确的,则不需要有encoding 属性。如果存在不正确的字符,则encoding 属性可以保留,因为它将通知 XML 转换这些字符的原始内容。

  2. UTF-8 是一种 Unicode 编码,但变量和文字是 VARCHAR 数据,而不是 NVARCHAR(这还需要在字符串文字前加上大写字母-N)。通过使用VARCHAR 并且不使用N-前缀,如果在执行此查询时,XML 文档中有任何字符无法放入由您所在的任何数据库的默认排序规则表示的代码页中,您将拥有已经丢失了这些字符(即使您可以在屏幕上看到它们,它们在 VARCHAR 变量中也不正确,或者如果您对该文字做了简单的 SELECT)。

  3. Windows(和 .NET、SQL Server 等)使用 UTF-16 Little Endian。 Í 字符 Latin Capital Letter I with Acute 存在于代码页 1252 UTF-16LE 中作为值 205(例如 SELECT ASCII('Í'), CHAR(205); ),这就是当您删除 encoding="utf-8" 和为什么您没有通过将其放在 VARCHAR 文字和变量中来“丢失”该字符。但是,如该链接页面上所示,UTF-8 编码中的字节序列是 195、141(是的,两个字节)。这意味着,如果该字符确实是 UTF-8 编码的,那么在放入 UTF-16LE 环境时,它看起来不会是该字符。

    XML 转换查看该字符的字节值 205(单字节,因为它当前是 VARCHAR 数据)并尝试提供与 UTF-8 中的 那个 序列等效的 UTF-16LE。除了 205 本身在 UTF-8 中不存在。因此,您需要添加下一个字符,即大写字母“A”,其值为 65。虽然 UTF-8 中有两个字节序列,但它们都不是 205、65。这就是为什么你得到illegal xml character 错误。

  4. 由于屏幕上的文本必须是 UTF-16LE,如果源真的是 UTF-8,那么底层的 UTF-8 字节序列就必须转换成 UTF-16LE。 Í 的底层字节序列是 195、141。因此,我们可以通过执行以下操作从 Code Page 1252 的常规 ASCII 字符(因为这又是当前的 VARCHAR 数据)创建该序列:

    DECLARE @poit VARCHAR(100);
    SET @poit = '<?xml version="1.0" encoding="UTF-8"?><test>V'
                  + CHAR(195) + CHAR(141) + 'A</test>';
    SELECT CONVERT(XML, @poit);
    

    返回:

    <test>VÍA</test>
    

    数据仍然是VARCHAR 并且 encoding="utf-8" 仍然在&lt;xml&gt; 元素中!

  5. 如果将数据保留为 VARCHAR,则仅对 encoding= 值进行以下更改即可:

    DECLARE @poit VARCHAR(100);
    SET @poit = '<?xml version="1.0" encoding="Windows-1252"?><test>VÍA</test>';
    SELECT CONVERT(XML, @poit);
    

    这假定源编码确实是“Windows-1252”,这是 Microsoft 的 Latin1_General 版本,它是 Latin1_General 排序规则的基础。

    但是,如果它与任何 VARCHAR 数据假定的当前数据库默认排序规则的代码页相同,则甚至不需要指定“编码”。

  6. 1234563李>

结论

  1. 将 XML 作为字符串处理时使用 NVARCHAR(MAX) 数据类型(而不是 VARCHAR)。

  2. 对于没有任何更改字符的字符串(即,所有内容在屏幕上看起来都很完美),然后只需删除 encoding="utf-8" 即可。无需将其替换为 UTF-16,因为该值的本质是在 NVARCHAR 变量或文字中(即以大写字母-N 为前缀的字符串)。


关于使用VARCHAR(MAX) 代替XML 甚至NVARCHAR(MAX) 以节省空间,请记住XML 数据类型在内部进行了优化,因此元素和属性名称只存储一次,在字典中,因此几乎没有完全写出的 XML 字符串版本的开销。因此,虽然 XML 类型确实将字符串存储为 UTF-16LE,if XML 文档有很多重复的元素和/或属性名称,然后使用 @987654362 @type 实际上可能比使用VARCHAR(MAX) 占用更少的空间:

DECLARE @ElementBased XML;
SET @ElementBased = (
                     SELECT * FROM master.sys.all_columns FOR XML PATH('Row')
                    );

DECLARE @AttributeBased XML;
SET @AttributeBased = (
                       SELECT * FROM master.sys.all_columns FOR XML RAW('Row')
                      );

SELECT @ElementBased AS [ElementBasedXML],
       @AttributeBased AS [AttributeBasedXML],

       DATALENGTH(@ElementBased) AS [ElementBasedXmlBytes],
       DATALENGTH(CONVERT(VARCHAR(MAX), @ElementBased)) AS [ElementBasedVarCharBytes],
       ((DATALENGTH(@ElementBased) * 1.0) / DATALENGTH(CONVERT(VARCHAR(MAX), @ElementBased))
               ) * 100 AS [XmlElementSizeRelativeToVarcharElementSize],

       DATALENGTH(@AttributeBased) AS [AttributeBasedXmlBytes],
       DATALENGTH(CONVERT(VARCHAR(MAX), @AttributeBased)) AS [AttributeBasedVarCharBytes],
       ((DATALENGTH(@AttributeBased) * 1.0) /
         DATALENGTH(CONVERT(VARCHAR(MAX), @AttributeBased))) * 100
               AS [XmlAttributeSizeRelativeToVarCharAttributeSize];

返回(至少在我的系统上):

ElementBasedXmlBytes                              1717896
ElementBasedVarCharBytes                          5889081
XmlElementSizeRelativeToVarcharElementSize        29.170867237180130482100

AttributeBasedXmlBytes                            1544661
AttributeBasedVarCharBytes                        3461864
XmlAttributeSizeRelativeToVarCharAttributeSize    44.619343798600984902900

如您所见,对于基于元素的 XML,XML 数据类型的大小是 VARCHAR(MAX) 版本的 29%,而对于基于属性的 XML,XML 数据类型的大小是VARCHAR(MAX) 版本。

【讨论】:

  • 感谢您添加关于重复字符串内部优化的最后一点,以及超级完整的答案!
  • 这是一个很好的答案,但它太长了,我几乎错过了结论并滚动到其他答案。
  • @nurettin 鉴于关于 Unicode / 编码 / 排序规则 / 等的大量错误信息和误解,并且关于这个主题的大多数帖子和答案都是不正确的(在不同程度上),我认为这个答案非常不会太长,因为非常需要此信息。但是,我确实意识到一天中的时间也只有这么多,所以我在顶部添加了一条注释,指示人们如果不想解释,请滚动到结论。
  • @SolomonRutzky 很整洁
【解决方案2】:

我会尝试将@poit 变量的数据类型从VARCHAR(100) 更改为NVARCHAR(100)。然后将 utf-8 编码替换为 utf-16,这样您的代码将如下所示:

    DECLARE @poit NVARCHAR(100)
    SET @poit = '<?xml version="1.0" encoding="utf-8"?><test>VÍA</test>'
    SELECT CONVERT(XML,REPLACE(@poit, 'utf-8', 'utf-16'))

只要您没有在返回大量结果的 SELECT 中调用带有替换的转换,性能应该会很好,并且可以完成工作。

参考:http://xml.silmaril.ie/characters.html

【讨论】:

  • 关于使用NVARCHAR 而不是VARCHAR 的好处。但是,您需要一直携带它并在文字前加上大写字母-N,否则您可能会丢失数据。此外,无需将utf-8 替换为utf-16,因为UTF-16 是NVARCHAR 数据无论如何都可以使用的唯一编码;-)。如果有兴趣,我的答案中有完整的解释:)。