Как удалить недопустимые кодовые точки из строки?
У меня есть программа, которая должна быть снабжена нормированными строками. Однако данные, которые поступают, не обязательно чисты, а 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, "");
}