【问题标题】:How to remove invalid code points from a string?如何从字符串中删除无效的代码点?
【发布时间】:2012-02-04 17:30:33
【问题描述】:

我有一个需要提供标准化字符串的例程。但是,传入的数据不一定是干净的,如果字符串包含无效的代码点,则 String.Normalize() 会引发 ArgumentException。

我想做的只是用一次性字符(例如“?”)替换这些代码点。但要做到这一点,我需要一种有效的方法来搜索字符串以首先找到它们。有什么好的方法吗?

以下代码有效,但它基本上使用 try/catch 作为粗略的 if 语句,因此性能很糟糕。我只是分享它来说明我正在寻找的行为:

private static string ReplaceInvalidCodePoints(string aString, string replacement)
{
    var builder = new StringBuilder(aString.Length);
    var enumerator = StringInfo.GetTextElementEnumerator(aString);

    while (enumerator.MoveNext())
    {
        string nextElement;
        try { nextElement = enumerator.GetTextElement().Normalize(); }
        catch (ArgumentException) { nextElement = replacement; }
        builder.Append(nextElement);
    }

    return builder.ToString();
}

(编辑:)我正在考虑将文本转换为 UTF-32,以便我可以快速迭代它并查看每个 dword 是否对应于一个有效的代码点。有没有可以做到这一点的功能?如果没有,是否有一个无效范围列表?

【问题讨论】:

  • 请注意,由于代理对,不可能简单地查看任意的DWORD 并判断它是否是有效的代码点。
  • UTF-32 不使用代理对。
  • 您是如何收到这些不良数据的?如果您使用 Encoding 类阅读它,则默认情况下应删除这些字符。
  • 与@Porges 的问题相关,如果是您的代码从源(例如文件、网络、大型数据库字段)创建字符串,那么可以将逻辑进一步向下推并处理早期阶段,吞吐量可能更高。
  • 谢谢。它已经尽可能地向前推进,与框架接口相抵触。这让我无法完全控制字符串的出处,所以我无法逃避可能会出现糟糕输入的计划。

标签: c# unicode


【解决方案1】:

http://msdn.microsoft.com/en-us/library/system.char%28v=vs.90%29.aspx 应该具有您在引用 C# 中有效/无效代码点列表时要查找的信息。至于怎么做,我需要一点时间来制定一个正确的回应。不过,该链接应该可以帮助您入门。

【讨论】:

  • 我在这些文档中的任何地方都看不到有效/无效的代码点列表 - 你能指出我们吗?谢谢
  • 查看页面顶部附近显示“备注”的位置并指出:“.NET Framework 使用 Char 结构来表示 Unicode 字符。Unicode 标准识别每个 Unicode具有唯一 21 位标量数字的字符称为代码点,并定义了 UTF-16 编码形式,该形式指定代码点如何编码为一个或多个 16 位值的序列。每个 16 位值的范围从十六进制0x0000 到 0xFFFF,存储在 Char 结构中。Char 对象的值是它的 16 位数字(序数)值。"
  • 好的,但这里的问题是String.Normalise 拒绝范围 0xfdd0-ef 和 0xfffe-f 作为无效代码点。这就是我们想要的信息,我在 System.Char 页面上看不到。
【解决方案2】:

我继续使用编辑中提示的解决方案。

我在 Unicode 空间中找不到易于使用的有效范围列表;即使是官方的 Unicode 字符数据库也需要进行比我真正想要处理的更多的解析。因此,我编写了一个快速脚本来循环 [0x0, 0x10FFFF] 范围内的每个数字,使用Encoding.UTF32.GetString(BitConverter.GetBytes(code)) 将其转换为string,然后尝试.Normalize()ing 结果。如果引发异常,则该值不是有效的代码点。

根据这些结果,我创建了以下函数:

bool IsValidCodePoint(UInt32 point)
{
    return (point >= 0x0 && point <= 0xfdcf)
        || (point >= 0xfdf0 && point <= 0xfffd)
        || (point >= 0x10000 && point <= 0x1fffd)
        || (point >= 0x20000 && point <= 0x2fffd)
        || (point >= 0x30000 && point <= 0x3fffd)
        || (point >= 0x40000 && point <= 0x4fffd)
        || (point >= 0x50000 && point <= 0x5fffd)
        || (point >= 0x60000 && point <= 0x6fffd)
        || (point >= 0x70000 && point <= 0x7fffd)
        || (point >= 0x80000 && point <= 0x8fffd)
        || (point >= 0x90000 && point <= 0x9fffd)
        || (point >= 0xa0000 && point <= 0xafffd)
        || (point >= 0xb0000 && point <= 0xbfffd)
        || (point >= 0xc0000 && point <= 0xcfffd)
        || (point >= 0xd0000 && point <= 0xdfffd)
        || (point >= 0xe0000 && point <= 0xefffd)
        || (point >= 0xf0000 && point <= 0xffffd)
        || (point >= 0x100000 && point <= 0x10fffd);
}

请注意,此功能不一定适用于通用清理,具体取决于您的需要。它不排除未分配或保留的代码点,只是专门指定为“非字符”的代码点(编辑:以及 Normalize() 似乎阻塞的其他一些代码点,例如 0xfffff)。但是,这些似乎是唯一会导致 IsNormalized()Normalize() 引发异常的代码点,所以这对我来说没问题。

之后,只需将字符串转换为 UTF-32 并进行梳理即可。由于Encoding.GetBytes() 返回一个字节数组并且IsValidCodePoint() 需要一个UInt32,我使用了一个不安全的块和一些强制转换来弥合差距:

unsafe string ReplaceInvalidCodePoints(string aString, char replacement)
{
    if (char.IsHighSurrogate(replacement) || char.IsLowSurrogate(replacement))
        throw new ArgumentException("Replacement cannot be a surrogate", "replacement");

    byte[] utf32String = Encoding.UTF32.GetBytes(aString);

    fixed (byte* d = utf32String)
    fixed (byte* s = Encoding.UTF32.GetBytes(new[] { replacement }))
    {
        var data = (UInt32*)d;
        var substitute = *(UInt32*)s;

        for(var p = data; p < data + ((utf32String.Length) / sizeof(UInt32)); p++)
        {
            if (!(IsValidCodePoint(*p))) *p = substitute;
        }
    }

    return Encoding.UTF32.GetString(utf32String);
}

相对而言,性能很好 - 比问题中发布的示例快几个数量级。将数据保留在 UTF-16 中可能会更快,内存效率更高,但代价是需要大量额外的代码来处理代理。当然,replacementchar 意味着替换字符必须在 BMP 上。

编辑:这是一个更简洁的 IsValidCodePoint() 版本:

private static bool IsValidCodePoint(UInt32 point)
{
    return point < 0xfdd0
        || (point >= 0xfdf0 
            && ((point & 0xffff) != 0xffff) 
            && ((point & 0xfffe) != 0xfffe)
            && point <= 0x10ffff
        );
}

【讨论】:

  • 对于未知字符有一个指定的代码点,您应该替换它,至少作为默认替换字符; U+FFFD。
  • 为了它的价值,你不需要不安全的代码;您可以使用BitConverter.ToUInt32 将数组中的字节转换为UInt32s。
  • 是的,但这会创建另一个数据副本。
  • “我在 Unicode 空间中找不到易于使用的有效范围列表”。您想在 Unicode 词汇表中查找非字符,或者进行更详细(和有趣)的探索,规范的 §16.7 unicode.org/versions/Unicode6.0.0/ch16.pdf
  • isValidCodePoint() 的编辑简洁版本中,((point &amp; 0xffff) != 0xffff) 不是必需的,因为((point &amp; 0xfffe) != 0xfffe) 可以同时捕获0xFFFE0xFFFF
【解决方案3】:

似乎唯一的方法就是像你所做的那样“手动”。这是一个与您的结果相同的版本,但速度更快(大约是所有 charschar.MaxValue 的字符串的 4 倍,直到 U+10FFFF 的改进较少)并且不需要 unsafe代码。我还简化并评论了我的 IsCharacter 方法来解释每个选择:

static string ReplaceNonCharacters(string aString, char replacement)
{
    var sb = new StringBuilder(aString.Length);
    for (var i = 0; i < aString.Length; i++)
    {
        if (char.IsSurrogatePair(aString, i))
        {
            int c = char.ConvertToUtf32(aString, i);
            i++;
            if (IsCharacter(c))
                sb.Append(char.ConvertFromUtf32(c));
            else
                sb.Append(replacement);
        }
        else
        {
            char c = aString[i];
            if (IsCharacter(c))
                sb.Append(c);
            else
                sb.Append(replacement);
        }
    }
    return sb.ToString();
}

static bool IsCharacter(int point)
{
    return point < 0xFDD0 || // everything below here is fine
        point > 0xFDEF &&    // exclude the 0xFFD0...0xFDEF non-characters
        (point & 0xfffE) != 0xFFFE; // exclude all other non-characters
}

【讨论】:

  • 我刚试过这个。输出与输入、无效点和所有内容相同。
  • 刚刚做了一些测试。似乎 UTF-16 编码替换了损坏的代码点,但不处理“非字符”。有趣!
  • 问题不是代理项损坏,而是定义为非字符的完整代码点。例如,U+FFFF。
  • 我的数据看到了类似的性能提升。谢谢!
  • 你有一个错误,因为它是非字符的 xxFFFF 和 xxFFFE。当你说 U+FFFE 不是非字符时,你抓住了这一点,但评论错误,尽管它是,并且你明确地允许 U+FFEF,但不应该这样做。您可以将它们组合到同一个检查中,因为两者都与测试 (point &amp; 0xFFFE) == 0xFFFE 匹配,并且没有其他点。
【解决方案4】:

我最喜欢 Regex 方法

public static string StripInvalidUnicodeCharacters(string str)
{
    var invalidCharactersRegex = new Regex("([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])");
    return invalidCharactersRegex.Replace(str, "");
}

【讨论】:

  • 自从我第一次提出这个问题以来,我已经完全不再使用正则表达式来处理这些类型的字符剥离工作。使用正则表达式可以节省一些击键次数,但在实践中,它的可读性较差、难以调试且性能较差。
  • @SeanU 这是一个有效的观点。我提供正则表达式解决方案只是为了完整性。
猜你喜欢
  • 1970-01-01
  • 2017-02-02
  • 2018-06-06
  • 2011-02-09
  • 2016-02-16
  • 1970-01-01
  • 2013-12-22
  • 1970-01-01
  • 2023-01-10
相关资源
最近更新 更多