在字节数组中以UTF8 XML文档编码ASCII字符串

时间:2012-02-15 17:42:52

标签: c# encoding utf-8 ascii

我有一些以下要求:

  

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

为简单起见,XML文档看起来像这样:

<?xml version="1.0" encoding="utf-8"?>
<foo>
  <lastname>John ÐØë</lastname>
  <city>John ÐØë</city>
  <other>UTF-8 string</other>
</foo>

“ÐØë”部分分别是(或应该)ASCII values 208,216,235。

我也有一个对象:

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

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

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

现在这就是我头痛的地方(或inception,如果你愿意......):

  • Visual studio /源代码位于 Unicode
  • 因此:对象具有 Unicode 姓氏
  • XML Serializer使用 UTF-8 对文档进行编码
  • 姓氏应仅包含(扩展) ASCII 字符;字符是有效的 ASCII 字符,但是以UTF-8编码形式显示

我的编码通常不会遇到任何问题;我熟悉The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)但是这个让我难过......

据我所知,UTF-8文档完全能够“包含”两种编码,因为代码点“重叠”。但是,当我需要将序列化消息转换为字节数组时,我迷失了。在进行转储时,我看到C3 XX C3 XX C3 XX(我手边没有实际的转储)。很明显(或者我已经盯着这个太长时间了)lastname / city字符串以unicode形式放入序列化文档中;字节数组表明了这一点。

现在我需要做什么,以及确保Lastname字符串进入XML文档,最后将字节数组作为 ASCII 字符串(以及实际的208,216,235)字节顺序),那个城市在那里作为 ISOLatin1

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

我的头疼...

6 个答案:

答案 0 :(得分:5)

不管怎样编码XML文档进行传输。做你想做的事情的正确方法 - 对某些非ASCII字符进行编码,使它们在没有受到伤害的情况下幸存下来 - 使用XML character references来表示需要如此保留的字符。例如,你的

ÐØë

使用XML character references表示为

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

接收[conformant] 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不能很好搭配的奇怪的东西。

答案 1 :(得分:0)

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

答案 2 :(得分:0)

你不能在UTF-8编码的字符串/字节数组中有208,216,235字节序列。

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

否则,如果您的要求与您所说的一样 - 只需要为给定输入询问确切的预期字节数组,并制定您自己的自定义序列化(或者可能是自定义编码,也不确定是否可行)。

答案 3 :(得分:0)

  
    

文档必须以UTF-8编码Lastname字段仅允许 ASCII。 City 仅允许 ISOLatin1。该消息必须作为MessageQueue放在(IBM Websphere)IBytesMessage上。

  

如果这是精确的规格,那么我认为你可能会误解它。您的任务不是编码,而是验证/回退之一。 整个文档(包括LastnameCity字段)必须编码为UTF-8。很简单,如果XML文档将其编码声明为UTF-8,然后包含在该编码下无效的字节值,则XML文档将无效。

方便地,ASCII与Uni​​code的前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的地方使用它们。它对您施加了限制,以确保不会被迫导致数据丢失(因为存在不允许的字符)。

修改:这是您提议的解决方法。我使用Latin1代替“扩展ASCII”,因为后一个术语含糊不清。

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;。)

答案 4 :(得分:0)

我将此理解为2个单独的要求:

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

2)城市名称仅限于ISOLatin1。

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

答案 5 :(得分:-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;
    }