Il modo più efficiente per rimuovere caratteri speciali dalla stringa

Voglio rimuovere tutti i caratteri speciali da una stringa. I caratteri consentiti sono AZ (maiuscolo o minuscolo), numeri (0-9), carattere di sottolineatura (_) o il segno di punto (.).

Ho il seguente, funziona ma sospetto (lo so!) Non è molto efficiente:

public static string RemoveSpecialCharacters(string str) { StringBuilder sb = new StringBuilder(); for (int i = 0; i = '0' && str[i] = 'A' && str[i] <= 'z' || (str[i] == '.' || str[i] == '_'))) { sb.Append(str[i]); } } return sb.ToString(); } 

Qual è il modo più efficiente per farlo? Come si presenta un’espressione regolare e come si confronta con la normale manipolazione delle stringhe?

Le stringhe che verranno pulite saranno piuttosto brevi, di solito tra 10 e 30 caratteri di lunghezza.

Perché pensi che il tuo metodo non sia efficiente? In realtà è uno dei modi più efficienti che puoi fare.

Ovviamente dovresti leggere il carattere in una variabile locale o usare un enumeratore per ridurre il numero di accessi alla matrice:

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

Una cosa che rende un metodo così efficiente è che si adatta bene. Il tempo di esecuzione sarà relativo alla lunghezza della stringa. Non ci sono brutte sorprese se lo si usasse su una grande corda.

Modificare:
Ho fatto un rapido test delle prestazioni, eseguendo ogni funzione un milione di volte con una stringa di 24 caratteri. Questi sono i risultati:

Funzione originale: 54,5 ms.
La mia modifica suggerita: 47.1 ms.
Il mio con l'impostazione di capacità StringBuilder: 43,3 ms.
Espressione regolare: 294,4 ms.

Modifica 2: ho aggiunto la distinzione tra AZ e az nel codice sopra. (Eseguo il test delle prestazioni e non ci sono differenze apprezzabili).

Modifica 3:
Ho provato la soluzione lookup + char [] e funziona in circa 13 ms.

Il prezzo da pagare è, naturalmente, l'inizializzazione dell'enorme tabella di ricerca e il suo mantenimento in memoria. Beh, non sono molti dati, ma è molto per una funzione così banale ...

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

Bene, a meno che tu non abbia davvero bisogno di spremere le prestazioni fuori dalla tua funzione, vai con ciò che è più facile da mantenere e capire. Un’espressione regolare sarebbe simile a questa:

Per prestazioni aggiuntive, puoi precompilarlo o semplicemente dirlo per compilare la prima chiamata (le chiamate successive saranno più veloci).

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

Suggerisco di creare una semplice tabella di ricerca, che è ansible inizializzare nel costruttore statico per impostare qualsiasi combinazione di caratteri a valida. Questo ti permette di fare un rapido controllo singolo.

modificare

Inoltre, per la velocità, ti consigliamo di inizializzare la capacità del tuo StringBuilder sulla lunghezza della stringa di input. Ciò eviterà riallocazioni. Questi due metodi insieme ti daranno velocità e flessibilità.

un’altra modifica

Penso che il compilatore potrebbe ottimizzarlo, ma per questioni di stile e di efficienza, raccomando foreach anziché for.

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

Un’espressione regolare sarà simile a:

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

Ma se le prestazioni sono molto importanti, ti consiglio di fare alcuni benchmark prima di selezionare il “percorso regolare” …

Se stai utilizzando un elenco dinamico di caratteri, LINQ può offrire una soluzione molto più rapida e aggraziata:

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

Ho confrontato questo approccio con due dei precedenti approcci “veloci” (compilazione delle versioni):

  • Soluzione Char array di LukeH – 427 ms
  • Soluzione StringBuilder – 429 ms
  • LINQ (questa risposta) – 98 ms

Si noti che l’algoritmo è leggermente modificato: i caratteri vengono passati come una matrice piuttosto che codificati, il che potrebbe influire leggermente sulle cose (cioè / le altre soluzioni avrebbero un loop interno per controllare l’array di caratteri).

Se passo a una soluzione hardcoded utilizzando una clausola LINQ where, i risultati sono:

  • Soluzione Char array – 7ms
  • Soluzione StringBuilder – 22ms
  • LINQ – 60 ms

Potrebbe essere utile consultare LINQ o un approccio modificato se si prevede di scrivere una soluzione più generica, piuttosto che codificare a fondo l’elenco di caratteri. LINQ ti offre sicuramente un codice conciso e altamente leggibile, ancor più del Regex.

Non sono convinto che il tuo algoritmo sia tutt’altro che efficiente. È O (n) e guarda solo ogni personaggio una volta. Non migliorerai di così, a meno che tu non conosca magicamente i valori prima di controllarli.

Vorrei comunque inizializzare la capacità del tuo StringBuilder alla dimensione iniziale della stringa. Immagino che il tuo problema prestazionale percepito derivi dalla riallocazione della memoria.

Nota a margine: il controllo di Az non è sicuro. Stai includendo [ , \ , ] , ^ , _ e `…

Nota a margine 2: per quel touch in più di efficienza, mettere i confronti in un ordine per minimizzare il numero di confronti. (Nella peggiore delle ipotesi, stai parlando di 8 confronti, quindi non pensare troppo.) Questo cambia con il tuo input previsto, ma un esempio potrebbe essere:

 if (str[i] >= '0' && str[i] <= 'z' && (str[i] >= 'a' || str[i] <= '9' || (str[i] >= 'A' && str[i] <= 'Z') || str[i] == '_') || str[i] == '.') 

Nota a margine 3: Se per qualsiasi ragione hai VERAMENTE bisogno che questo sia veloce, un'istruzione switch potrebbe essere più veloce. Il compilatore dovrebbe creare una tabella di salto per te, risultando in un solo confronto:

 switch (str[i]) { case '0': case '1': . . . case '.': sb.Append(str[i]); break; } 

Vorrei usare una stringa Sostituisci con un’espressione regolare alla ricerca di “caratteri speciali”, sostituendo tutti i caratteri trovati con una stringa vuota.

Mi sembra buono L’unico miglioramento che vorrei fare è inizializzare StringBuilder con la lunghezza della stringa.

 StringBuilder sb = new StringBuilder(str.Length); 
 StringBuilder sb = new StringBuilder(); for (int i = 0; i < fName.Length; i++) { if (char.IsLetterOrDigit(fName[i])) { sb.Append(fName[i]); } } 

Sono d’accordo con questo esempio di codice. L’unico diverso lo faccio in estensione Metodo di tipo stringa. In modo che tu possa usarlo in una linea o codice molto semplice:

 string test = "[email protected]#$123"; test.RemoveSpecialCharacters(); 

Grazie a Guffa per il tuo esperimento.

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

Dovevo fare qualcosa di simile per lavoro, ma nel mio caso dovevo filtrare tutto ciò che non è una lettera, un numero o uno spazio bianco (ma potresti facilmente modificarlo in base alle tue esigenze). Il filtraggio è fatto lato client in JavaScript, ma per ragioni di sicurezza sto facendo anche il filtro sul lato server. Dato che posso aspettarmi che la maggior parte delle stringhe sia pulita, vorrei evitare di copiare la stringa a meno che non ne abbia davvero bisogno. Questo ha permesso di implementare l’implementazione qui sotto, che dovrebbe funzionare meglio per stringhe pulite e sporche.

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

Per S & G, modo 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()); 

Non penso che questo sarà il modo più efficiente, comunque.

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

Uso:

 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 } 

E otterrai una stringa pulita.

erase() lo my_predicate() di tutti i caratteri speciali ed è altamente personalizzabile con la funzione my_predicate() .

HashSet è O (1)
Non sono sicuro se è più veloce del confronto esistente

 private static HashSet ValidChars = new HashSet() { '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(); } 

Ho provato e questo non è più veloce della risposta accettata.
Lo lascerò come se avessi bisogno di un set di caratteri configurabile questa sarebbe una buona soluzione.

Mi chiedo se una sostituzione basata su Regex (eventualmente compilata) sia più veloce. Dovrei testare che qualcuno abbia trovato che questo è ~ 5 volte più lento.

Oltre a questo, è necessario inizializzare StringBuilder con una lunghezza prevista, in modo che la stringa intermedia non debba essere copiata mentre cresce.

Un buon numero è la lunghezza della stringa originale o qualcosa di leggermente inferiore (a seconda della natura degli ingressi delle funzioni).

Infine, puoi usare una tabella di ricerca (nell’intervallo 0..127) per scoprire se un personaggio deve essere accettato.

Il seguente codice ha il seguente output (la conclusione è che possiamo anche salvare alcune risorse di memoria che allocano una dimensione più piccola dell’array):

 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 

È anche ansible aggiungere le seguenti linee di codice per supportare le impostazioni internazionali della lingua russa (la dimensione dell'array sarà 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); } 

Non sono sicuro che sia il modo più efficace, ma funziona per me

  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 

Puoi usare espressioni regolari come segue:

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

Ci sono molte soluzioni proposte qui, alcune più efficienti di altre, ma forse non molto leggibili. Eccone uno che potrebbe non essere il più efficiente, ma sicuramente utilizzabile per la maggior parte delle situazioni, ed è abbastanza conciso e leggibile, sfruttando 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(". -"); 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 RemoveSpecialCharacters(string str){ return str.replaceAll("[^A-Za-z0-9_\\\\.]", ""); } 

Se sei preoccupato per la velocità, usa i puntatori per modificare la stringa esistente. È ansible bloccare la stringa e ottenere un puntatore, quindi eseguire un ciclo for su ciascun carattere, sovrascrivendo ogni carattere non valido con un carattere sostitutivo. Sarebbe estremamente efficiente e non richiederebbe l’allocazione di alcuna nuova memoria di stringa. Dovresti inoltre compilare il modulo con l’opzione non sicura e aggiungere il modificatore “non sicuro” all’intestazione del metodo per poter utilizzare i puntatori.

 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 }