Melhor maneira de dividir a string em linhas

143

Como você divide uma sequência de linhas múltiplas em linhas?

Eu sei assim

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

parece um pouco feio e perde linhas vazias. Existe uma solução melhor?

Konstantin Spirin
fonte
1
Eu gosto dessa solução, não sei como facilitar. O segundo parâmetro remove os vazios, é claro.
NappingRabbit

Respostas:

172
  • Se parecer feio, basta remover a ToCharArraychamada desnecessária .

  • Se você deseja dividir por um \nou por \r, você tem duas opções:

    • Use uma matriz literal - mas isso fornecerá linhas vazias para finais de linha no estilo do Windows \r\n:

      var result = text.Split(new [] { '\r', '\n' });
    • Use uma expressão regular, conforme indicado por Bart:

      var result = Regex.Split(text, "\r\n|\r|\n");
  • Se você deseja preservar linhas vazias, por que você diz explicitamente ao C # para jogá-las fora? ( StringSplitOptionsparâmetro) - use em seu StringSplitOptions.Nonelugar.

Konrad Rudolph
fonte
2
Removendo ToCharArray fará específico da plataforma de código (nova linha pode ser '\ n')
Konstantin Spirin
1
@ Will: com a chance de você estar se referindo a mim em vez de Konstantin: Eu acredito ( fortemente ) que o código de análise deve se esforçar para trabalhar em todas as plataformas (ou seja, também deve ler arquivos de texto que foram codificados em plataformas diferentes da plataforma de execução) ) Então, para analisar, Environment.NewLineé um não-ir para mim. De fato, de todas as soluções possíveis, eu prefiro a que usa expressões regulares, pois somente ela lida com todas as plataformas de origem corretamente.
precisa saber é o seguinte
2
@ Hamish Bem, basta olhar para a documentação do enum, ou olhar na pergunta original! É StringSplitOptions.RemoveEmptyEntries.
Konrad Rudolph
8
Que tal o texto que contém '\ r \ n \ r \ n'. string.Split retornará 4 linhas vazias; no entanto, com '\ r \ n', ele deve fornecer 2. Fica pior se '\ r \ n' e '\ r' forem misturados em um arquivo.
username
1
@SurikovPavel Use a expressão regular. Essa é definitivamente a variante preferida, pois funciona corretamente com qualquer combinação de terminações de linha.
Konrad Rudolph
134
using (StringReader sr = new StringReader(text)) {
    string line;
    while ((line = sr.ReadLine()) != null) {
        // do something
    }
}
Jack
fonte
12
Essa é a abordagem mais limpa, na minha opinião subjetiva.
Primo
5
Qualquer idéia em termos de desempenho (em comparação com string.Splitou Regex.Split)?
Uwe Keim
52

Atualização: Veja aqui uma solução alternativa / assíncrona.


Isso funciona muito bem e é mais rápido que o Regex:

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

É importante ter o "\r\n"primeiro na matriz para que seja considerado como uma quebra de linha. O acima fornece os mesmos resultados que qualquer uma dessas soluções Regex:

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

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

Só que o Regex é 10 vezes mais lento. Aqui está o meu teste:

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

Resultado:

00: 00: 03.8527616

00: 00: 31.8017726

00: 00: 32.5557128

e aqui está o método de extensão:

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
fonte
Adicione mais alguns detalhes para tornar sua resposta mais útil para os leitores.
Mohit Jain
Feito. Também foi adicionado um teste para comparar seu desempenho com a solução Regex.
orad 8/08/14
Um pouco mais rápido padrão devido à menor retrocesso com a mesma funcionalidade se uma utilidades[\r\n]{1,2}
ΩmegaMan
@ OmegaMan Isso tem um comportamento diferente. Ele corresponderá \n\rou \n\ncomo quebra de linha única, o que não está correto.
orad 27/02
3
@OmegaMan Como é Hello\n\nworld\n\num caso extremo? É claramente uma linha com texto, seguida por uma linha vazia, seguida por outra linha com texto, seguida por uma linha vazia.
Brandin
36

Você pode usar o Regex.Split:

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

Editar: adicionado |\rà conta de terminadores de linha Mac (mais antigos).

Bart Kiers
fonte
Porém, isso não funcionará em arquivos de texto no estilo OS X, pois eles são usados ​​apenas \rcomo final de linha.
9289 Konrad Rudolph
2
@ Konrad Rudolph: AFAIK, '\ r' foi usado em sistemas MacOS muito antigos e quase nunca é mais encontrado. Mas se o OP precisar dar conta disso (ou se eu estiver enganado), a regex pode ser facilmente estendida para dar conta dela, é claro: \ r? \ N | \ r
Bart Kiers
@Bart: Eu não acho que você está enganado, mas eu tenho repetidamente encontrou todos os possíveis finais de linha na minha carreira como programador.
9289 Konrad Rudolph
@ Konrad, você provavelmente está certo. Melhor prevenir do que remediar, eu acho.
227 Bart Kiers
1
@ EgamegaMan: Isso perderá linhas vazias, por exemplo, \ n \ n.
Mike Rosoft 21/03/19
9

Se você deseja manter linhas vazias, remova as StringSplitOptions.

var result = input.Split(System.Environment.NewLine.ToCharArray());
Jonas Elfström
fonte
2
A nova linha pode ser '\ n' e o texto de entrada pode conter "\ n \ r".
Konstantin Spirin
4

Eu tive essa outra resposta, mas essa, com base na resposta de Jack , é significativamente mais rápida, pode ser preferida, pois funciona de forma assíncrona, embora um pouco mais 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

Teste:

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

Resultado:

00: 00: 03.9603894

00: 00: 00.0029996

00: 00: 04.8221971

orad
fonte
Eu me pergunto se isso é porque você não está realmente inspecionando os resultados do enumerador e, portanto, ele não está sendo executado. Infelizmente, estou com preguiça de verificar.
precisa
Sim, é mesmo !! Quando você adiciona .ToList () às duas chamadas, a solução StringReader é realmente mais lenta! Na minha máquina é 6.74s vs. 5.10s
JCH2k
Isso faz sentido. Eu ainda prefiro esse método porque ele permite obter linhas de forma assíncrona.
Orad 6/11
Talvez você deve remover o cabeçalho "melhor solução" em sua outra resposta e editar este ...
JCH2k
4
string[] lines = input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
MAG TOR
fonte
2

Ligeiramente torcido, mas um bloco iterador para fazer isso:

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

Você pode ligar para:

var result = input.Lines().ToArray();
JDunkerley
fonte
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
fonte
1

É complicado lidar adequadamente com terminações de linhas mistas . Como sabemos, os personagens linha de terminação pode ser "Line Feed" (ASCII 10, \n, \x0A, \u000A), "Return" (ASCII 13, \r, \x0D, \u000D), ou alguma combinação deles. Voltando ao DOS, o Windows usa a sequência de dois caracteres CR-LF \u000D\u000A, portanto, essa combinação deve emitir apenas uma única linha. O Unix usa um único \u000AMacs e muito antigos usam um único \u000Dcaractere. A maneira padrão de tratar misturas arbitrárias desses caracteres em um único arquivo de texto é a seguinte:

  • cada caractere CR ou LF deve pular para a próxima linha, EXCETO ...
  • ... se um CR é imediatamente seguido por LF ( \u000D\u000A), esses dois juntos pulam apenas uma linha.
  • String.Empty é a única entrada que não retorna linhas (qualquer caractere implica pelo menos uma linha)
  • A última linha deve ser retornada, mesmo que não tenha CR nem LF.

A regra anterior descreve o comportamento de StringReader.ReadLine e funções relacionadas, e a função mostrada abaixo produz resultados idênticos. É uma função eficiente de quebra de linha de C # que implementa obedientemente essas diretrizes para manipular corretamente qualquer sequência ou combinação arbitrária de CR / LF. As linhas enumeradas não contêm caracteres CR / LF. Linhas vazias são preservadas e retornadas 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: Se você não se importa com a sobrecarga de criar uma StringReaderinstância em cada chamada, pode usar o seguinte código C # 7 . Como observado, embora o exemplo acima possa ser um pouco mais eficiente, ambas as funções produzem exatamente os mesmos 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
fonte