【问题标题】:How to decode surrogate characters encoded as UTF8?如何解码编码为 UTF8 的代理字符?
【发布时间】:2016-11-12 14:23:30
【问题描述】:

我的 C# 程序获取一些 UTF-8 编码数据并使用 Encoding.UTF8.GetString(data) 对其进行解码。当产生数据的程序获取 BMP 之外的字符时,它会将它们编码为 2 个代理字符,每个代理字符分别编码为 UTF-8。在这种情况下,我的程序无法正确解码。

如何在 C# 中解码此类数据?

示例:

static void Main(string[] args)
{
    string orig = "????";
    byte[] correctUTF8 = Encoding.UTF8.GetBytes(orig); // Simulate correct conversion using std::codecvt_utf8_utf16<wchar_t>
    Console.WriteLine("correctUTF8: " + BitConverter.ToString(correctUTF8));  // F0-9F-8C-8E - that's what the C++ program should've produced

    // Simulate bad conversion using std::codecvt_utf8<wchar_t> - that's what I get from the program
    byte[] badUTF8 = new byte[] { 0xED, 0xA0, 0xBC, 0xED, 0xBC, 0x8E };
    string badString = Encoding.UTF8.GetString(badUTF8); // ���� (4 * U+FFFD 'REPLACMENT CHARACTER')
    // How can I convert this?
}

注意:编码程序是用C++编写的,使用std::codecvt_utf8&lt;wchar_t&gt;转换数据(代码如下)。正如@PeterDuniho 的回答正确指出的那样,它应该使用std::codecvt_utf8_utf16&lt;wchar_t&gt;。不幸的是,我无法控制此程序,也无法更改其行为 - 只能处理其格式错误的输入。

std::wstring_convert<std::codecvt_utf8<wchar_t>> utf8Converter;
std::string utf8str = utf8Converter.to_bytes(wstr);

【问题讨论】:

  • 我得到的角色是0xD83C 0xDF0E,而不是你声称的0xD83D 0xDF0E。此外,如果我使用 .NET 将该字符编码为 UTF8,我会得到 F0 9F 8C 8E,而不是您声称的 ED A0 BC ED BC 8E。最后,当我将 F0 9F 8C 8E 解码回 C# 字符串时,我得到了我开始使用的 "????",它以 UTF16 编码为原始 0xD83C 0xDF0E,正如预期的那样。请提供一个良好的minimal reproducible example 可靠地重现您的问题。目前,这看起来只不过是您的代码转换为 UTF8 的问题(这看起来根本不像 C#……它似乎是 C++)。
  • 代理代码点不能以 UTF-8(或任何 UTF)编码,因此 Encoding.UTF8.GetString 正确地将无效字节替换为 U+FFFD。你所拥有的看起来像CESU-8
  • @PeterDuniho:字符已更正抱歉。我添加了示例,并澄清我不再控制生产程序。

标签: c# c++ unicode utf-8 surrogate-pairs


【解决方案1】:

没有好的Minimal, Complete, and Verifiable code example 是不可能确定的。但在我看来,您好像在 C++ 中使用了错误的转换器。

std::codecvt_utf8&lt;wchar_t&gt; 语言环境转换自 UCS-2,而不是 UTF-16。两者非常相似,但 UCS-2 不支持对要编码的字符进行编码所需的代理对。

相反,您应该使用std::codecvt_utf8_utf16&lt;wchar_t&gt;

std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> utf8Converter;
std::string utf8str = utf8Converter.to_bytes(wstr);

当我使用该转换器时,我得到了所需的 UTF-8 字节:F0 9F 8C 8E。当然,这些在 .NET 中被解释为 UTF-8 时可以正确解码。


附录:

问题已更新,表明无法更改编码代码。您被编码为无效 UTF8 的 UCS-2 卡住了。由于 UTF8 无效,您必须自己解码文本。

我看到了几种合理的方法来做到这一点。首先,编写一个不关心 UTF8 是否包含无效字节序列的解码器。其次,使用 C++ std::wstring_convert&lt;std::codecvt_utf8&lt;wchar_t&gt;&gt; 转换器为您解码字节(例如,用 C++ 编写接收代码,或者编写可以从 C# 代码调用的 C++ DLL 来完成工作)。

第二个选项在某种意义上更可靠,即您使用的正是最初创建坏数据的解码器。另一方面,即使创建一个 DLL 也可能是矫枉过正,更不用说用 C++ 编写整个客户端了。制作 DLL,即使使用 C++/CLI,您仍然很难让互操作正常工作,除非您已经是专家。

我对 C++/CLI 很熟悉,但几乎不是专家。我更擅长 C#,所以这里是第一个选项的一些代码:

private const int _khighOffset = 0xD800 - (0x10000 >> 10);

/// <summary>
/// Decodes a nominally UTF8 byte sequence as UTF16. Ignores all data errors
/// except those which prevent coherent interpretation of the input data.
/// Input with invalid-but-decodable UTF8 sequences will be decoded without
/// error, and may lead to invalid UTF16.
/// </summary>
/// <param name="bytes">The UTF8 byte sequence to decode</param>
/// <returns>A string value representing the decoded UTF8</returns>
/// <remarks>
/// This method has not been thoroughly validated. It should be tested
/// carefully with a broad range of inputs (the entire UTF16 code point
/// range would not be unreasonable) before being used in any sort of
/// production environment.
/// </remarks>
private static string DecodeUtf8WithOverlong(byte[] bytes)
{
    List<char> result = new List<char>();
    int continuationCount = 0, continuationAccumulator = 0, highBase = 0;
    char continuationBase = '\0';

    for (int i = 0; i < bytes.Length; i++)
    {
        byte b = bytes[i];

        if (b < 0x80)
        {
            result.Add((char)b);
            continue;
        }

        if (b < 0xC0)
        {
            // Byte values in this range are used only as continuation bytes.
            // If we aren't expecting any continuation bytes, then the input
            // is invalid beyond repair.
            if (continuationCount == 0)
            {
                throw new ArgumentException("invalid encoding");
            }

            // Each continuation byte represents 6 bits of the actual
            // character value
            continuationAccumulator <<= 6;
            continuationAccumulator |= (b - 0x80);
            if (--continuationCount == 0)
            {
                continuationAccumulator += highBase;

                if (continuationAccumulator > 0xffff)
                {
                    // Code point requires more than 16 bits, so split into surrogate pair
                    char highSurrogate = (char)(_khighOffset + (continuationAccumulator >> 10)),
                        lowSurrogate = (char)(0xDC00 + (continuationAccumulator & 0x3FF));

                    result.Add(highSurrogate);
                    result.Add(lowSurrogate);
                }
                else
                {
                    result.Add((char)(continuationBase | continuationAccumulator));
                }
                continuationAccumulator = 0;
                continuationBase = '\0';
                highBase = 0;
            }
            continue;
        }

        if (b < 0xE0)
        {
            continuationCount = 1;
            continuationBase = (char)((b - 0xC0) * 0x0040);
            continue;
        }

        if (b < 0xF0)
        {
            continuationCount = 2;
            continuationBase = (char)(b == 0xE0 ? 0x0800 : (b - 0xE0) * 0x1000);
            continue;
        }

        if (b < 0xF8)
        {
            continuationCount = 3;
            highBase = (b - 0xF0) * 0x00040000;
            continue;
        }

        if (b < 0xFC)
        {
            continuationCount = 4;
            highBase = (b - 0xF8) * 0x01000000;
            continue;
        }

        if (b < 0xFE)
        {
            continuationCount = 5;
            highBase = (b - 0xFC) * 0x40000000;
            continue;
        }

        // byte values of 0xFE and 0xFF are invalid
        throw new ArgumentException("invalid encoding");
    }

    return new string(result.ToArray());
}

我用你的地球字符测试了它,它工作得很好。它还为该字符正确解码正确的 UTF8(即F0 9F 8C 8E)。如果您打算使用该代码来解码所有 UTF8 输入,那么您当然会希望使用完整范围的数据对其进行测试。

【讨论】:

  • 谢谢,这确实是生产者程序的正确代码。不幸的是,我无法控制它,所以我正在寻找 C# 消费端的修复程序来补偿这个给定的行为。
  • 查看编辑。我没有费心编写 C++/CLI 解码器,因为那会花费我更长的时间,而且 95% 的时间我都在与与实际问题无关的东西搏斗。 :)
  • 谢谢,这是我一直在寻找的答案,虽然我希望.NET框架或知名库中的现成解码器......我熟悉C++/CLI,但它需要单独对我们的构建系统进行不合理的投资,更不用说额外的 DLL。先生,您是君子、学者、人中的王子!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-05
  • 2016-05-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-07
相关资源
最近更新 更多