为什么连字符(–)会触发非法XML字符错误(C#/SSSMS)



这不是关于如何克服"XML解析:…非法XML字符"错误的问题,而是关于为什么会发生我知道有修复方法(1、2、3),但在选择最佳解决方案之前,我需要知道问题是从哪里产生的(是什么原因导致了隐藏的错误?)。

我们正在使用C#调用一个基于Java的Web服务。根据返回的强类型数据,我们正在创建一个将传递给SQL Server的XML文件。webservice数据使用UTF-8编码,因此在C#中我们创建文件,并在适当的地方指定UTF-8:

var encodingType = Encoding.UTF8;
// logic removed...
var xdoc = new XDocument();
xdoc.Declaration = new XDeclaration("1.0", encodingType.WebName, "yes");
// logic removed...
System.IO.File.WriteAllText(xmlFullPath, xdoc.Declaration.ToString() + xdoc.Document.ToString(), encodingType);

这将在磁盘上创建一个XML文件,该文件包含以下(缩写)数据:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<records>
  <r RecordName="Option - Foo" />
  <r RecordName="Option – Bar" />
</records>

请注意,在第二条记录中,-不同。我相信第二次审判即将进行。

如果我在Firefox/IE/VS2015中打开那个XML文件。它打开时不会出错。W3C XML验证器也可以正常工作。但是,SSMS 2012不喜欢它:

declare @xml XML = '<?xml version="1.0" encoding="utf-8" standalone="yes"?><records>
  <r RecordName="Option - Foo" />
  <r RecordName="Option – Bar" />
</records>';

XML解析:第3行,字符25,非法的XML字符

那么,为什么连字符会导致错误呢?从我的研究来看,

只有少数实体需要转义:<,>,\,'和&在HTML和XML。源

连破折号不是其中之一。编码版本(用&#8211;替换)工作良好。

更新

根据输入,人们表示短划线不被识别为UTF-8,但它在这里列出了http://www.fileformat.info/info/unicode/char/2013/index.htm那么,作为一个完全合法的字符,为什么SSMS在作为XML(使用UTF-8或UTF-16)传递时不读取它呢?

请允许我回答自己的问题,以便我自己充分理解。我不会接受这个答案;正是其他答案的结合把我带到了这里。如果这个答案在未来对你有帮助,请在其他帖子中也投赞成票。

基本的基本规则是,带有Unicode字符的XML应传递给SQL Server,并由SQL Server解析为Unicode。因此,C#应该将XML生成为UTF-16;SSMS和.Net默认值。

原始问题的原因

此变量声明使用UTF-8编码的XML,但如果不使用UTF-8编码,则不能使用实体短划线。这是错误的:

DECLARE @badxml xml = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<records>
  <r RecordName="Option – Bar" />
</records>';

XML解析:第3行,字符29,非法的XML字符

另一种不起作用的方法是在XML中将UTF-8转换为UTF-16。这里的字符串不是unicode,因此隐式转换失败:

DECLARE @xml xml = '<?xml version="1.0" encoding="utf-16" standalone="yes"?>
<records>
  <r RecordName="Option – Bar" />
</records>';

XML解析:第1行,字符56,无法切换编码

解决方案

有效的替代方案有:

1) 保留为UTF-8,但在实体上使用十六进制编码(参考):

DECLARE @xml xml = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<records>
  <r RecordName="Option &#x2013; Bar" />
</records>';

2) 如上所述,但在实体上使用十进制编码(参考):

DECLARE @xml xml = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<records>
  <r RecordName="Option &#8211; Bar" />
</records>';

3) 包含原始实体,但在声明中删除UTF-8编码(然后SSMS应用UTF-16;它的默认值):

DECLARE @xml xml = '<?xml version="1.0" standalone="yes"?>
<records>
  <r RecordName="Option – Bar" />
</records>';

4) 保留UTF-16声明,但将XML强制转换为Unicode(在强制转换为XML之前请注意前面的N):

DECLARE @xml xml = N'<?xml version="1.0" encoding="utf-16" standalone="yes"?>
<records>
  <r RecordName="Option – Bar" />
</records>';

您能修改XML编码声明吗?如果是;

declare @xml XML = N'<?xml version="1.0" encoding="utf-16" standalone="yes"?><records>
  <r RecordName="Option - Foo" />
  <r RecordName="Option – Bar" />
</records>';
select @xml
(No column name)
<records><r RecordName="Option - Foo" /><r RecordName="Option – Bar" /></records>

推测性编辑

这两种方法都会因非法xml字符而失败:

set @xml = '<?xml version="1.0" encoding="utf-8"?><x> – </x>'
set @xml = '<?xml version="1.0" encoding="utf-16"?><x> – </x>'

因为它们将非unicode varchar传递给XML解析器;字符串包含Unicode,因此必须按Unicode处理,即nvarchar(utf-16)(否则,包含的3个字节被误解为多个字符,并且一个或多个不在XML可接受的范围内)

这确实将CCD_ 9字符串传递给解析器,但失败,无法切换编码

set @xml = N'<?xml version="1.0" encoding="utf-8"?><x> – </x>'

这是因为nvarchar(utf-16)字符串被传递给XML解析器,但XML文档声明其utf-8和在两种编码中不等价

这一切都是utf-16

set @xml = N'<?xml version="1.0" encoding="utf-16"?><x> – </x>'

SQL服务器内部使用UTF-16。要么放弃编码,要么转换为unicode

您要查找的原因是:指定UTF-8时,此字符未知。

--without your directive, SQL Server picks its default
declare @xml XML = 
'<records>
  <r RecordName="Option - Foo" />
  <r RecordName="Option – Bar" />
</records>';
select @xml;
--or UNICODE, but you must use UTF-16
declare @xml2 XML = 
CAST('<?xml version="1.0" encoding="utf-16" standalone="yes"?>
<records>
  <r RecordName="Option - Foo" />
  <r RecordName="Option – Bar" />
</records>' AS NVARCHAR(MAX));
select @xml2

更新

UTF-8的意思是,有8位的数据块用来携带信息。基本字符只是一个块,很容易。。。

其他字符也可以进行编码。有"c2"one_answers"c3"代码(请查看此处)。c3代码需要三个块来进行编码。但是内部使用的UTF16需要2字节的编码字符。

希望现在一切都清楚了。。。

更新2

此代码将向您显示,连字符具有ASCII代码45和您的连字符150:

DECLARE @x VARCHAR(100)=
'<r RecordName="Option - Foo" /><r RecordName="Option – Bar" />';
WITH RunningNumbers AS
(
    SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS Nmbr
    FROM sys.objects
)
SELECT SUBSTRING(@x,Nmbr,1), ASCII(SUBSTRING(@x,Nmbr,1)) AS ASCII_Code
FROM RunningNumbers
WHERE ASCII(SUBSTRING(@x,Nmbr,1)) IS NOT NULL;

看看这里所有7位的字符都是"普通"的,应该编码没有问题。"扩展ASCII"取决于代码表,可能会有所不同。150可能是冲刺或其他什么。UTF8使用一些棘手的编码来允许奇怪的字符是"合法的"。显然(这对我来说也是新的)内部使用的UTF16无法处理c3字符。

MSDN指南中说:

SQLXML 4.0依赖于SQL中提供的对DTD的有限支持服务器SQL Server允许xml数据类型数据中的内部DTD,可用于提供默认值和替换实体引用及其扩展内容。SQLXML传递XML数据"按原样"(包括内部DTD)发送到服务器。您可以转换使用第三方工具将DTD转换为XML架构(XSD)文档,并加载将具有内联XSD模式的数据插入数据库。

最新更新