Как удалить неверные кодовые точки из строки? - PullRequest
10 голосов
/ 07 января 2012

У меня есть подпрограмма, которая должна быть снабжена нормализованными строками.Однако поступающие данные не обязательно являются чистыми, и 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, чтобы я мог быстро перебрать его и увидетьесли каждое слово соответствует правильной кодовой точкеЕсть ли функция, которая сделает это?Если нет, то существует ли список недопустимых диапазонов?

Ответы [ 4 ]

8 голосов
/ 12 января 2012

Кажется, что единственный способ сделать это - «вручную», как вы сделали. Вот версия, которая дает те же результаты, что и ваша, но немного быстрее (примерно в 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
}
3 голосов
/ 09 января 2012

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

Я не смог найти простой в использовании список допустимых диапазонов в пространстве 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);
}

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

edit: Вот гораздо более краткая версия IsValidCodePoint ():

private static bool IsValidCodePoint(UInt32 point)
{
    return point < 0xfdd0
        || (point >= 0xfdf0 
            && ((point & 0xffff) != 0xffff) 
            && ((point & 0xfffe) != 0xfffe)
            && point <= 0x10ffff
        );
}
0 голосов
/ 14 июня 2014

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

public static string StripInvalidUnicodeCharacters(string str)
{
    var invalidCharactersRegex = new Regex("([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])");
    return invalidCharactersRegex.Replace(str, "");
}
0 голосов
/ 07 января 2012

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...