我有一个需要提供规范化字符串的例程。但是,进入的数据不一定是干净的,如果字符串包含无效的代码点,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是否对应一个有效的代码点。有没有这样做的功能?如果没有,是否有一个无效范围列表在那里漂浮?
答案 0 :(得分:8)
似乎唯一的方法是“手动”,就像你已经完成的那样。这是一个与您的结果相同的版本,但速度要快一些(约为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
}
答案 1 :(得分:3)
我继续使用编辑中暗示的解决方案。
我在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);
}
请注意,根据您的需要,此功能不一定适用于通用清理。它不排除未分配或保留的代码点,只是那些被专门指定为“非字符”的代码点(编辑:以及其他一些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
);
}
答案 2 :(得分:0)
http://msdn.microsoft.com/en-us/library/system.char%28v=vs.90%29.aspx应该包含您要查找的信息。至于如何做,我需要一点点来制定正确的答案。该链接应该可以帮助您开始。
答案 3 :(得分:0)
我最喜欢Regex的方法
public static string StripInvalidUnicodeCharacters(string str)
{
var invalidCharactersRegex = new Regex("([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])");
return invalidCharactersRegex.Replace(str, "");
}