如何从字符串中删除无效的代码点
本文关键字:无效 代码 删除 字符串 | 更新日期: 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是否对应于一个有效的代码点。有什么函数可以做到这一点吗?如果没有,是否有一个无效范围的列表?
似乎唯一的方法是像您所做的那样"手动"。这是一个与您的结果相同的版本,但速度稍快(在所有chars
到char.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中可能会更快、更高效地存储,但代价是需要大量额外的代码来处理代理。当然,replacement
是char
意味着替换字符必须在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!"
如果您愿意,可以很容易地删除这些替换字符。