2012-02-15 77 views
3

我有一些如下因素的要求:编码ASCII字符串的UTF8 XML文档中Byte数组

...的文件必须UTF8编码...的现场只允许(扩展)ASCII ... 只允许ISOLatin1 ...消息必须提上(IBM的Websphere)的MessageQueue作为IBytesMessage

XML文档,用于纯朴起见,看起来像这样:(或者应当是)分别

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

的 “DOE” 部分是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序列化程序使用UTF-8
  • 姓氏应该只包含(扩展)ASCII字符文档编码;该字符是有效ASCII字符但ofcourse在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(我没有实际的转储)。很明显(或者我一直盯着这个太长),姓氏/城市字符串以unicode形式放入序列化文档中;字节数组暗示如此。

现在我需要做什么以及在哪里确保姓氏字符串进入XML文档并最终以字符串形式作为ASCII字符串(以及实际的208,216,235字节序列),并且那个城市在那里作为ISOLatin1

我知道的要求是倒退,但我不能改变这些(第三方)。我总是在内部项目中使用UTF-8,所以我必须支持unicode-utf8 => ASCII/ISOLatin1转换(当然,只适用于那些集合中的字符)。

我的头好痛......

+2

“”ÐØë“部分分别是(或应该是)ASCII值208,216,235。”这是无意义的。没有ASCII值> 127 – CodesInChaos 2012-02-15 17:56:00

+0

有,它被称为扩展ASCII(http://en.wikipedia.org/wiki/Extended_ASCII)。虽然它不是标准化的,但我需要允许(一些)disctritics,因此被迫使用它(并希望最好)。 – RobIII 2012-02-15 17:59:10

+0

'文件必须以UTF-8编码'是我们必须关心的唯一要求? – 2012-02-15 18:19:39

回答

5

不要介意XML文档是如何编码传输的。正确的方式做你想做的事—编码某些非ASCII字符,使他们幸免于难的旅行—是使用XML character references来表示需要如此保存的字符。例如,您

ÐØë 

使用XML character references作为

&#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一起玩。

+0

嗯;我没有考虑XML字符引用(我确实知道它们的存在)。现在我只是好奇他们是否会计数&#x00D0;作为8个字节或1,因为他们告诉我我的测试字符串(见我的其他答案)太长了...将测试... – RobIII 2012-02-15 19:22:12

+0

从文档的角度来看,它是一个单一的字符。一旦正确解析,消费者应该看到一个字符,就像你必须在文档内容中用'<'表示'<'或'&#x003C;'一样。 – 2012-02-15 19:27:48

+0

我明白这是一个单一的字符(从文件的角度来看),但现在我真的很好奇第三方的回应是什么。我害怕他们正在计算字节......噢,他们的问题:P – RobIII 2012-02-15 19:35:05

0

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

+0

那导致了?????字符... – RobIII 2012-02-15 18:08:49

+0

嗯..现在我的头也疼。 “ – 2012-02-15 18:16:57

0

您根本不可能有以UTF-8编码的字符串/字节阵列208,216,235字节序列。

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

否则,如果你的需求,你说 - 只要求准确预计字节数组给定的输入,并创建了自己的自定义序列化(或者自定义编码,也不能肯定是否是可能的)。

+0

”在UTF-8编码的字符串/字节数组中,您无法拥有208,216,235字节序列。“ 这将是因为ASCII的“扩展”部分,对吧?因为“正常”ASCII与UTF-8共享代码点(0-127)。 我并不真的期待制作我自己的序列化或自定义编码,并且应该是* only *解决方案,然后将它解决;我会把问题放在他们的膝上。 – RobIII 2012-02-15 18:04:44

+0

维基百科搜索: [链接](http://en.wikipedia.org/wiki/Extended_ASCII#Multi_byte_character_sets):“这意味着所有字节0x00-0x7F的含义与ASCII中相同。因此,看起来我将不得不放弃对变音符号的支持...... – RobIII 2012-02-15 18:14:38

+0

因为208属于0x80-0x7FF范围,必须使用UTF8编码为2个字节(http://en.wikipedia.org/wiki/UTF- 8)。有效的UTF8字节流不允许2个字节的'11'作为最高位彼此跟随。 – 2012-02-15 18:21:46

0

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

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

便利地,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的允许地方使用它们。它会对你施加限制,以确保它不会被迫丢失数据(因为存在不允许的字符)。

编辑:这是你提议的解决方法。我在使用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;)。

+0

不幸的是:没有。例如:姓氏(也)限制为70个字符。我发送了测试字符串“ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz&'!<>ÀÁÂÄÆÇÇÈÉÊËÌ”,导致第三方响应:姓氏太长:83字节,应该是70.当然,它包含变音符号。但我也明确地告诉区分符号可以在姓氏字段中... – RobIII 2012-02-15 19:03:32

+0

但我也明确地告诉区分符号*在*姓氏字段中是允许的......因此我开始怀疑自己的知识,但似乎结果我一直没有错。 你可能是正确的,他们需要与传统的东西接口;我已经使用了正则表达式(0x00-0x7F),但是已经删除了它们,因为我必须处理像“DuPré”这样的名称,并且他们明确告诉我变音符号不会成为问题。但现在他们告诉我测试字符串太长,它不是:它正好是70个字符长,但83个字节。 – RobIII 2012-02-15 19:13:21

+0

尝试在我的更新中给出的代码;它会将Latin1字符(例如'é')编码为一个字节。 – Douglas 2012-02-15 19:26:19

-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; 
    } 
0

我明白这是2个独立的要求:

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

2)城市名称仅限于ISOLatin1。

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