27 votes

Comment diviser une chaîne par chaînes et inclure les délimiteurs à l'aide de .NET?

Il y a beaucoup de questions similaires, mais apparemment pas de match parfait, c'est pourquoi je vous demande.

Je voudrais diviser une chaîne aléatoire (par exemple, 123xx456yy789) par une liste de chaîne de caractères délimiteurs (par exemple, xx, yy) et comprennent les délimiteurs dans le résultat (ici: 123, xx, 456, yy, 789).

Bonne performance est un joli bonus. Regex doit être évitée, si possible.

Mise à jour: j'ai fait quelques vérifications de performance et comparé les résultats (trop paresseux pour officiellement vérifier tout de même). Les solutions testées sont (en ordre aléatoire):

  1. Gabe
  2. Guffa
  3. Mafu
  4. Regex

Les autres solutions n'ont pas été testés soit parce qu'ils étaient semblables à une autre solution ou qu'ils sont venus trop tard.

C'est le code de test:

class Program
{
    private static readonly List<Func<string, List<string>, List<string>>> Functions;
    private static readonly List<string> Sources;
    private static readonly List<List<string>> Delimiters;

    static Program ()
    {
        Functions = new List<Func<string, List<string>, List<string>>> ();
        Functions.Add ((s, l) => s.SplitIncludeDelimiters_Gabe (l).ToList ());
        Functions.Add ((s, l) => s.SplitIncludeDelimiters_Guffa (l).ToList ());
        Functions.Add ((s, l) => s.SplitIncludeDelimiters_Naive (l).ToList ());
        Functions.Add ((s, l) => s.SplitIncludeDelimiters_Regex (l).ToList ());

        Sources = new List<string> ();
        Sources.Add ("");
        Sources.Add (Guid.NewGuid ().ToString ());

        string str = "";
        for (int outer = 0; outer < 10; outer++) {
            for (int i = 0; i < 10; i++) {
                str += i + "**" + DateTime.UtcNow.Ticks;
            }
            str += "-";
        }
        Sources.Add (str);

        Delimiters = new List<List<string>> ();
        Delimiters.Add (new List<string> () { });
        Delimiters.Add (new List<string> () { "-" });
        Delimiters.Add (new List<string> () { "**" });
        Delimiters.Add (new List<string> () { "-", "**" });
    }

    private class Result
    {
        public readonly int FuncID;
        public readonly int SrcID;
        public readonly int DelimID;
        public readonly long Milliseconds;
        public readonly List<string> Output;

        public Result (int funcID, int srcID, int delimID, long milliseconds, List<string> output)
        {
            FuncID = funcID;
            SrcID = srcID;
            DelimID = delimID;
            Milliseconds = milliseconds;
            Output = output;
        }

        public void Print ()
        {
            Console.WriteLine ("S " + SrcID + "\tD " + DelimID + "\tF " + FuncID + "\t" + Milliseconds + "ms");
            Console.WriteLine (Output.Count + "\t" + string.Join (" ", Output.Take (10).Select (x => x.Length < 15 ? x : x.Substring (0, 15) + "...").ToArray ()));
        }
    }

    static void Main (string[] args)
    {
        var results = new List<Result> ();

        for (int srcID = 0; srcID < 3; srcID++) {
            for (int delimID = 0; delimID < 4; delimID++) {
                for (int funcId = 3; funcId >= 0; funcId--) { // i tried various orders in my tests
                    Stopwatch sw = new Stopwatch ();
                    sw.Start ();

                    var func = Functions[funcId];
                    var src = Sources[srcID];
                    var del = Delimiters[delimID];

                    for (int i = 0; i < 10000; i++) {
                        func (src, del);
                    }
                    var list = func (src, del);
                    sw.Stop ();

                    var res = new Result (funcId, srcID, delimID, sw.ElapsedMilliseconds, list);
                    results.Add (res);
                    res.Print ();
                }
            }
        }
    }
}

Comme vous pouvez le voir, c'était vraiment juste un moyen rapide et sale de test, mais j'ai couru le test à plusieurs reprises et avec ordre différent et le résultat était toujours très cohérent. Le temps mesuré les cadres sont de l'ordre de quelques millisecondes à quelques secondes pour les plus grands ensembles de données. J'ai ignoré les valeurs dans le bas de millisecondes dans ma suite à l'évaluation parce qu'ils semblaient négligeable dans la pratique. Voici le résultat sur ma boîte:

S 0 D 0 F 3 11 ms
1
S 0 D 0 F 2 7ms
1
S 0 D 0 F 1 6ms
1
S 0 D 0 F 0 4ms
0
S 0 D 1 F 3 28ms
1
S 0 D 1 F 2 8ms
1
S 0 D 1 F 1 7ms
1
S 0 D 1 F 0 3ms
0
S 0 D 2 F 3 30ms
1
S 0 D 2 F 2 8ms
1
S 0 D 2 F 1 6ms
1
S 0 D 2 F 0 3ms
0
S 0 D 3 F 3 30ms
1
S 0 D 3 F 2 10ms
1
S 0 D 3 F 1 8ms
1
S 0 D 3 F 0 3ms
0
S 1 D 0 F 3 9ms
1 9e5282ec-e2a2-4...
S 1 D 0 p 2 6ms
1 9e5282ec-e2a2-4...
S 1 D 0 F 1 5ms
1 9e5282ec-e2a2-4...
S 1 D 0 F 0 5ms
1 9e5282ec-e2a2-4...
S 1 D 1 F 3 63ms
9 9e5282ec - e2a2 - 4265 - 8276 - 6dbb50fdae37
S 1 D 1 F 2 37ms
9 9e5282ec - e2a2 - 4265 - 8276 - 6dbb50fdae37
S 1 D 1 F 1 29ms
9 9e5282ec - e2a2 - 4265 - 8276 - 6dbb50fdae37
S 1 D 1 F 0 22ms
9 9e5282ec - e2a2 - 4265 - 8276 - 6dbb50fdae37
S 1 D 2 F 3 30ms
1 9e5282ec-e2a2-4...
S 1 D 2 F 2 10ms
1 9e5282ec-e2a2-4...
S 1 D 2 F 1 10ms
1 9e5282ec-e2a2-4...
S 1 D 2 F 0 12ms
1 9e5282ec-e2a2-4...
S 1 D 3 F 3 73ms
9 9e5282ec - e2a2 - 4265 - 8276 - 6dbb50fdae37
S 1 D 3 F 2 40ms
9 9e5282ec - e2a2 - 4265 - 8276 - 6dbb50fdae37
S 1 D 3 F 1 33ms
9 9e5282ec - e2a2 - 4265 - 8276 - 6dbb50fdae37
S 1 D 3 F 0 30ms
9 9e5282ec - e2a2 - 4265 - 8276 - 6dbb50fdae37
S 2 D 0 F 3 10 ms
1 0**634226552821...
S 2 D 0 F 2 109ms
1 0**634226552821...
S 2 D 0 F 1 5ms
1 0**634226552821...
S 2 D 0 F 0 127ms
1 0**634226552821...
S 2 D 1 F 3 184ms
21 0**634226552821... - 0**634226552821... - 0**634226552821... - 0**634226
552821... - 0**634226552821... -
S 2 D 1 F 2 364ms
21 0**634226552821... - 0**634226552821... - 0**634226552821... - 0**634226
552821... - 0**634226552821... -
S 2 D 1 F 1 134ms
21 0**634226552821... - 0**634226552821... - 0**634226552821... - 0**634226
552821... - 0**634226552821... -
S 2 D 1 F 0 517ms
20 0**634226552821... - 0**634226552821... - 0**634226552821... - 0**634226
552821... - 0**634226552821... -
S 2 D 2 F 3 688ms
201 0 ** 634226552821217... ** 634226552821217... ** 634226552821217... ** 6
34226552821217... **
S 2 D 2 F 2 2404ms
201 0 ** 634226552821217... ** 634226552821217... ** 634226552821217... ** 6
34226552821217... **
S 2 D 2 F 1 874ms
201 0 ** 634226552821217... ** 634226552821217... ** 634226552821217... ** 6
34226552821217... **
S 2 D 2 F 0 717ms
201 0 ** 634226552821217... ** 634226552821217... ** 634226552821217... ** 6
34226552821217... **
S 2 D 3 F 3 1205ms
221 0 ** 634226552821217... ** 634226552821217... ** 634226552821217... ** 6
34226552821217... **
S 2 D 3 F 2 3471ms
221 0 ** 634226552821217... ** 634226552821217... ** 634226552821217... ** 6
34226552821217... **
S 2 D 3 F 1 1008ms
221 0 ** 634226552821217... ** 634226552821217... ** 634226552821217... ** 6
34226552821217... **
S 2 D 3 F 0 1095ms
220 0 ** 634226552821217... ** 634226552821217... ** 634226552821217... ** 6
34226552821217... **

J'ai comparé les résultats et c'est ce que j'ai trouvé:

  • Tous les 4 fonctions sont assez rapides pour l'usage commun.
  • La version naïve (aka ce que j'ai écrit au départ) est la pire en termes de temps de calcul.
  • Regex est un peu lent sur les petits jeux de données (probablement dû à l'initialisation de la surcharge).
  • Regex n'a bien sur des données de grande taille et atteint une vitesse similaire comme la non-regex solutions.
  • La performance la meilleure semble être Guffa de la version dans l'ensemble, ce qui est prévisible à partir du code.
  • Gabe version parfois omet un élément, mais je n'ai pas enquêter sur cette (bug?).

Pour conclure sur ce sujet, je suggère d'utiliser les Regex, ce qui est assez rapide. Si la performance est critique, je préfère Guffa de mise en œuvre.

36voto

Ahmad Mageed Points 44495

Malgré votre réticence à utiliser les regex, il fait bien conserve les délimiteurs à l'aide d'un groupe avec l' Regex.Split méthode:

string input = "123xx456yy789";
string pattern = "(xx|yy)";
string[] result = Regex.Split(input, pattern);

Si vous supprimez les parenthèses du modèle, en utilisant seulement "xx|yy", les délimiteurs ne sont pas conservées. Assurez-vous d'utiliser les Regex.S'échapper sur le motif si vous utilisez l'un des caractères qui ont une signification dans la regex. Les personnages comprennent \, *, +, ?, |, {, [, (,), ^, $,., #. Par exemple, un séparateur d' . devrait être échappé \.. Étant donné une liste de séparateurs, vous avez besoin de "OU" à l'aide de la pipe | symbole, et cela aussi est un personnage qui arrive échappé. Pour construire le modèle utilisez le code suivant (merci à @gabe pour le signaler):

var delimiters = new List<string> { ".", "xx", "yy" };
string pattern = "(" + String.Join("|", delimiters.Select(d => Regex.Escape(d))
                                                  .ToArray())
                  + ")";

Les parenthèses sont concaténés plutôt qu'inclus dans le modèle, car ils seraient incorrectement échappé à vos fins.

EDIT: En plus, si l' delimiters liste est vide, la dernière tendance, à tort, () et ce serait la cause de vide matchs. Pour éviter cela un chèque pour les délimiteurs peuvent être utilisés. Avec tout cela à l'esprit l'extrait de code devient:

string input = "123xx456yy789";
// to reach the else branch set delimiters to new List();
var delimiters = new List<string> { ".", "xx", "yy", "()" }; 
if (delimiters.Count > 0)
{
    string pattern = "("
                     + String.Join("|", delimiters.Select(d => Regex.Escape(d))
                                                  .ToArray())
                     + ")";
    string[] result = Regex.Split(input, pattern);
    foreach (string s in result)
    {
        Console.WriteLine(s);
    }
}
else
{
    // nothing to split
    Console.WriteLine(input);
}

Si vous avez besoin d'un casse match pour les délimiteurs utiliser l' RegexOptions.IgnoreCase option: Regex.Split(input, pattern, RegexOptions.IgnoreCase)

EDIT #2: la solution jusqu'à présent correspond à split jetons qui pourrait être une sous-chaîne d'une chaîne plus longue. Si le fractionnement jeton doit être correspond tout à fait, plutôt que de faire partie d'une sous-chaîne, comme un scénario où les mots d'une phrase sont utilisés comme délimiteurs, puis la limite de mot \b métacaractère doit être ajoutée autour du motif.

Prenons l'exemple de cette phrase (oui, c'est ringard): "Welcome to stackoverflow... where the stack never overflows!"

Si les délimiteurs { "stack", "flow" } de la solution actuelle serait divisé "stackoverflow" et retour 3 cordes { "stack", "over", "flow" }. Si vous avez besoin d'une correspondance exacte, alors le seul endroit ce serait divisé serait le mot "pile", plus loin dans la phrase, et non pas "stackoverflow".

Pour obtenir une correspondance exacte le comportement de modifier le modèle pour inclure \b comme en \b(delim1|delim2|delimN)\b:

string pattern = @"\b("
                + String.Join("|", delimiters.Select(d => Regex.Escape(d)))
                + @")\b";

Enfin, si le parage les espaces avant et après le délimiteur est souhaitée, ajoutez - \s* autour du motif comme en \s*(delim1|delim2|delimN)\s*. Ceci peut être combiné avec d' \b comme suit:

string pattern = @"\s*\b("
                + String.Join("|", delimiters.Select(d => Regex.Escape(d)))
                + @")\b\s*";

11voto

Nagg Points 4290

Ok, désolé, peut-être celui-ci:

     string source = "123xx456yy789";
    foreach (string delimiter in delimiters)
        source = source.Replace(delimiter, ";" + delimiter + ";");
    string[] parts = source.Split(';');
 

3voto

Gabe Points 49718

Voici une solution qui n'utilise pas d'expression régulière et ne fait pas plus de chaînes que nécessaire:

 public static List<string> Split(string searchStr, string[] separators)
{
    List<string> result = new List<string>();
    int length = searchStr.Length;
    int lastMatchEnd = 0;
    for (int i = 0; i < length; i++)
    {
        for (int j = 0; j < separators.Length; j++)
        {
            string str = separators[j];
            int sepLen = str.Length;
            if (((searchStr[i] == str[0]) && (sepLen <= (length - i))) && ((sepLen == 1) || (String.CompareOrdinal(searchStr, i, str, 0, sepLen) == 0)))
            {
                result.Add(searchStr.Substring(lastMatchEnd, i - lastMatchEnd));
                result.Add(separators[j]);
                i += sepLen - 1;
                lastMatchEnd = i + 1;
                break;
            }
        }
    }
    if (lastMatchEnd != length)
        result.Add(searchStr.Substring(lastMatchEnd));
    return result;
}
 

3voto

Guffa Points 308133

J'ai trouvé une solution pour quelque chose de similaire il y a quelque temps. Pour fractionner efficacement une chaîne, vous pouvez conserver une liste de la prochaine occurrence de chaque délimiteur. De cette façon, vous minimisez le temps que vous devez rechercher pour chaque délimiteur.

Cet algorithme fonctionnera bien même pour une longue chaîne et un grand nombre de délimiteurs:

 string input = "123xx456yy789";
string[] delimiters = { "xx", "yy" };

int[] nextPosition = delimiters.Select(d => input.IndexOf(d)).ToArray();
List<string> result = new List<string>();
int pos = 0;
while (true) {
  int firstPos = int.MaxValue;
  string delimiter = null;
  for (int i = 0; i < nextPosition.Length; i++) {
    if (nextPosition[i] != -1 && nextPosition[i] < firstPos) {
      firstPos = nextPosition[i];
      delimiter = delimiters[i];
    }
  }
  if (firstPos != int.MaxValue) {
    result.Add(input.Substring(pos, firstPos - pos));
    result.Add(delimiter);
    pos = firstPos + delimiter.Length;
    for (int i = 0; i < nextPosition.Length; i++) {
      if (nextPosition[i] != -1 && nextPosition[i] < pos) {
        nextPosition[i] = input.IndexOf(delimiters[i], pos);
      }
    }
  } else {
    result.Add(input.Substring(pos));
    break;
  }
}
 

(Avec des réserves pour tout bogue, je viens de jeter cette version ensemble maintenant et je ne l'ai pas testée minutieusement.)

2voto

mafu Points 8920

Une implémentation naïve

 public IEnumerable<string> SplitX (string text, string[] delimiters)
{
    var split = text.Split (delimiters, StringSplitOptions.None);

    foreach (string part in split) {
        yield return part;
        text = text.Substring (part.Length);

        string delim = delimiters.FirstOrDefault (x => text.StartsWith (x));
        if (delim != null) {
            yield return delim;
            text = text.Substring (delim.Length);
        }
    }
}
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X