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?



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.


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

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

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).


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

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


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

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

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

É 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;
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.