¿Cómo puedo dividir una cadena por cadenas e incluir los delimitadores usando .NET?

Hay muchas preguntas similares, pero aparentemente no coinciden, es por eso que pregunto.

Me gustaría dividir una cadena aleatoria (por ejemplo, 123xx456yy789 ) por una lista de delimitadores de cadena (por ejemplo, xx , yy ) e incluir los delimitadores en el resultado (aquí: 123 , xx , 456 , yy , 789 ).

Un buen rendimiento es una buena ventaja. Regex debe evitarse, si es posible.

Actualización : realicé algunas comprobaciones de rendimiento y comparé los resultados (aunque son demasiado flojos para comprobarlos formalmente). Las soluciones probadas son (en orden aleatorio):

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

No se probaron otras soluciones porque eran similares a otra solución o llegaron demasiado tarde.

Este es el código de prueba:

 class Program { private static readonly List<Func<string, List, List>> Functions; private static readonly List Sources; private static readonly List<List> Delimiters; static Program () { Functions = new List<Func<string, List, List>> (); 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 (); 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> (); Delimiters.Add (new List () { }); Delimiters.Add (new List () { "-" }); Delimiters.Add (new List () { "**" }); Delimiters.Add (new List () { "-", "**" }); } private class Result { public readonly int FuncID; public readonly int SrcID; public readonly int DelimID; public readonly long Milliseconds; public readonly List Output; public Result (int funcID, int srcID, int delimID, long milliseconds, List 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 (); for (int srcID = 0; srcID < 3; srcID++) { for (int delimID = 0; delimID = 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 (); } } } } } 

Como puede ver, en realidad fue solo una prueba rápida y sucia, pero realicé la prueba varias veces y con un orden diferente y el resultado siempre fue muy consistente. Los marcos de tiempo medidos están en el rango de milisegundos hasta segundos para los conjuntos de datos más grandes. Ignoré los valores en el rango bajo en milisegundos en mi siguiente evaluación porque parecían insignificantes en la práctica. Aquí está la salida en mi caja:

  S 0 D 0 F 3 11ms
 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 F 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 10ms
 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 ... ** 

Comparé los resultados y esto es lo que encontré:

  • Las 4 funciones son lo suficientemente rápidas para el uso común.
  • La versión ingenua (también conocida como lo que escribí inicialmente) es la peor en términos de tiempo de cálculo.
  • Regex es un poco lento en pequeños conjuntos de datos (probablemente debido a la sobrecarga de inicialización).
  • Regex funciona bien en datos grandes y alcanza una velocidad similar a la de las soluciones no regex.
  • La mejor interpretación del rendimiento parece ser la versión general de Guffa, que es de esperar del código.
  • La versión de Gabe a veces omite un elemento, pero no investigué esto (¿error?).

Para concluir este tema, sugiero usar Regex, que es bastante rápido. Si el rendimiento es crítico, preferiría la implementación de Guffa.

A pesar de su renuencia a usar expresiones regulares, conserva muy bien los delimitadores al usar un grupo junto con el método Regex.Split :

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

Si elimina los paréntesis del patrón, usando solo "xx|yy" , los delimitadores no se conservan. Asegúrese de usar Regex.Escape en el patrón si usa metacaracteres que tengan un significado especial en expresiones regulares. Los caracteres incluyen \, *, +, ?, |, {, [, (,), ^, $,., # . Por ejemplo, un delimitador de . debe ser escapado \. . Dada una lista de delimitadores, necesitas “O” usar el tubo | símbolo y que también es un personaje que se escapó. Para construir correctamente el patrón use el siguiente código (gracias a @gabe para señalar esto):

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

Los paréntesis se concatenan en lugar de incluirse en el patrón, ya que se escaparían incorrectamente para sus propósitos.

EDITAR: Además, si la lista de delimiters está vacía, el patrón final sería incorrectamente () y esto provocaría coincidencias en blanco. Para evitar esto, se puede usar un control para los delimitadores. Con todo esto en mente, el fragmento se convierte en:

 string input = "123xx456yy789"; // to reach the else branch set delimiters to new List(); var delimiters = new List { ".", "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 necesita una coincidencia insensible a mayúsculas y minúsculas para los delimitadores, use la opción Regex.Split(input, pattern, RegexOptions.IgnoreCase) : Regex.Split(input, pattern, RegexOptions.IgnoreCase)

EDIT # 2: la solución hasta ahora coincide con los tokens de división que podrían ser una subcadena de una cadena más grande. Si el token dividido debe coincidir por completo, en lugar de ser parte de una subcadena, como un escenario donde las palabras en una oración se usan como delimitadores, entonces el metacaracter de palabra-límite \b debe agregarse alrededor del patrón.

Por ejemplo, considere esta oración (sí, es cursi): "Welcome to stackoverflow... where the stack never overflows!"

Si los delimitadores fueran { "stack", "flow" } la solución actual dividiría “stackoverflow” y devolvería 3 cadenas { "stack", "over", "flow" } . Si necesita una coincidencia exacta, entonces el único lugar donde se dividiría sería en la palabra “stack” más adelante en la oración y no en “stackoverflow”.

Para lograr un comportamiento de coincidencia exacta, modifique el patrón para incluir \b como en \b(delim1|delim2|delimN)\b :

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

Finalmente, si desea recortar los espacios antes y después de los delimitadores, agregue \s* alrededor del patrón como en \s*(delim1|delim2|delimN)\s* . Esto se puede combinar con \b siguiente manera:

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

Ok, lo siento, tal vez este:

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

Aquí hay una solución que no usa una expresión regular y no crea más cadenas de las necesarias:

 public static List Split(string searchStr, string[] separators) { List result = new List(); 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; } 

Se me ocurrió una solución para algo similar hace un tiempo. Para dividir de manera eficiente una cadena, puede mantener una lista de la próxima ocurrencia de cada delimitador. De esta forma, minimiza los tiempos en los que debe buscar cada delimitador.

Este algoritmo funcionará bien incluso para una cadena larga y una gran cantidad de delimitadores:

 string input = "123xx456yy789"; string[] delimiters = { "xx", "yy" }; int[] nextPosition = delimiters.Select(d => input.IndexOf(d)).ToArray(); List result = new List(); 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; } } 

(Con reservas para cualquier error, acabo de lanzar esta versión ahora y no la he probado exhaustivamente).

Una implementación ingenua

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

Esto tendrá semántica idéntica a String.Split modo predeterminado (por lo que no incluye tokens vacíos).

Se puede hacer más rápido mediante el uso de código inseguro para iterar sobre la cadena de origen, aunque esto requiere que usted mismo escriba el mecanismo de iteración en lugar de usar el retorno de rendimiento. Asigna el mínimo absoluto (una subcadena por token no separador más el enumerador de envoltura) de forma tan realista para mejorar el rendimiento que tendría que:

  • use aún más código inseguro (usando ‘CompareOrdinal’ yo efectivamente lo soy)
    • principalmente para evitar la sobrecarga de la búsqueda de caracteres en la cadena con un búfer de caracteres
  • hacer uso del conocimiento específico del dominio sobre las fonts de entrada o tokens.
    • usted puede estar contento de eliminar la verificación nula de los separadores
    • usted puede saber que los separadores casi nunca son caracteres individuales

El código está escrito como un método de extensión

 public static IEnumerable SplitWithTokens( string str, string[] separators) { if (separators == null || separators.Length == 0) { yield return str; yield break; } int prev = 0; for (int i = 0; i < str.Length; i++) { foreach (var sep in separators) { if (!string.IsNullOrEmpty(sep)) { if (((str[i] == sep[0]) && (sep.Length <= (str.Length - i))) && ((sep.Length == 1) || (string.CompareOrdinal(str, i, sep, 0, sep.Length) == 0))) { if (i - prev != 0) yield return str.Substring(prev, i - prev); yield return sep; i += sep.Length - 1; prev = i + 1; break; } } } } if (str.Length - prev > 0) yield return str.Substring(prev, str.Length - prev); } 

Mi primera publicación / respuesta … este es un enfoque recursivo.

  static void Split(string src, string[] delims, ref List final) { if (src.Length == 0) return; int endTrimIndex = src.Length; foreach (string delim in delims) { //get the index of the first occurance of this delim int indexOfDelim = src.IndexOf(delim); //check to see if this delim is at the begining of src if (indexOfDelim == 0) { endTrimIndex = delim.Length; break; } //see if this delim comes before previously searched delims else if (indexOfDelim < endTrimIndex && indexOfDelim != -1) endTrimIndex = indexOfDelim; } final.Add(src.Substring(0, endTrimIndex)); Split(src.Remove(0, endTrimIndex), delims, ref final); }