Il modo migliore per dividere la stringa in linee


143

Come si divide la stringa multi-linea in linee?

Lo so così

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

sembra un po 'brutto e perde le righe vuote. C'è una soluzione migliore?



1
Mi piace questa soluzione, non so come renderla più semplice. Il secondo parametro rimuove ovviamente i vuoti.
NappingRabbit

Risposte:


172
  • Se sembra brutto, basta rimuovere la ToCharArraychiamata non necessaria .

  • Se vuoi dividere per uno \no \r, hai due opzioni:

    • Usa un array letterale, ma questo ti darà linee vuote per i finali di linea in stile Windows \r\n:

      var result = text.Split(new [] { '\r', '\n' });
    • Usa un'espressione regolare, come indicato da Bart:

      var result = Regex.Split(text, "\r\n|\r|\n");
  • Se vuoi conservare le righe vuote, perché dici esplicitamente a C # di buttarle via? ( StringSplitOptionsparametro): utilizzare StringSplitOptions.Noneinvece.


2
La rimozione di ToCharArray renderà il codice specifico per la piattaforma (NewLine può essere '\ n')
Konstantin Spirin

1
@Will: per caso ti riferivi a me invece di Konstantin: credo ( fortemente ) che il codice di analisi dovrebbe sforzarsi di funzionare su tutte le piattaforme (cioè dovrebbe anche leggere file di testo codificati su piattaforme diverse rispetto alla piattaforma di esecuzione ). Quindi, per l'analisi, Environment.NewLineè un no-go per quanto mi riguarda. In effetti, tra tutte le possibili soluzioni preferisco quella che utilizza espressioni regolari poiché solo quella gestisce correttamente tutte le piattaforme di origine.
Konrad Rudolph,

2
@Hamish Bene, basta guardare la documentazione dell'enum o guardare nella domanda originale! Lo è StringSplitOptions.RemoveEmptyEntries.
Konrad Rudolph,

8
Che ne dici del testo che contiene '\ r \ n \ r \ n'. string.Split restituirà 4 righe vuote, tuttavia con '\ r \ n' dovrebbe dare 2. Peggiora se '\ r \ n' e '\ r' sono mescolati in un unico file.
nome utente

1
@SurikovPavel Usa l'espressione regolare. Questa è sicuramente la variante preferita, poiché funziona correttamente con qualsiasi combinazione di terminazioni di linea.
Konrad Rudolph,

134
using (StringReader sr = new StringReader(text)) {
    string line;
    while ((line = sr.ReadLine()) != null) {
        // do something
    }
}

12
Questo è l'approccio più pulito, secondo la mia opinione soggettiva.
primo

5
Qualche idea in termini di prestazioni (rispetto a string.Splito Regex.Split)?
Uwe Keim,

52

Aggiornamento: vedi qui per una soluzione alternativa / asincrona.


Funziona alla grande ed è più veloce di Regex:

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

È importante avere "\r\n"prima nell'array in modo che venga preso come un'interruzione di riga. Quanto sopra fornisce gli stessi risultati di una di queste soluzioni Regex:

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

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

Solo che Regex risulta essere circa 10 volte più lento. Ecco il mio test:

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

Produzione:

00: 00: 03,8527616

00: 00: 31,8,017726 millions

00: 00: 32,5,557128 millions

ed ecco il metodo di estensione:

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

Aggiungi alcuni dettagli per rendere la tua risposta più utile per i lettori.
Mohit Jain,

Fatto. Aggiunto anche un test per confrontare le sue prestazioni con la soluzione Regex.
orad

Pattern un po 'più veloce a causa del minor backtracking con la stessa funzionalità se si usa[\r\n]{1,2}
ΩmegaMan

@OmegaMan Che ha un comportamento diverso. Corrisponderà \n\ro \n\ncome singola interruzione di riga che non è corretto.
orad,

3
@OmegaMan Come è Hello\n\nworld\n\nun caso limite? È chiaramente una riga con testo, seguita da una riga vuota, seguita da un'altra riga con testo, seguita da una riga vuota.
Brandin,

36

È possibile utilizzare Regex.Split:

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

Modifica: aggiunto |\rall'account per terminatori di linea (più vecchi) per Mac.


Questo non funzionerà sui file di testo in stile OS X, poiché questi usano solo \rcome fine riga.
Konrad Rudolph,

2
@Konrad Rudolph: AFAIK, '\ r' è stato usato su sistemi MacOS molto vecchi e non si incontra quasi mai più. Ma se l'OP ha bisogno di spiegarlo (o se sbaglio), allora il regex può essere facilmente esteso per
spiegarlo

@Bart: Io non ti pare stia sbagliando, ma io ho più volte incontrato tutti i possibili fine riga nella mia carriera come programmatore.
Konrad Rudolph,

@Konrad, probabilmente hai ragione. Meglio prevenire che curare, immagino.
Bart Kiers,

1
@ ΩmegaMan: questo perderà le righe vuote, ad es. \ N \ n.
Mike Rosoft,

9

Se vuoi mantenere le righe vuote basta rimuovere StringSplitOptions.

var result = input.Split(System.Environment.NewLine.ToCharArray());

2
NewLine può essere '\ n' e il testo di input può contenere "\ n \ r".
Konstantin Spirin,

4

Ho avuto quest'altra risposta, ma questa, basata sulla risposta di Jack , è significativamente più veloce, si potrebbe preferire poiché funziona in modo asincrono, anche se leggermente più lento.

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

Test:

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

Produzione:

00: 00: 03,9603894

00: 00: 00,0029996

00: 00: 04,8221971


Mi chiedo se questo perché non stai effettivamente controllando i risultati dell'enumeratore e quindi non viene eseguito. Sfortunatamente, sono troppo pigro per controllare.
James Holwell,

Sì, in realtà lo è !! Quando aggiungi .ToList () a entrambe le chiamate, la soluzione StringReader è in realtà più lenta! Sulla mia macchina sono 6.74s contro 5.10s
JCH2k

Questo ha senso. Preferisco ancora questo metodo perché mi consente di ottenere le linee in modo asincrono.
orad

Forse dovresti rimuovere l'intestazione "soluzione migliore" sull'altra tua risposta e modificarla ...
JCH2k

4
string[] lines = input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);

2

Leggermente contorto, ma un blocco iteratore per farlo:

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

È quindi possibile chiamare:

var result = input.Lines().ToArray();

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

1

È difficile gestire correttamente le terminazioni di linee miste . Come sappiamo, i caratteri di terminazione linea possono essere "Line Feed" (ASCII 10, \n, \x0A, \u000A), "Carriage Return" (ASCII 13, \r, \x0D, \u000D), o una combinazione di questi. Tornando al DOS, Windows utilizza la sequenza di due caratteri CR-LF \u000D\u000A, quindi questa combinazione dovrebbe emettere una sola riga. Unix usa un singolo \u000AMac molto vecchio e usa un solo \u000Dpersonaggio. Il modo standard di trattare miscele arbitrarie di questi caratteri all'interno di un singolo file di testo è il seguente:

  • ogni singolo carattere CR o LF dovrebbe passare alla riga successiva SALVO ...
  • ... se un CR è immediatamente seguito da LF ( \u000D\u000A), questi due saltano insieme solo una riga.
  • String.Empty è l'unico input che non restituisce righe (qualsiasi carattere comporta almeno una riga)
  • L'ultima riga deve essere restituita anche se non ha né CR né LF.

La regola precedente descrive il comportamento di StringReader.ReadLine e le funzioni correlate e la funzione mostrata di seguito produce risultati identici. È un'efficace funzione di interruzione della linea C # che implementa debitamente queste linee guida per gestire correttamente qualsiasi sequenza o combinazione arbitraria di CR / LF. Le righe elencate non contengono alcun carattere CR / LF. Le righe vuote vengono conservate e restituite come 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: se non ti dispiace il sovraccarico di creare StringReaderun'istanza per ogni chiamata, puoi invece usare il seguente codice C # 7 . Come notato, mentre l'esempio sopra può essere leggermente più efficiente, entrambe queste funzioni producono esattamente gli stessi risultati.

public static IEnumerable<String> Lines(this String s)
{
    using (var tr = new StringReader(s))
        while (tr.ReadLine() is String L)
            yield return L;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.