ASCII字符串在UTF8 XML文档中的编码为字节数组。

3
我有以下要求:

...文件必须以UTF-8编码... 姓氏字段仅允许(扩展)ASCII... 城市仅允许ISOLatin1... 消息必须作为IBytesMessage放在(IBM Websphere)消息队列中

XML文档如下:
<?xml version="1.0" encoding="utf-8"?>
<foo>
  <lastname>John ÐØë</lastname>
  <city>John ÐØë</city>
  <other>UTF-8 string</other>
</foo>

"ÐØë" 部分是(或应该是) ASCII 值 208、216、235。

我还有一个对象:

public class foo {
  public string lastname { get; set; }
}

所以我实例化一个对象并设置姓氏:

var x = new foo() { lastname = "John ÐØë", city = "John ÐØë" };

现在这里就是我的头痛之处(或者你可以说是梦中的梦……):

  • Visual Studio / 源代码使用的是Unicode
  • 因此:对象有一个Unicode的姓氏
  • XML序列化器使用UTF-8来编码文档
  • 姓氏应该只包含(扩展)ASCII字符;这些字符是有效的ASCII字符,但当然是以UTF-8编码形式存在

通常我不会遇到任何编码问题;我熟悉软件开发人员绝对必须知道的有关Unicode和字符集的绝对最低限度(没有借口!),但这个问题让我束手无策……

我明白UTF-8文档可以“包含”两种编码,因为代码点“重叠”。但是当我需要将序列化的消息转换为字节数组时,我就迷失了。在进行转储时,我看到C3 XX C3 XX C3 XX(我手头没有实际的转储)。很明显(或者我看得太久了),姓氏/城市字符串以其Unicode形式放入序列化文档中;字节数组表明如此。

现在,我需要在哪里做什么才能确保Lastname字符串作为 ASCII 字符串(以及实际的208、216、235字节序列)进入XML文档和最终的字节数组,并且City作为 ISOLatin1 ?

我知道要求是反向的,但我无法改变这些要求(第三方)。我总是为我们的内部项目使用UTF-8,因此我必须支持unicode-utf8 => ASCII / ISOLatin1转换(当然,仅适用于这些集合中的字符)。

我的头疼了...


2
“ÐØë” 部分是(或应该是)ASCII 值 208、216、235。这是无意义的。没有大于127的ASCII值。 - CodesInChaos
有一个叫做扩展ASCII的东西(http://en.wikipedia.org/wiki/Extended_ASCII)。虽然它没有标准化,但我需要允许一些变音符号,因此被迫使用它(并希望一切顺利)。 - RobIII
文档必须使用UTF-8进行编码。这是我们唯一需要关注的要求吗? - Chris S
@ChrisS 嗯,文档必须使用UTF8编码,消息必须作为字节数组放入队列中(这实际上应该就是“UTF8字节”),lastname/city节点中的值必须使用ASCII/ISOLatin1编码(或者至少在UTF8或其他编码方式中编码为ASCII/ISOLatin1)...我并没有捏造这些要求... - RobIII
6个回答

5
不用在意XML文档如何进行编码传输。实现你想要的目标——对某些非ASCII字符进行编码以便它们可以安全地传输,正确的方法是使用 XML字符引用来代表需要被保留的字符。例如,你的
ÐØë

使用XML字符引用来表示

&#x00D0;&#x00D8;&#x00EB;

接收方符合规范的XML处理器将/应该/必须将这些数字字符引用转换回它们表示的字符。以下是一些可以实现此功能的代码:

public static string ConvertToXmlCharacterReference( this string xml )
{
  StringBuilder sb  = new StringBuilder( s.Length ) ;
  const char    SP  = '\u0020' ; // anything lower than SP is a control character
  const char    DEL = '\u007F' ; // anything above DEL isn't ASCII, per se.

  foreach( char ch in xml )
  {
    bool isPrintableAscii = ch >= SP && ch <= DEL ;

    if ( isPrintableAscii ) { sb.Append(ch)                             ; }
    else                    { sb.AppendFormat( "&#x{0:X4}" , (int) ch ) ; }

  }

  string instance = sb.ToString() ;
  return instance ;
}

你也可以使用正则表达式进行替换,或编写XSLT来完成相同的工作。但是这个任务非常简单,不需要那种方法。上面的代码可能更快、更节省内存,并且更容易理解。
你应该注意,由于你想在同一文档中保留两种不同的编码,所以你的转换程序需要区分从"扩展ASCII"到XML字符引用的转换和从"ISO Latin 1"到XML字符引用的转换。
在两种情况下,字符引用指定ISO/IEC 10646字符集中的代码点-基本上是unicode。你需要将字符映射到适当的代码点。由于CLR世界中的字符串是UTF-16编码的,所以这不应该成为问题。我相信上面的代码应该可以很好地工作,除非你有一些与UTF-16不兼容的非常奇怪的东西。

嗯,我没有考虑过XML字符引用(我知道它们的存在)。现在我只是好奇他们是否会将Ð视为8个字节还是1个字节,因为他们告诉我我的测试字符串(请参见我的其他回复)太长了...会进行测试... - RobIII
从文档的角度来看,它是一个单一的字符。一旦正确解析,消费者应该看到一个单一的字符,就像您必须将文档内容中的 < 表示为 &lt;&#x003C; 一样。 - Nicholas Carey
我知道这只是一个单一字符(从文件的角度来看),但现在我真的很好奇第三方的反应会是什么。我担心他们正在计算字节……哦,那就是他们的问题:P - RobIII
我现在会接受这个答案。当第三方测试恢复时,我会让大家知道实际结果是什么... - RobIII

0
该文档必须使用UTF-8进行编码。Lastname字段仅允许ASCII字符。City字段仅允许ISOLatin1字符集。消息必须作为IBytesMessage放置在(IBM Websphere)MessageQueue上。
如果这是确切的规范,那么我认为您可能误解了它。您的任务不是编码,而是验证/回退。整个文档 - 包括Lastname和City字段 - 必须编码为UTF-8。简单地说,如果XML文档声明其编码为UTF-8,然后包含不符合该编码的字节值,则该文档将无效。
方便的是,ASCII与Unicode的前128个代码点重叠;Latin1与前256个代码点重叠。
要检查Lastname是否可以表示为ASCII,则可以检查其所有字符的代码点是否在0-127范围内。
bool isLastnameAscii = foo.Lastname.All(c => (int)c < 128);

为了符合您的规范,您需要将无效字符强制转换为替换字符(通常为?),方法是将字符串编码为ASCII,然后再进行解码:
foo.Lastname = Encoding.ASCII.GetString(Encoding.ASCII.GetBytes(foo.Lastname));

同样地,对于City

bool isCityLatin1 = foo.City.All(c => (int)c < 256);

Encoding latin1 = Encoding.GetEncoding("iso-8859-1");
foo.City = latin1.GetString(latin1.GetBytes(foo.City));

因此,您应该将所有内容保存为UTF-8。

我的假设是,您的第三方软件可以使用UTF-8正确解码XML文档;但是,它必须提取LastnameCity字段,并在只允许ASCII和Latin1的某个地方使用它们。它对您施加了限制,以确保不会因存在不允许的字符而强制发生数据丢失。

编辑:这是您提出的解决方法。我在“扩展ASCII”的位置使用Latin1,因为后者术语是模糊的。

var x = new foo() { lastname = "John ÐØë", city = "John ÐØë", other = "—" };

using (var stream = new MemoryStream())
using (var utf8writer = new StreamWriter(stream, Encoding.UTF8))            
using (var latin1writer = new StreamWriter(stream, Encoding.GetEncoding("iso-8859-1")))
{
    utf8writer.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
    utf8writer.WriteLine("<foo>");
    utf8writer.Flush();

    latin1writer.WriteLine("  <lastname>" + SecurityElement.Escape(x.lastname) + "</lastname>");
    latin1writer.WriteLine("  <city>" + SecurityElement.Escape(x.city) + "</city>");
    latin1writer.Flush();

    utf8writer.WriteLine("  <other>" + SecurityElement.Escape(x.other) + "</other>");
    utf8writer.WriteLine("/<foo>");
    utf8writer.Flush();

    byte[] bytes = stream.ToArray();
}

SecurityElement.Escape 可以将字符串中的无效 XML 字符替换为它们的有效 XML 等价物(例如,将 < 替换为 &lt,将 & 替换为 &amp;)。


很遗憾,不行。例如:姓氏也被限制在70个字符以内。我发送了测试字符串“ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz&'!<>ÀÁÂÃÄÅÆÇÈÉÊËÌ”,结果是(第三方)响应:姓氏太长了:它是83个字节,应该是70个字节。当然,它包含变音符号。但我也明确告知姓氏字段允许使用变音符号... - RobIII
但是我也明确地被告知姓氏字段允许使用变音符号... 结果我开始怀疑自己的知识,但事实证明一直以来我没错。 你可能是正确的,他们需要与旧系统进行接口;我已经设置好了正则表达式(0x00-0x7F),但将它们删除了,因为我必须处理像“Du Pré”这样的名称,并且他们明确告诉我变音符号不会是一个问题。但现在他们告诉我测试字符串太长了,其实并不是:它恰好是70个字符长,但83个字节。 - RobIII
尝试使用我更新的代码;它可以将Latin1字符(例如é)编码为一个字节。 - Douglas
我肯定不会“手写”XML文档。我需要序列化大量的对象类型;这样对于每个文档来说都需要花费数天的时间才能搞定。 - RobIII

0

我理解这有两个独立的要求:

1)XML必须是UTF-8编码的;

2)城市名称仅限于ISOLatin1。

这意味着当您将UTF-8解码为Uncode时,城市字符仅来自于ISOLatin1集。换句话说,XML可以是ISOLatin1编码的(所有文本都来自于ISOLatin1代码表),但它是UTF-8的。ISOLatin1是Unicode表的一小部分,而UTF-8是Unicode的8位编码。


0
所以.. System.Text.Encoding.ASCII.GetBytes(string) 可能会做你想要的事情.. 将一个字符串转换为 ASCII 编码的字节数组。

嗯...现在我的头也疼了。 - Sam Axe

0

在UTF-8编码的字符串/字节数组中,您无法简单地拥有208、216、235字节序列。

我希望您可以将XML保存为ISO 8859-1格式,无论是否在XML <?xml version="1.0" encoding="XXXXXXXXXX"?>处理指令中提及(甚至在XML头中指定无效的UTF-8编码)。

否则,如果您的要求如您所述 - 只需为给定输入请求精确的预期字节数组,并制作自己的自定义序列化(或者也许是自定义编码,不确定是否可能)。


在UTF-8编码的字符串/字节数组中,你根本无法拥有208、216、235字节序列。这是因为ASCII的“扩展”部分,对吧?因为“普通”的ASCII与UTF-8共享代码点(0-127)。我并不想自己编写序列化或自定义编码,如果那是唯一的解决方案,那就算了;我会把问题交回给他们。 - RobIII
在维基百科上找到了这个链接:link:“这意味着所有的字节0x00-0x7F与ASCII中的含义相同”。所以看起来我只需要放弃对变音符号的支持... - RobIII
因为208落在0x80-0x7FF范围内,必须以UTF8的2个字节编码(http://en.wikipedia.org/wiki/UTF-8)。有效的UTF8字节流不允许两个最高位为`11`的字节相互跟随。 - Alexei Levenkov
那么当我告诉第三方他们错误地认为变音符号没问题时,我一直是正确的吗?符合这些要求并允许使用变音符号是不可能的,我是对的吗? - RobIII
你无法获得208、216、235字节序列,你能获得的是具有UTF8字节流的有效编码字符(Encoding.UTF8.GetBytes("\u00D0\u00D8\u00EB") -> 195,144,195,152, 195,171)。显然,208不是你要查找的字符的Unicode代码,但如果必须的话(即手动将字符串转换为另一种编码并使用转换后的代码重建字符串),你仍然可以在字符串中使用208。 - Alexei Levenkov

-1

Nicholas Carey 的答案是可以的,但是有误差并且代码无法工作。我没有足够的声望来评论,所以在这里写出可用代码:

public static string ConvertToXmlCharacterReference(string xml)
    {
        StringBuilder sb = new StringBuilder();
        const char SP = '\u0020'; // anything lower than SP is a control character
        const char DEL = '\u007F'; // anything above DEL isn't ASCII, per se.
        int i = 0;
        foreach (char ch in xml)
        {
            bool isPrintableAscii = ch >= SP && ch <= DEL;
            if (isPrintableAscii)
            {
                sb.Append(ch);
            }
            else
            {
                sb.AppendFormat("&#x{0:X4};", (int) ch);
            }
        }
        string instance = sb.ToString();
        return instance;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接