如何从字符串中删除无效的代码点?

时间:2012-01-07 03:25:51

标签: c# unicode

我有一个需要提供规范化字符串的例程。但是,进入的数据不一定是干净的,如果字符串包含无效的代码点,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是否对应一个有效的代码点。有没有这样做的功能?如果没有,是否有一个无效范围列表在那里漂浮?

4 个答案:

答案 0 :(得分:8)

似乎唯一的方法是“手动”,就像你已经完成的那样。这是一个与您的结果相同的版本,但速度要快一些(约为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
}

答案 1 :(得分:3)

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

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

根据这些结果,我创建了以下功能:

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

答案 2 :(得分:0)

在引用C#中的有效/无效代码点列表时,

http://msdn.microsoft.com/en-us/library/system.char%28v=vs.90%29.aspx应该包含您要查找的信息。至于如何做,我需要一点点来制定正确的答案。该链接应该可以帮助您开始。

答案 3 :(得分:0)

我最喜欢Regex的方法

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