从字符串中删除特殊字符的最有效方法

我想从字符串中删除所有特殊字符。允许输入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个字符之间。

540986 次浏览

我将使用字符串替换为正则表达式搜索“特殊字符”,替换所有字符找到一个空字符串。

我建议创建一个简单的查找表,您可以在静态构造函数中初始化它,以将任何字符组合设置为有效。这让您可以进行快速、单一的检查。

编辑

另外,为了提高速度,您需要将StringBuilder的容量初始化为输入字符串的长度。这将避免重新分配。这两种方法结合起来会给你速度和灵活性。

另一个编辑

我认为编译器可能会优化它,但作为风格和效率的问题,我建议foreach而不是for。

除非您真的需要从函数中挤出性能,否则就使用最容易维护和理解的方法。正则表达式是这样的:

为了获得额外的性能,您可以预先编译它,或者只是告诉它在第一次调用时编译(后续调用将更快)。

public static string RemoveSpecialCharacters(string str)
{
return Regex.Replace(str, "[^a-zA-Z0-9_.]+", "", RegexOptions.Compiled);
}

正则表达式如下所示:

public string RemoveSpecialChars(string input)
{
return Regex.Replace(input, @"[^0-9a-zA-Z\._]", string.Empty);
}

但如果性能非常重要,我建议你在选择“正则表达式路径”之前做一些基准测试……

我觉得不错。我要做的唯一改进是用字符串的长度初始化StringBuilder

StringBuilder sb = new StringBuilder(str.Length);

我觉得你的算法很有效。它是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;
}

你为什么认为你的方法没有效率?这实际上是最有效的方法之一。

当然,你应该将字符读入局部变量或使用枚举器来减少数组访问的次数:

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();
}

使这种方法高效的一个原因是它具有良好的可伸缩性。执行时间将与字符串的长度相关。如果你把它用在一根大弦上,就不会有令人讨厌的意外。

< p >编辑:< br > 我做了一个快速的性能测试,用24个字符串运行每个函数一百万次。结果如下:

原函数:54.5 ms 我建议修改:47.1 ms.
我与设置StringBuilder容量:43.3 ms.
正则表达式:294.4 ms

< p >编辑2: 我在上面的代码中添加了A-Z和A-Z之间的区别。(我重新运行了性能测试,没有明显的差异)

< p >编辑3:< br > 我测试了查找+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);
}

我想知道基于regx的替代品(可能是编译的)是否更快。有人发现这是~5倍慢。

除此之外,您应该使用预期的长度初始化StringBuilder,这样在中间字符串增长时就不必复制它。

一个好的数字是原始字符串的长度,或者稍低一些(取决于函数输入的性质)。

最后,您可以使用一个查找表(范围0..127)来确定一个字符是否被接受。

如果担心速度问题,可以使用指针编辑现有字符串。您可以固定字符串并获取指向它的指针,然后在每个字符上运行for循环,用替换字符覆盖每个无效字符。这将是非常有效的,并且不需要分配任何新的字符串内存。为了使用指针,你还需要用不安全选项来编译你的模块,并在方法头中添加“不安全”修饰符。

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
}
public 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);
}

对于标普,linq化的方式:

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());

然而,我不认为这是最有效的方法。

StringBuilder sb = new StringBuilder();


for (int i = 0; i < fName.Length; i++)
{
if (char.IsLetterOrDigit(fName[i]))
{
sb.Append(fName[i]);
}
}
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();
}

我同意这个代码示例。唯一不同的是,我把它变成字符串类型的扩展方法。所以你可以在非常简单的一行或代码中使用它:

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();
}
}

下面的代码有以下输出(结论是,我们也可以节省一些内存资源分配数组更小的大小):

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);
}

如果你使用的是动态字符列表,LINQ可以提供一个更快更优雅的解决方案:

public static string RemoveSpecialCharacters(string value, char[] specialCharacters)
{
return new String(value.Except(specialCharacters).ToArray());
}

我将这种方法与之前的两种“快速”方法(发行版编译)进行了比较:

  • 字符数组解决方案由LukeH - 427毫秒
  • StringBuilder解决方案- 429毫秒
  • LINQ(这个答案)- 98毫秒

注意,算法略有修改-字符作为数组传入,而不是硬编码,这可能会有轻微的影响(即/其他解决方案将有一个内部for循环来检查字符数组)。

如果我使用LINQ where子句切换到硬编码的解决方案,结果是:

  • 字符数组解决方案- 7ms
  • StringBuilder解决方案- 22ms
  • LINQ - 60毫秒

如果您计划编写一个更通用的解决方案,而不是硬编码字符列表,那么可能值得考虑LINQ或经过修改的方法。LINQ绝对能给你简洁、高可读性的代码——甚至比Regex更好。

使用:

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()函数进行高度自定义。

public static string RemoveSpecialCharacters(string str){
return str.replaceAll("[^A-Za-z0-9_\\\\.]", "");
}
我必须为工作做一些类似的事情,但在我的情况下,我必须过滤所有不是字母,数字或空白(但你可以很容易地修改它到你的需要)。 过滤是用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();
}

哈希集是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();
}

我测试了,这并不比接受的答案快 如果你需要一组可配置的字符,我会把它留在这里,这将是一个很好的解决方案。< / p >

我不确定这是最有效的方法,但对我来说很有效

 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

正则表达式的使用方法如下:

return Regex.Replace(strIn, @"[^\w\.@-]", "", RegexOptions.None, TimeSpan.FromSeconds(1.0));

这里有很多建议的解决方案,有些比其他的更有效,但可能不是很好读。这里有一个可能不是最有效的,但在大多数情况下肯定是可用的,并且非常简洁易读,利用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());
public static string RemoveAllSpecialCharacters(this string text) {
if (string.IsNullOrEmpty(text))
return text;


string result = Regex.Replace(text, "[:!@#$%^&*()}{|\":?><\\[\\]\\;'/.,~]", " ");
return result;
}

最短的路只有三条线……

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();
}

LINQ的简单方法

string text = "123a22 ";
var newText = String.Join(string.Empty, text.Where(x => x != 'a'));

另一种试图通过减少分配来提高性能的方法,特别是在多次调用此函数的情况下。

它之所以有效,是因为可以保证结果不会比输入长,因此可以在不在内存中创建额外副本的情况下传递输入和输出。因此,不能使用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);
}

如果你需要清理输入字符串以防注入或拼写错误(罕见事件),最快的方法是使用switch()来检查所有字符(编译器在优化switch()的执行时间方面做得很好),再加上额外的代码来删除发现的不需要的字符。下面是解决方案:

    public static string RemoveExtraCharacters(string input)
{
if (string.IsNullOrEmpty(input))
return "";


input = input.Trim();


StringBuilder sb = null;


reStart:
if (!string.IsNullOrEmpty(input))
{
var len = input.Length; ;


for (int i = 0; i < len; i++)
{
switch (input[i])
{
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
case 'G':
case 'H':
case 'I':
case 'J':
case 'K':
case 'L':
case 'M':
case 'N':
case 'O':
case 'Q':
case 'P':
case 'R':
case 'S':
case 'T':
case 'U':
case 'V':
case 'W':
case 'X':
case 'Y':
case 'Z':
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
case 'g':
case 'h':
case 'i':
case 'j':
case 'k':
case 'l':
case 'm':
case 'n':
case 'o':
case 'q':
case 'p':
case 'r':
case 's':
case 't':
case 'u':
case 'v':
case 'w':
case 'x':
case 'y':
case 'z':
case '/':
case '_':
case '-':
case '+':
case '.':
case ',':
case '*':
case ':':
case '=':
case ' ':
case '^':
case '$':
break;


default:
if (sb == null)
sb = new StringBuilder();


sb.Append(input.Substring(0, i));
if (i + 1 < len)
{
input = input.Substring(i + 1);
goto reStart;
}
else
input = null;
break;
}
}
}


if (sb != null)
{
if (input != null)
sb.Append(input);
return sb.ToString();
}


return input;
}