Как удалить недопустимые кодовые точки из строки?

У меня есть программа, которая должна быть снабжена нормированными строками. Однако данные, которые поступают, не обязательно чисты, а String.Normalize() вызывает ArgumentException, если строка содержит недопустимые кодовые точки.

То, что я хотел бы сделать, - это просто заменить эти кодовые точки наброски, такие как "?". Но для этого мне нужен эффективный способ поиска строки, чтобы найти их в первую очередь. Что это хороший способ сделать это?

Следующий код работает, но в основном использует try/catch как грубый if-statement, поэтому производительность ужасна. Я просто использую его, чтобы проиллюстрировать поведение, которое я ищу:

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

(edit:) Я собираюсь преобразовать текст в UTF-32, чтобы я мог быстро перебирать его и видеть, соответствует ли каждый dword действительной кодовой точке. Есть ли функция, которая сделает это? Если нет, есть ли список недействительных диапазонов, плавающих вокруг?

Ответ 1

Кажется, что единственный способ сделать это - "вручную", как вы это сделали. Здесь версия, которая дает те же результаты, что и у вас, но немного быстрее (примерно в 4 раза по всей строке chars до char.MaxValue, меньше улучшения до 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
}

Ответ 2

Я продолжил решение, намеченное в редактировании.

Я не смог найти простой в использовании список допустимых диапазонов в пространстве Unicode; даже официальная база данных символов Юникода собиралась взять больше парсинга, чем я действительно хотел иметь дело. Поэтому вместо этого я написал быстрый script цикл для каждого числа в диапазоне [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);
}

Обратите внимание, что эта функция не всегда подходит для очистки общего назначения в зависимости от ваших потребностей. Он не исключает неназначенные или зарезервированные кодовые точки, только те, которые специально обозначены как "нехарактерные" (edit: и некоторые другие, которые, как кажется, затухают в 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, по-видимому, был бы более быстрым и более эффективным с точки зрения памяти, но ценой большого количества дополнительного кода для работы с суррогатами. И, конечно, replacement be char означает, что символ замещения должен быть в BMP.

edit: Здесь представлена ​​более сжатая версия IsValidCodePoint():

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

Ответ 3

http://msdn.microsoft.com/en-us/library/system.char%28v=vs.90%29.aspx должна иметь информацию, которую вы ищете, когда ссылаетесь на список допустимых/недействительных кодовых точек на С#. Что касается того, как это сделать, мне потребуется немного времени, чтобы сформулировать правильный ответ. Эта ссылка должна помочь вам начать работу.

Ответ 4

Мне нравится Regex подход наиболее

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