La mejor manera de dividir cuerdas en líneas

143

¿Cómo se divide una cadena de varias líneas en líneas?

Yo se de esta manera

var result = input.Split("\n\r".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

se ve un poco feo y pierde líneas vacías. ¿Hay una mejor solución?

Konstantin Spirin
fuente
1
Me gusta esta solución, no sé cómo hacerlo más fácil. El segundo parámetro elimina los vacíos, por supuesto.
NappingRabbit

Respuestas:

172
  • Si se ve feo, simplemente elimine la ToCharArrayllamada innecesaria .

  • Si desea dividir entre uno \nu otro \r, tiene dos opciones:

    • Use una matriz literal, pero esto le dará líneas vacías para las terminaciones de línea estilo Windows \r\n:

      var result = text.Split(new [] { '\r', '\n' });
    • Use una expresión regular, como lo indica Bart:

      var result = Regex.Split(text, "\r\n|\r|\n");
  • Si desea conservar las líneas vacías, ¿por qué le dice explícitamente a C # que las tire? ( StringSplitOptionsparámetro): use StringSplitOptions.Noneen su lugar.

Konrad Rudolph
fuente
2
La eliminación de ToCharArray hará que el código sea específico de la plataforma (NewLine puede ser '\ n')
Konstantin Spirin
1
@Will: en caso de que te refieras a mí en lugar de a Konstantin: creo ( firmemente ) que el código de análisis debería esforzarse por funcionar en todas las plataformas (es decir, también debería leer archivos de texto codificados en plataformas diferentes a la plataforma de ejecución ) Entonces, para analizar, Environment.NewLinees un no-go en lo que a mí respecta. De hecho, de todas las soluciones posibles, prefiero la que usa expresiones regulares, ya que solo eso maneja todas las plataformas de origen correctamente.
Konrad Rudolph
2
@Hamish Bueno, solo mira la documentación de la enumeración, ¡o mira la pregunta original! Es StringSplitOptions.RemoveEmptyEntries.
Konrad Rudolph
8
¿Qué tal el texto que contiene '\ r \ n \ r \ n'? string.Split devolverá 4 líneas vacías, sin embargo, con '\ r \ n' debería dar 2. Empeora si '\ r \ n' y '\ r' se mezclan en un archivo.
nombre de usuario
1
@SurikovPavel Use la expresión regular. Esa es definitivamente la variante preferida, ya que funciona correctamente con cualquier combinación de terminaciones de línea.
Konrad Rudolph el
134
using (StringReader sr = new StringReader(text)) {
    string line;
    while ((line = sr.ReadLine()) != null) {
        // do something
    }
}
Jack
fuente
12
Este es el enfoque más limpio, en mi opinión subjetiva.
primo
55
¿Alguna idea en términos de rendimiento (en comparación con string.Splito Regex.Split)?
Uwe Keim el
52

Actualización: Vea aquí para una solución alternativa / asíncrona.


Esto funciona muy bien y es más rápido que Regex:

input.Split(new[] {"\r\n", "\r", "\n"}, StringSplitOptions.None)

Es importante tener "\r\n"primero en la matriz para que se tome como un salto de línea. Lo anterior da los mismos resultados que cualquiera de estas soluciones Regex:

Regex.Split(input, "\r\n|\r|\n")

Regex.Split(input, "\r?\n|\r")

Excepto que Regex resulta ser aproximadamente 10 veces más lento. Aquí está mi prueba:

Action<Action> measure = (Action func) => {
    var start = DateTime.Now;
    for (int i = 0; i < 100000; i++) {
        func();
    }
    var duration = DateTime.Now - start;
    Console.WriteLine(duration);
};

var input = "";
for (int i = 0; i < 100; i++)
{
    input += "1 \r2\r\n3\n4\n\r5 \r\n\r\n 6\r7\r 8\r\n";
}

measure(() =>
    input.Split(new[] {"\r\n", "\r", "\n"}, StringSplitOptions.None)
);

measure(() =>
    Regex.Split(input, "\r\n|\r|\n")
);

measure(() =>
    Regex.Split(input, "\r?\n|\r")
);

Salida:

00: 00: 03.8527616

00: 00: 31.8017726

00: 00: 32.5557128

y aquí está el método de extensión:

public static class StringExtensionMethods
{
    public static IEnumerable<string> GetLines(this string str, bool removeEmptyLines = false)
    {
        return str.Split(new[] { "\r\n", "\r", "\n" },
            removeEmptyLines ? StringSplitOptions.RemoveEmptyEntries : StringSplitOptions.None);
    }
}

Uso:

input.GetLines()      // keeps empty lines

input.GetLines(true)  // removes empty lines
orad
fuente
Agregue algunos detalles más para que su respuesta sea más útil para los lectores.
Mohit Jain
Hecho. También se agregó una prueba para comparar su rendimiento con la solución Regex.
Orad
Patrón algo más rápido debido a menos retroceso con la misma funcionalidad si se usa[\r\n]{1,2}
ΩmegaMan
@OmegaMan Eso tiene un comportamiento diferente. Coincidirá \n\ro \n\ncomo un salto de línea único que no es correcto.
orad
3
@OmegaMan ¿Cómo es Hello\n\nworld\n\nun caso extremo? Es claramente una línea con texto, seguida de una línea vacía, seguida de otra línea con texto, seguida de una línea vacía.
Brandin el
36

Puedes usar Regex.Split:

string[] tokens = Regex.Split(input, @"\r?\n|\r");

Editar: agregado |\ra la cuenta para terminadores de línea Mac (más antiguos).

Bart Kiers
fuente
Sin embargo, esto no funcionará en los archivos de texto de estilo OS X, ya que estos solo se usan \rcomo final de línea.
Konrad Rudolph el
2
@Konrad Rudolph: AFAIK, '\ r' se usó en sistemas MacOS muy antiguos y casi nunca se encuentra. Pero si el OP necesita tenerlo en cuenta (o si me equivoco), entonces la expresión regular puede extenderse fácilmente para tenerlo en cuenta, por supuesto: \ r? \ N | \ r
Bart Kiers
@Bart: Yo no creo que se equivoca, pero me he encontrado repetidamente todos los posibles finales de línea en mi carrera como programador.
Konrad Rudolph el
@ Konrad, probablemente tengas razón. Mejor prevenir que curar, supongo.
Bart Kiers el
1
@ ΩmegaMan: Eso perderá líneas vacías, por ejemplo, \ n \ n.
Mike Rosoft
9

Si desea mantener líneas vacías, simplemente elimine las StringSplitOptions.

var result = input.Split(System.Environment.NewLine.ToCharArray());
Jonas Elfström
fuente
2
NewLine puede ser '\ n' y el texto de entrada puede contener "\ n \ r".
Konstantin Spirin
4

Tuve esta otra respuesta, pero esta, basada en la respuesta de Jack , es significativamente más rápida , ya que funciona de forma asíncrona, aunque un poco más lenta.

public static class StringExtensionMethods
{
    public static IEnumerable<string> GetLines(this string str, bool removeEmptyLines = false)
    {
        using (var sr = new StringReader(str))
        {
            string line;
            while ((line = sr.ReadLine()) != null)
            {
                if (removeEmptyLines && String.IsNullOrWhiteSpace(line))
                {
                    continue;
                }
                yield return line;
            }
        }
    }
}

Uso:

input.GetLines()      // keeps empty lines

input.GetLines(true)  // removes empty lines

Prueba:

Action<Action> measure = (Action func) =>
{
    var start = DateTime.Now;
    for (int i = 0; i < 100000; i++)
    {
        func();
    }
    var duration = DateTime.Now - start;
    Console.WriteLine(duration);
};

var input = "";
for (int i = 0; i < 100; i++)
{
    input += "1 \r2\r\n3\n4\n\r5 \r\n\r\n 6\r7\r 8\r\n";
}

measure(() =>
    input.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)
);

measure(() =>
    input.GetLines()
);

measure(() =>
    input.GetLines().ToList()
);

Salida:

00: 00: 03.9603894

00: 00: 00.0029996

00: 00: 04.8221971

orad
fuente
Me pregunto si esto se debe a que en realidad no está inspeccionando los resultados del enumerador y, por lo tanto, no se está ejecutando. Lamentablemente, soy demasiado vago para comprobarlo.
James Holwell
Sí, en realidad lo es! Cuando agrega .ToList () a ambas llamadas, la solución StringReader es realmente más lenta. En mi máquina es 6.74s vs. 5.10s
JCH2k
Eso tiene sentido. Todavía prefiero este método porque me permite obtener líneas de forma asincrónica.
orad
Tal vez debería eliminar el encabezado de "mejor solución" en su otra respuesta y editar esta ...
JCH2k
4
string[] lines = input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
MAG TOR
fuente
2

Ligeramente torcido, pero un bloque iterador para hacerlo:

public static IEnumerable<string> Lines(this string Text)
{
    int cIndex = 0;
    int nIndex;
    while ((nIndex = Text.IndexOf(Environment.NewLine, cIndex + 1)) != -1)
    {
        int sIndex = (cIndex == 0 ? 0 : cIndex + 1);
        yield return Text.Substring(sIndex, nIndex - sIndex);
        cIndex = nIndex;
    }
    yield return Text.Substring(cIndex + 1);
}

Entonces puede llamar:

var result = input.Lines().ToArray();
JDunkerley
fuente
1
    private string[] GetLines(string text)
    {

        List<string> lines = new List<string>();
        using (MemoryStream ms = new MemoryStream())
        {
            StreamWriter sw = new StreamWriter(ms);
            sw.Write(text);
            sw.Flush();

            ms.Position = 0;

            string line;

            using (StreamReader sr = new StreamReader(ms))
            {
                while ((line = sr.ReadLine()) != null)
                {
                    lines.Add(line);
                }
            }
            sw.Close();
        }



        return lines.ToArray();
    }
John Thompson
fuente
1

Es complicado manejar correctamente las terminaciones de línea mixtas . Como sabemos, los caracteres de fin de línea pueden ser "salto de línea" (ASCII 10, \n, \x0A, \u000A), "retorno de carro" (ASCII 13, \r, \x0D, \u000D), o alguna combinación de ellos. Volviendo a DOS, Windows usa la secuencia de dos caracteres CR-LF \u000D\u000A, por lo que esta combinación solo debería emitir una sola línea. Unix usa un solo \u000A, y Macs muy antiguos usaban un solo \u000Dcarácter. La forma estándar de tratar mezclas arbitrarias de estos caracteres dentro de un solo archivo de texto es la siguiente:

  • todos y cada uno de los caracteres CR o LF deben pasar a la siguiente línea EXCEPTO ...
  • ... si un CR es seguido inmediatamente por LF ( \u000D\u000A) entonces estos dos juntos saltan solo una línea.
  • String.Empty es la única entrada que no devuelve líneas (cualquier carácter implica al menos una línea)
  • La última línea debe devolverse incluso si no tiene CR ni LF.

La regla anterior describe el comportamiento de StringReader.ReadLine y funciones relacionadas, y la función que se muestra a continuación produce resultados idénticos. Es una función eficiente de salto de línea C # que implementa debidamente estas pautas para manejar correctamente cualquier secuencia arbitraria o combinación de CR / LF. Las líneas enumeradas no contienen ningún carácter CR / LF. Las líneas vacías se conservan y devuelven como String.Empty.

/// <summary>
/// Enumerates the text lines from the string.
///   ⁃ Mixed CR-LF scenarios are handled correctly
///   ⁃ String.Empty is returned for each empty line
///   ⁃ No returned string ever contains CR or LF
/// </summary>
public static IEnumerable<String> Lines(this String s)
{
    int j = 0, c, i;
    char ch;
    if ((c = s.Length) > 0)
        do
        {
            for (i = j; (ch = s[j]) != '\r' && ch != '\n' && ++j < c;)
                ;

            yield return s.Substring(i, j - i);
        }
        while (++j < c && (ch != '\r' || s[j] != '\n' || ++j < c));
}

Nota: Si no le importa la sobrecarga de crear una StringReaderinstancia en cada llamada, puede usar el siguiente código C # 7 en su lugar. Como se señaló, aunque el ejemplo anterior puede ser un poco más eficiente, ambas funciones producen exactamente los mismos resultados.

public static IEnumerable<String> Lines(this String s)
{
    using (var tr = new StringReader(s))
        while (tr.ReadLine() is String L)
            yield return L;
}
Glenn Slayden
fuente