web-dev-qa-db-fra.com

Le moyen le plus efficace de supprimer les caractères spéciaux de la chaîne

Je veux supprimer tous les caractères spéciaux d'une chaîne. Les caractères autorisés sont A à Z (majuscule ou minuscule), des nombres (0 à 9), un trait de soulignement (_) ou le signe de point (.).

J'ai le suivant, ça marche mais je soupçonne (je sais!) Que ce n'est pas très efficace:

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

Quel est le moyen le plus efficace de le faire? À quoi ressemblerait une expression régulière et comment se comparerait-elle à une manipulation de chaîne normale?

Les chaînes qui seront nettoyées seront plutôt courtes, généralement entre 10 et 30 caractères.

251
ObiWanKenobi

Pourquoi pensez-vous que votre méthode n'est pas efficace? C'est en fait l'un des moyens les plus efficaces de le faire.

Vous devez bien sûr lire le caractère dans une variable locale ou utiliser un énumérateur pour réduire le nombre d'accès au tableau:

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

Une chose qui rend une telle méthode efficace est qu’elle s’adapte bien. Le temps d'exécution sera relatif à la longueur de la chaîne. Il n'y a pas de mauvaise surprise si vous l'utilisiez sur une grosse corde.

Modifier:
J'ai fait un test de performance rapide, exécutant chaque fonction un million de fois avec une chaîne de 24 caractères. Ce sont les résultats:

Fonction d'origine: 54,5 ms.
Mon changement suggéré: 47,1 ms.
Mine avec le paramètre Capacité de StringBuilder: 43,3 ms.
Expression régulière: 294.4 ms.

Edit 2: J'ai ajouté la distinction entre A-Z et a-z dans le code ci-dessus. (Je répète le test de performance et il n'y a pas de différence notable.)

Edit 3:
J'ai testé la solution lookup + char [] en environ 13 ms.

Le prix à payer est, bien sûr, l’initialisation de l’énorme table de consultation et sa conservation en mémoire. Eh bien, ce n'est pas beaucoup de données, mais c'est beaucoup pour une fonction aussi triviale ...

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

Eh bien, à moins que vous n'ayez vraiment besoin de réduire les performances de votre fonction, choisissez simplement ce qui est le plus facile à maintenir et à comprendre. Une expression régulière ressemblerait à ceci:

Pour des performances supplémentaires, vous pouvez le pré-compiler ou simplement lui demander de le compiler lors du premier appel (les appels suivants seront plus rapides).

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

Je suggère de créer une table de recherche simple, que vous pouvez initialiser dans le constructeur statique pour définir toute combinaison de caractères sur valide. Cela vous permet d'effectuer une vérification simple et rapide.

modifier

En outre, pour des raisons de rapidité, vous souhaiterez initialiser la capacité de votre StringBuilder à la longueur de votre chaîne d'entrée. Cela évitera les réaffectations. Ces deux méthodes combinées vous donneront à la fois vitesse et flexibilité.

une autre édition

Je pense que le compilateur pourrait l’optimiser, mais pour des raisons de style et d’efficacité, je recommande foreach au lieu de.

15
Steven Sudit
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);
}
12
LukeH

Une expression régulière ressemblera à:

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

Mais si les performances sont très importantes, je vous recommande de faire quelques tests de performance avant de sélectionner le "chemin des regex" ...

11
CMS

Si vous utilisez une liste dynamique de caractères, LINQ peut offrir une solution beaucoup plus rapide et élégante:

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

J'ai comparé cette approche à deux des approches "rapides" précédentes (compilation de versions):

  • Solution de tableau de caractères par LukeH - 427 ms
  • Solution StringBuilder - 429 ms
  • LINQ (cette réponse) - 98 ms

Notez que l'algorithme est légèrement modifié - les caractères sont transmis sous forme de tableau plutôt que codés en dur, ce qui pourrait avoir un impact mineur (par exemple, les autres solutions auraient une boucle interne pour vérifier le tableau de caractères).

Si je passe à une solution codée en dur à l'aide d'une clause LINQ où, les résultats sont les suivants:

  • Solution de tableau de caractères - 7ms
  • Solution StringBuilder - 22ms
  • LINQ - 60 ms

Cela vaut peut-être la peine de regarder LINQ ou une approche modifiée si vous envisagez d'écrire une solution plus générique plutôt que de coder en dur la liste de caractères. LINQ vous fournit définitivement un code concis et très lisible, encore plus que Regex.

9
ShadowChaser

Je ne suis pas convaincu que votre algorithme est tout sauf efficace. C'est O(n) et ne regarde chaque caractère qu'une seule fois. Vous n'allez pas faire mieux que ça, sauf si vous connaissez magiquement les valeurs avant de les vérifier.

Je voudrais cependant initialiser la capacité de votre StringBuilder à la taille initiale de la chaîne. Je suppose que votre problème de performance perçu provient de la réallocation de la mémoire.

Note latérale: Vérifier A-z n'est pas sûr. Vous incluez [, \, ], ^, _ et `...

Note 2: Pour une efficacité supplémentaire, placez les comparaisons dans l'ordre afin de minimiser le nombre de comparaisons. (Au pire, vous parlez de 8 comparaisons, alors ne réfléchissez pas trop.) Cela change en fonction de votre apport attendu, mais un exemple pourrait être:

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

Note 3: Si, pour une raison quelconque, vous avez VRAIMENT besoin que cela soit rapide, une instruction switch peut être plus rapide. Le compilateur devrait créer une table de sauts pour vous, ne générant qu'une seule comparaison:

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

Cela me semble bien. La seule amélioration que je ferais serait d’initialiser le StringBuilder avec la longueur de la chaîne.

StringBuilder sb = new StringBuilder(str.Length);
3
bruno conde

Je suis d'accord avec cet exemple de code. Le seul différent, je le fais en méthode d'extension de type chaîne. Pour que vous puissiez l'utiliser dans une ligne ou un code très simple:

string test = "abc@#$123";
test.RemoveSpecialCharacters();

Merci à Guffa pour votre expérience.

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();
        }
}
3
Tola Ch.

Vous pouvez utiliser une expression régulière comme suit:

return Regex.Replace(strIn, @"[^\w\.@-]", "", RegexOptions.None, TimeSpan.FromSeconds(1.0));
3
Giovanny Farto M.
StringBuilder sb = new StringBuilder();

for (int i = 0; i < fName.Length; i++)
{
   if (char.IsLetterOrDigit(fName[i]))
    {
       sb.Append(fName[i]);
    }
}
3
Chamika Sandamal

Je devais faire quelque chose de similaire pour le travail, mais dans mon cas, je devais filtrer tout ce qui n'était pas une lettre, un chiffre ou un espace (mais vous pouvez facilement le modifier en fonction de vos besoins). Le filtrage est effectué côté client en JavaScript, mais pour des raisons de sécurité, je fais également le filtrage côté serveur. Comme je peux m'attendre à ce que la plupart des chaînes soient propres, j'aimerais éviter de les copier sauf si j'en ai vraiment besoin. Ceci me permet d’envisager l’implémentation ci-dessous, qui devrait mieux fonctionner à la fois pour les chaînes propres et sales.

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

Je voudrais utiliser une chaîne remplacer par une expression régulière à la recherche de "caractères spéciaux", en remplaçant tous les caractères trouvés par une chaîne vide.

2
Stephen Wrighton

Il y a beaucoup de solutions proposées ici, certaines plus efficaces que d'autres, mais peut-être pas très lisibles. En voici un qui n'est peut-être pas le plus efficace, mais certainement utilisable dans la plupart des situations, et qui est assez concis et lisible, exploitant 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());
1
Steve Faiwiszewski

Utilisation:

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
}

Et vous aurez une chaîne propre s.

erase() supprime tous les caractères spéciaux et est hautement personnalisable avec la fonction my_predicate().

1
Bhavya Agarwal

HashSet est O (1)
Pas sûr que ce soit plus rapide que la comparaison existante

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

J'ai testé et cela pas plus vite que la réponse acceptée.
Je vais le laisser comme si vous aviez besoin d’un jeu de caractères configurable, ce serait une bonne solution.

1
paparazzo

Je me demande si un remplacement basé sur Regex (éventuellement compilé) est plus rapide. Devrait tester que Quelqu'un a trouvé que c'était 5 fois plus lent.

En dehors de cela, vous devez initialiser StringBuilder avec une longueur attendue, de sorte que la chaîne intermédiaire ne doive pas être copiée pendant sa croissance.

Un bon nombre est la longueur de la chaîne d'origine, ou quelque chose de légèrement inférieur (en fonction de la nature des entrées de fonctions).

Enfin, vous pouvez utiliser une table de correspondance (dans la plage 0..127) pour savoir si un caractère doit être accepté.

1
Christian Klauser

Le code suivant a la sortie suivante (la conclusion est que nous pouvons également économiser certaines ressources de mémoire en allouant une taille de tableau plus petite):

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  

Vous pouvez également ajouter les lignes de code suivantes pour prendre en charge les paramètres régionaux russes (la taille du tableau sera 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);
}
1
Pavel Shkleinik
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();
}
1
Shiko

Je ne suis pas sûr que ce soit le moyen le plus efficace, mais cela fonctionne pour moi

 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
1
RonaldPaguay

Pour S & G's, façon 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());

Je ne pense pas que ce sera le moyen le plus efficace, cependant.

1
Will