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

本文关键字:无效 代码 删除 字符串 | 更新日期: 2023-09-27 18:19:50

我有一个例程,需要提供规范化的字符串。但是,传入的数据并不一定是干净的,如果字符串包含无效的代码点,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是否对应于一个有效的代码点。有什么函数可以做到这一点吗?如果没有,是否有一个无效范围的列表?

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

似乎唯一的方法是像您所做的那样"手动"。这是一个与您的结果相同的版本,但速度稍快(在所有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
}

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

我在Unicode空间中找不到一个易于使用的有效范围列表;即使是官方的Unicode字符数据库也需要比我真正想处理的更多的解析。因此,我编写了一个快速脚本,对[0x0,0x10FFFF]范围内的每个数字进行循环,使用Encoding.UTF32.GetString(BitConverter.GetBytes(code))将其转换为string,然后尝试.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
        );
}

http://msdn.microsoft.com/en-us/library/system.char%28v=vs.90%29.aspx应该有您在引用C#中的有效/无效代码点列表时所要查找的信息。至于如何做,我需要一点时间才能做出正确的回应。不过,这个链接应该可以帮助你开始。

我最喜欢Regex方法

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

如果您使用.Net core3+,这里有一个简单的方法。

public string FixInvalidCodePoints(string s)
{
    return string.Join(string.Empty, s.EnumerateRunes().Select(r => r.ToString()));
}

无效的代理项对将替换为Rune.ReplacementChar,即U+FFFD'�'.

示例:

FixInvalidCodePoints("Hello'ud800world!"); //returns "Hello�world!"

如果您愿意,可以很容易地删除这些替换字符。