从字符串中删除特殊字符的最有效方法
Posted
技术标签:
【中文标题】从字符串中删除特殊字符的最有效方法【英文标题】:Most efficient way to remove special characters from string 【发布时间】:2010-11-10 08:23:38 【问题描述】:我想从字符串中删除所有特殊字符。允许的字符是 A-Z(大写或小写)、数字 (0-9)、下划线 (_) 或点号 (.)。
我有以下,它有效,但我怀疑(我知道!)它不是很有效:
public static string RemoveSpecialCharacters(string str)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < str.Length; i++)
if ((str[i] >= '0' && str[i] <= '9')
|| (str[i] >= 'A' && str[i] <= 'z'
|| (str[i] == '.' || str[i] == '_')))
sb.Append(str[i]);
return sb.ToString();
最有效的方法是什么?正则表达式会是什么样子,它与普通字符串操作相比如何?
要清理的字符串会很短,通常在 10 到 30 个字符之间。
【问题讨论】:
我不会把它放在答案中,因为它不会更有效,但是有许多静态 char 方法,如 char.IsLetterOrDigit() 可以在 if 语句中使用至少让它更清晰。 我不确定检查 A 到 z 是否安全,因为它会输入 6 个非字母字符,只需要其中一个字符(下划线)。 专注于使您的代码更具可读性。除非您以每秒 500 次的循环执行此操作,否则效率并不是什么大问题。使用正则表达式,它会更容易阅读。l 拜伦,您需要强调可读性可能是对的。但是,我对正则表达式的可读性持怀疑态度。 :-) 正则表达式是否可读有点像德语是否可读;这取决于您是否知道(尽管在这两种情况下,您都会不时遇到毫无意义的语法规则;) 【参考方案1】:为什么你认为你的方法效率不高?这实际上是您可以做到的最有效的方法之一。
您当然应该将字符读入局部变量或使用枚举器来减少数组访问次数:
public static string RemoveSpecialCharacters(this string str)
StringBuilder sb = new StringBuilder();
foreach (char c in str)
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '.' || c == '_')
sb.Append(c);
return sb.ToString();
使这种方法高效的一个原因是它可以很好地扩展。执行时间将与字符串的长度相关。如果您将它用于大字符串,也不会令人讨厌。
编辑: 我做了一个快速的性能测试,用 24 个字符的字符串运行每个函数一百万次。结果如下:
原始函数:54.5 毫秒。 我建议的更改:47.1 毫秒。 设置 StringBuilder 容量的矿井:43.3 毫秒。 正则表达式:294.4 毫秒。
编辑 2: 我在上面的代码中添加了 A-Z 和 a-z 之间的区别。 (我重新进行了性能测试,没有明显差异。)
编辑 3: 我测试了lookup+char[]的方案,运行时间大约是13毫秒。
当然,要付出的代价是初始化庞大的查找表并将其保存在内存中。嗯,数据不多,但对于这样一个微不足道的功能来说,它就足够了……
private static bool[] _lookup;
static Program()
_lookup = new bool[65536];
for (char c = '0'; c <= '9'; c++) _lookup[c] = true;
for (char c = 'A'; c <= 'Z'; c++) _lookup[c] = true;
for (char c = 'a'; c <= 'z'; c++) _lookup[c] = true;
_lookup['.'] = true;
_lookup['_'] = true;
public static string RemoveSpecialCharacters(string str)
char[] buffer = new char[str.Length];
int index = 0;
foreach (char c in str)
if (_lookup[c])
buffer[index] = c;
index++;
return new string(buffer, 0, index);
【讨论】:
我同意。我要做的唯一其他更改是将初始容量参数添加到 StringBuilder 构造函数“= new StringBuilder(str.Length)”。 根据我的测试,我的答案是使用char[]
缓冲区而不是StringBuilder
,在这个问题上略有优势。 (虽然我的可读性较差,所以性能上的小小好处可能不值得。)
@Steven:很可能是这样,但基准不言自明!在我的测试中,使用char[]
缓冲区的性能(略)优于StringBuilder
,即使在扩展到数万个字符长度的字符串时也是如此。
@downvoter:为什么要投反对票?如果你不解释你认为错误的地方,它就无法改进答案。
@SILENT:不,它没有,但你应该只这样做一次。如果每次调用该方法时分配一个那么大的数组(并且如果您频繁调用该方法),那么该方法将成为迄今为止最慢的方法,并为垃圾收集器带来大量工作。【参考方案2】:
好吧,除非你真的需要从你的函数中挤出性能,否则就选择最容易维护和理解的东西。正则表达式如下所示:
为了提高性能,您可以预编译它,也可以告诉它在第一次调用时编译(后续调用会更快。)
public static string RemoveSpecialCharacters(string str)
return Regex.Replace(str, "[^a-zA-Z0-9_.]+", "", RegexOptions.Compiled);
【讨论】:
我猜这可能是一个足够复杂的查询,它会比 OP 的方法更快,尤其是在预编译的情况下。然而,我没有证据支持这一点。应该对其进行测试。除非它非常慢,否则无论如何我都会选择这种方法,因为它更容易阅读和维护。 +1 它是一个非常简单的正则表达式(没有回溯或任何复杂的东西),所以它应该非常快。 @rmeador:如果不编译,它的速度大约慢 5 倍,编译速度比他的方法慢 3 倍。虽然仍然简单 10 倍:-D 正则表达式不是神奇的锤子,也永远不会比手工优化的代码快。 对于那些记得 Knuth 关于优化的名言的人,这就是开始的地方。然后,如果您发现需要额外的千分之一毫秒的性能,请使用其他技术之一。【参考方案3】:我建议创建一个简单的查找表,您可以在静态构造函数中对其进行初始化,以将任意字符组合设置为有效。这让您可以进行快速、单一的检查。
编辑
另外,为了速度,您需要将 StringBuilder 的容量初始化为输入字符串的长度。这将避免重新分配。将这两种方法结合使用将为您提供速度和灵活性。
另一个编辑
我认为编译器可能会对其进行优化,但考虑到样式和效率,我建议使用 foreach 而不是 for。
【讨论】:
对于数组,for
和 foreach
产生类似的代码。我不知道字符串虽然。我怀疑 JIT 是否知道 String 的类数组性质。
我敢打赌,JIT 比您的 [笑话删除] 更了解字符串的类似数组的性质。 Anders etal 做了很多工作来优化 .net 中关于字符串的所有内容
我已经使用 HashSetpublic static string RemoveSpecialCharacters(string str)
char[] buffer = new char[str.Length];
int idx = 0;
foreach (char c in str)
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z')
|| (c >= 'a' && c <= 'z') || (c == '.') || (c == '_'))
buffer[idx] = c;
idx++;
return new string(buffer, 0, idx);
【讨论】:
+1,经过测试,比 StringBuilder 快 40%。 0.0294 毫秒/字符串与 0.0399 毫秒/字符串 只是为了确定,你的意思是有或没有预分配的StringBuilder? 使用预分配,仍然比 char[] 分配和新字符串慢 40%。 我喜欢这个。我调整了这个方法foreach (char c in input.Where(c => char.IsLetterOrDigit(c) || allowedSpecialCharacters.Any(x => x == c))) buffer[idx++] = c;
【参考方案5】:
正则表达式如下所示:
public string RemoveSpecialChars(string input)
return Regex.Replace(input, @"[^0-9a-zA-Z\._]", string.Empty);
但如果性能非常重要,我建议您在选择“正则表达式路径”之前做一些基准测试...
【讨论】:
【参考方案6】:如果您使用的是动态字符列表,LINQ 可能会提供更快、更优雅的解决方案:
public static string RemoveSpecialCharacters(string value, char[] specialCharacters)
return new String(value.Except(specialCharacters).ToArray());
我将这种方法与之前的两种“快速”方法(发布编译)进行了比较:
LukeH 的字符数组解决方案 - 427 毫秒 StringBuilder 解决方案 - 429 毫秒 LINQ(此答案)- 98 毫秒请注意,该算法稍作修改 - 字符作为数组而不是硬编码传入,这可能会稍微影响一些事情(即/其他解决方案将有一个内部 foror 循环来检查字符数组)。
如果我使用 LINQ where 子句切换到硬编码解决方案,结果是:
字符数组解决方案 - 7 毫秒 StringBuilder 解决方案 - 22 毫秒 LINQ - 60 毫秒如果您打算编写更通用的解决方案,而不是对字符列表进行硬编码,那么可能值得研究 LINQ 或修改后的方法。 LINQ 绝对可以为您提供简洁、易读的代码——甚至比正则表达式还要多。
【讨论】:
这种方法看起来不错,但它不起作用 - except() 是一个集合操作,因此您最终只会得到字符串中每个唯一字符的第一次出现。【参考方案7】:我不相信你的算法是高效的。这是 O(n) 并且只查看每个字符一次。 除非你在检查它们之前神奇地知道值,否则你不会得到比这更好的。
不过,我会将StringBuilder
的容量初始化为字符串的初始大小。我猜您认为性能问题来自内存重新分配。
旁注:检查A
-z
是不安全的。您包括[
、\
、]
、^
、_
和`...
旁注 2:为了提高效率,将比较按顺序排列,以尽量减少比较次数。 (在最坏的情况下,你说的是 8 次比较,所以不要想太多。)这会随着你的预期输入而变化,但一个例子可能是:
if (str[i] >= '0' && str[i] <= 'z' &&
(str[i] >= 'a' || str[i] <= '9' || (str[i] >= 'A' && str[i] <= 'Z') ||
str[i] == '_') || str[i] == '.')
旁注 3:如果出于某种原因您真的需要它快速,则 switch 语句可能会更快。编译器应该为你创建一个跳转表,只产生一个比较:
switch (str[i])
case '0':
case '1':
.
.
.
case '.':
sb.Append(str[i]);
break;
【讨论】:
我同意你不能在这个上击败 O(n)。但是,每次比较的成本可以降低。表查找具有较低的固定成本,而随着您添加更多异常,一系列比较的成本将会增加。 关于旁注3,你真的认为跳转表会比查表快吗? 我对交换机方案进行了快速性能测试,结果与对比结果相同。 @Steven Sudit - 我敢说他们实际上差不多。想进行测试吗? O(n) 符号有时让我很生气。人们会基于算法已经是 O(n) 的事实做出愚蠢的假设。如果我们更改此例程以将 str[i] 调用替换为一个函数,该函数通过与世界另一端的服务器构建一次性 SSL 连接来检索比较值......你肯定会看到巨大的性能差异,算法仍然是 O(n)。每种算法的 O(1) 成本都很重要,而且不等价!【参考方案8】:你可以使用正则表达式如下:
return Regex.Replace(strIn, @"[^\w\.@-]", "", RegexOptions.None, TimeSpan.FromSeconds(1.0));
【讨论】:
【参考方案9】:StringBuilder sb = new StringBuilder();
for (int i = 0; i < fName.Length; i++)
if (char.IsLetterOrDigit(fName[i]))
sb.Append(fName[i]);
【讨论】:
【参考方案10】:这对我来说似乎很好。我要做的唯一改进是用字符串的长度初始化StringBuilder
。
StringBuilder sb = new StringBuilder(str.Length);
【讨论】:
【参考方案11】:我同意此代码示例。唯一不同的是我把它变成了字符串类型的扩展方法。这样您就可以在非常简单的行或代码中使用它:
string test = "abc@#$123";
test.RemoveSpecialCharacters();
感谢 Guffa 的实验。
public static class MethodExtensionHelper
public static string RemoveSpecialCharacters(this string str)
StringBuilder sb = new StringBuilder();
foreach (char c in str)
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_')
sb.Append(c);
return sb.ToString();
【讨论】:
【参考方案12】:我会使用字符串替换和正则表达式搜索“特殊字符”,将找到的所有字符替换为空字符串。
【讨论】:
+1 代码肯定更少,并且可以说更易读,忽略一次写入正则表达式。 @kenny - 我同意。最初的问题甚至指出字符串很短 - 10-30 个字符。但显然很多人仍然认为我们正在以秒为单位出售 CPU 时间...... Reguler expressin 太懒惰了,所以不要一直用。【参考方案13】:我不得不为工作做一些类似的事情,但在我的情况下,我必须过滤所有不是字母、数字或空格的东西(但您可以根据需要轻松修改它)。 过滤是在 javascript 中的客户端完成的,但出于安全原因,我也在服务器端进行过滤。由于我可以预期大多数字符串都是干净的,因此除非我真的需要,否则我想避免复制字符串。这让我可以实现下面的实现,它应该对干净和脏字符串都有更好的表现。
public static string EnsureOnlyLetterDigitOrWhiteSpace(string input)
StringBuilder cleanedInput = null;
for (var i = 0; i < input.Length; ++i)
var currentChar = input[i];
var charIsValid = char.IsLetterOrDigit(currentChar) || char.IsWhiteSpace(currentChar);
if (charIsValid)
if(cleanedInput != null)
cleanedInput.Append(currentChar);
else
if (cleanedInput != null) continue;
cleanedInput = new StringBuilder();
if (i > 0)
cleanedInput.Append(input.Substring(0, i));
return cleanedInput == null ? input : cleanedInput.ToString();
【讨论】:
【参考方案14】:这里有很多建议的解决方案,有些比其他的更有效,但可能不是很可读。这可能不是最有效的,但肯定适用于大多数情况,并且非常简洁易读,利用了 Linq:
string stringToclean = "This is a test. Do not try this at home; you might get hurt. Don't believe it?";
var validPunctuation = new HashSet<char>(". -");
var cleanedVersion = new String(stringToclean.Where(x => (x >= 'A' && x <= 'Z') || (x >= 'a' && x <= 'z') || validPunctuation.Contains(x)).ToArray());
var cleanedLowercaseVersion = new String(stringToclean.ToLower().Where(x => (x >= 'a' && x <= 'z') || validPunctuation.Contains(x)).ToArray());
【讨论】:
【参考方案15】:对于 S&G 的,Linq-ified 方式:
var original = "(*^%foo)(@)&^@#><>?:\":';=-+_";
var valid = new char[]
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D',
'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8',
'9', '0', '.', '_' ;
var result = string.Join("",
(from x in original.ToCharArray()
where valid.Contains(x) select x.ToString())
.ToArray());
不过,我认为这不会是最有效的方法。
【讨论】:
不是,因为它是线性搜索。【参考方案16】:public string RemoveSpecial(string evalstr)
StringBuilder finalstr = new StringBuilder();
foreach(char c in evalstr)
int charassci = Convert.ToInt16(c);
if (!(charassci >= 33 && charassci <= 47))// special char ???
finalstr.append(c);
return finalstr.ToString();
【讨论】:
【参考方案17】:用途:
s.erase(std::remove_if(s.begin(), s.end(), my_predicate), s.end());
bool my_predicate(char c)
return !(isalpha(c) || c=='_' || c==' '); // depending on you definition of special characters
你会得到一个干净的字符串s
。
erase()
将去除所有特殊字符,并通过 my_predicate()
函数高度自定义。
【讨论】:
【参考方案18】:HashSet 是 O(1) 不知道是不是比现有的比较快
private static HashSet<char> ValidChars = new HashSet<char>() 'a', 'b', 'c', 'A', 'B', 'C', '1', '2', '3', '_' ;
public static string RemoveSpecialCharacters(string str)
StringBuilder sb = new StringBuilder(str.Length / 2);
foreach (char c in str)
if (ValidChars.Contains(c)) sb.Append(c);
return sb.ToString();
我进行了测试,这并不比接受的答案快。 我会保留它,就好像您需要一组可配置的字符一样,这将是一个很好的解决方案。
【讨论】:
为什么你认为比较不是O(1)? @Guffa 我不确定不是,我删除了我的评论。和+1。在发表评论之前我应该做更多的测试。【参考方案19】:我想知道基于正则表达式的替换(可能已编译)是否更快。 必须测试一下有人发现这要慢约 5 倍。
除此之外,您应该使用预期长度初始化 StringBuilder,这样中间字符串在增长时不必被复制。
一个好的数字是原始字符串的长度,或者稍短一些(取决于函数输入的性质)。
最后,您可以使用查找表(范围为 0..127)来确定是否要接受一个字符。
【讨论】:
一个正则表达式已经测试过了,它慢了大约五倍。对于 0..127 范围内的查找表,您仍然需要在使用查找表之前对字符代码进行范围检查,因为字符是 16 位值,而不是 7 位值。 @Guffa Err... 是吗? ;)【参考方案20】:以下代码有如下输出(结论是我们还可以通过分配更小的数组来节省一些内存资源):
lookup = new bool[123];
for (var c = '0'; c <= '9'; c++)
lookup[c] = true; System.Diagnostics.Debug.WriteLine((int)c + ": " + (char)c);
for (var c = 'A'; c <= 'Z'; c++)
lookup[c] = true; System.Diagnostics.Debug.WriteLine((int)c + ": " + (char)c);
for (var c = 'a'; c <= 'z'; c++)
lookup[c] = true; System.Diagnostics.Debug.WriteLine((int)c + ": " + (char)c);
48: 0
49: 1
50: 2
51: 3
52: 4
53: 5
54: 6
55: 7
56: 8
57: 9
65: A
66: B
67: C
68: D
69: E
70: F
71: G
72: H
73: I
74: J
75: K
76: L
77: M
78: N
79: O
80: P
81: Q
82: R
83: S
84: T
85: U
86: V
87: W
88: X
89: Y
90: Z
97: a
98: b
99: c
100: d
101: e
102: f
103: g
104: h
105: i
106: j
107: k
108: l
109: m
110: n
111: o
112: p
113: q
114: r
115: s
116: t
117: u
118: v
119: w
120: x
121: y
122: z
您还可以添加以下代码行以支持俄语语言环境(数组大小为 1104):
for (var c = 'А'; c <= 'Я'; c++)
lookup[c] = true; System.Diagnostics.Debug.WriteLine((int)c + ": " + (char)c);
for (var c = 'а'; c <= 'я'; c++)
lookup[c] = true; System.Diagnostics.Debug.WriteLine((int)c + ": " + (char)c);
【讨论】:
【参考方案21】:我不确定这是最有效的方法,但它对我有用
Public Function RemoverTildes(stIn As String) As String
Dim stFormD As String = stIn.Normalize(NormalizationForm.FormD)
Dim sb As New StringBuilder()
For ich As Integer = 0 To stFormD.Length - 1
Dim uc As UnicodeCategory = CharUnicodeInfo.GetUnicodeCategory(stFormD(ich))
If uc <> UnicodeCategory.NonSpacingMark Then
sb.Append(stFormD(ich))
End If
Next
Return (sb.ToString().Normalize(NormalizationForm.FormC))
End Function
【讨论】:
答案确实有效,但问题是针对C#。(PS:我知道这几乎是五年前的事了,但仍然...... ) 我使用 Telerik VB 到 C# 转换器,(反之亦然)并且代码工作得很好 - 不过不确定其他人。 (另一件事,converter.telerik.com)【参考方案22】:最短的路只有 3 行...
public static string RemoveSpecialCharacters(string str)
var sb = new StringBuilder();
foreach (var c in str.Where(c => c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c == '.' || c == '_')) sb.Append(c);
return sb.ToString();
【讨论】:
【参考方案23】:LINQ
的简单方法
string text = "123a22 ";
var newText = String.Join(string.Empty, text.Where(x => x != 'a'));
【讨论】:
【参考方案24】:另一种尝试通过减少分配来提高性能的方法,尤其是在多次调用此函数时。
它之所以有效,是因为您可以保证结果不会比输入长,因此可以传递输入和输出,而无需在内存中创建额外的副本。由于这个原因,您不能使用 stackalloc
创建缓冲区数组,因为这需要从缓冲区中复制。
public static string RemoveSpecialCharacters(this string str)
return RemoveSpecialCharacters(str.AsSpan()).ToString();
public static ReadOnlySpan<char> RemoveSpecialCharacters(this ReadOnlySpan<char> str)
Span<char> buffer = new char[str.Length];
int idx = 0;
foreach (char c in str)
if (char.IsLetterOrDigit(c))
buffer[idx] = c;
idx++;
return buffer.Slice(0, idx);
【讨论】:
【参考方案25】:public static string RemoveAllSpecialCharacters(this string text)
if (string.IsNullOrEmpty(text))
return text;
string result = Regex.Replace(text, "[:!@#$%^&*()|\":?><\\[\\]\\;'/.,~]", " ");
return result;
【讨论】:
答案错误。如果你要使用正则表达式,它应该是包容性的,而不是排他性的,因为你现在错过了一些字符。实际上,正则表达式已经有了答案。并且要完整 - 正则表达式比较慢,然后直接比较字符函数。【参考方案26】:如果您担心速度,请使用指针来编辑现有字符串。您可以固定字符串并获取指向它的指针,然后对每个字符运行 for 循环,用替换字符覆盖每个无效字符。这将非常有效,并且不需要分配任何新的字符串内存。您还需要使用 unsafe 选项编译模块,并在方法头中添加“unsafe”修饰符以使用指针。
static void Main(string[] args)
string str = "string!$%with^&*invalid!!characters";
Console.WriteLine( str ); //print original string
FixMyString( str, ' ' );
Console.WriteLine( str ); //print string again to verify that it has been modified
Console.ReadLine(); //pause to leave command prompt open
public static unsafe void FixMyString( string str, char replacement_char )
fixed (char* p_str = str)
char* c = p_str; //temp pointer, since p_str is read-only
for (int i = 0; i < str.Length; i++, c++) //loop through each character in string, advancing the character pointer as well
if (!IsValidChar(*c)) //check whether the current character is invalid
(*c) = replacement_char; //overwrite character in existing string with replacement character
public static bool IsValidChar( char c )
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '.' || c == '_');
//return char.IsLetterOrDigit( c ) || c == '.' || c == '_'; //this may work as well
【讨论】:
Noooooooooo!在 .NET 中更改字符串是 BAAAAAAAAAAAAD!框架中的一切都依赖于字符串是不可变的规则,如果你打破它,你会得到非常令人惊讶的副作用......【参考方案27】:public static string RemoveSpecialCharacters(string str)
return str.replaceAll("[^A-Za-z0-9_\\\\.]", "");
【讨论】:
恐怕replaceAll
不是C#字符串函数,而是Java或JavaScript以上是关于从字符串中删除特殊字符的最有效方法的主要内容,如果未能解决你的问题,请参考以下文章
从Unicode字符串中删除文件名中禁用字符的最有效方法[复制]