Cara terbaik untuk memisahkan string menjadi garis

143

Bagaimana Anda membagi string multi-baris menjadi garis?

Saya tahu cara ini

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

terlihat agak jelek dan kehilangan garis kosong. Apakah ada solusi yang lebih baik?

Konstantin Spirin
sumber
1
Saya suka solusi ini, saya tidak tahu bagaimana membuatnya lebih mudah. Parameter kedua menghapus kosong tentu saja.
NappingRabbit

Jawaban:

172
  • Jika terlihat jelek, hapus saja ToCharArraypanggilan yang tidak perlu .

  • Jika Anda ingin membagi dengan salah satu \natau \r, Anda punya dua opsi:

    • Gunakan array literal - tetapi ini akan memberi Anda baris kosong untuk akhiran bergaya Windows \r\n:

      var result = text.Split(new [] { '\r', '\n' });
    • Gunakan ekspresi reguler, seperti yang ditunjukkan oleh Bart:

      var result = Regex.Split(text, "\r\n|\r|\n");
  • Jika Anda ingin mempertahankan baris kosong, mengapa Anda secara eksplisit meminta C # untuk membuangnya? ( StringSplitOptionsparameter) - gunakan StringSplitOptions.Nonesaja.

Konrad Rudolph
sumber
2
Menghapus ToCharArray akan membuat platform spesifik kode (NewLine dapat '\ n')
Konstantin Spirin
1
@ Akan: jika Anda merujuk kepada saya alih-alih Konstantin: Saya percaya ( sangat ) bahwa kode parsing harus berusaha untuk bekerja pada semua platform (yaitu juga harus membaca file teks yang dikodekan pada platform yang berbeda dari platform yang mengeksekusi ). Jadi untuk parsing, Environment.NewLineadalah jalan keluar sejauh yang saya ketahui. Bahkan, dari semua solusi yang mungkin saya lebih suka yang menggunakan ekspresi reguler karena hanya itu yang menangani semua platform sumber dengan benar.
Konrad Rudolph
2
@ Hamish Yah lihat saja dokumentasi enum, atau lihat pertanyaan aslinya! Ini StringSplitOptions.RemoveEmptyEntries.
Konrad Rudolph
8
Bagaimana dengan teks yang mengandung '\ r \ n \ r \ n'. string.Split akan mengembalikan 4 baris kosong, namun dengan '\ r \ n' itu akan memberi 2. Semakin buruk jika '\ r \ n' dan '\ r' dicampur dalam satu file.
nama pengguna
1
@SurikovPavel Gunakan ekspresi reguler. Itu jelas merupakan varian yang disukai, karena ia bekerja dengan benar dengan kombinasi akhiran.
Konrad Rudolph
134
using (StringReader sr = new StringReader(text)) {
    string line;
    while ((line = sr.ReadLine()) != null) {
        // do something
    }
}
Mendongkrak
sumber
12
Ini adalah pendekatan terbersih, menurut pendapat subjektif saya.
primo
5
Adakah gagasan dalam hal kinerja (dibandingkan dengan string.Splitatau Regex.Split)?
Uwe Keim
52

Pembaruan: Lihat di sini untuk solusi alternatif / async.


Ini berfungsi dengan baik dan lebih cepat daripada Regex:

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

Penting untuk memiliki yang "\r\n"pertama dalam array sehingga diambil sebagai satu baris. Di atas memberikan hasil yang sama dengan salah satu dari solusi Regex ini:

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

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

Kecuali bahwa Regex ternyata sekitar 10 kali lebih lambat. Inilah tes saya:

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

Keluaran:

00:00: 03.8527616

00:00: 31.8017726

00:00: 32.5557128

dan inilah Metode Extension:

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

Pemakaian:

input.GetLines()      // keeps empty lines

input.GetLines(true)  // removes empty lines
orad
sumber
Harap tambahkan beberapa detail lagi untuk membuat jawaban Anda lebih bermanfaat bagi pembaca.
Mohit Jain
Selesai Juga menambahkan tes untuk membandingkan kinerjanya dengan solusi Regex.
orad
Pola yang agak lebih cepat karena kurang mundur dengan fungsi yang sama jika digunakan[\r\n]{1,2}
--megaMan
@OmegaMan Itu memiliki beberapa perilaku yang berbeda. Ini akan cocok \n\ratau \n\nsebagai pemecah baris tunggal yang tidak benar.
orad
3
@OmegaMan Bagaimana Hello\n\nworld\n\nkasus tepi? Itu jelas satu baris dengan teks, diikuti oleh baris kosong, diikuti oleh baris lain dengan teks, diikuti oleh baris kosong.
Brandin
36

Anda bisa menggunakan Regex. Letakkan:

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

Sunting: ditambahkan |\rke akun untuk terminator garis Mac (lama).

Bart Kiers
sumber
Ini tidak akan berfungsi pada file teks gaya OS X, karena ini hanya menggunakan \rsebagai akhir baris.
Konrad Rudolph
2
@Konrad Rudolph: AFAIK, '\' digunakan pada sistem MacOS yang sangat lama dan hampir tidak pernah ditemukan lagi. Tetapi jika OP perlu menjelaskannya (atau jika saya salah), maka regex dapat dengan mudah diperpanjang untuk menjelaskannya tentu saja: \ r? \ N | \ r
Bart Kiers
@ Bart: Saya tidak berpikir Anda salah, tetapi saya telah berulang kali menemukan semua kemungkinan hasil akhir dalam karier saya sebagai seorang programmer.
Konrad Rudolph
@ Konrad, Anda mungkin benar. Lebih baik aman daripada menyesal, kurasa.
Bart Kiers
1
@ ΩmegaMan: Itu akan kehilangan baris kosong, mis. \ N \ n.
Mike Rosoft
9

Jika Anda ingin menjaga baris kosong cukup hapus StringSplitOptions.

var result = input.Split(System.Environment.NewLine.ToCharArray());
Jonas Elfström
sumber
2
NewLine dapat berupa '\ n' dan teks input dapat berisi "\ n \ r".
Konstantin Spirin
4

Saya punya jawaban lain ini tetapi yang ini, berdasarkan jawaban Jack , secara signifikan lebih cepat mungkin lebih disukai karena ia bekerja secara serempak, walaupun sedikit lebih lambat.

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

Pemakaian:

input.GetLines()      // keeps empty lines

input.GetLines(true)  // removes empty lines

Uji:

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

Keluaran:

00:00: 03.9603894

00.00: 00.0029996

00:00: 04.8221971

orad
sumber
Saya bertanya-tanya apakah ini karena Anda tidak benar-benar memeriksa hasil enumerator, dan karena itu tidak dieksekusi. Sayangnya, saya terlalu malas untuk memeriksanya.
James Holwell
Ya, sebenarnya !! Saat Anda menambahkan .ToList () ke kedua panggilan, solusi StringReader sebenarnya lebih lambat! Di mesin saya itu adalah 6.74s vs 5.10s
JCH2k
Itu masuk akal. Saya masih lebih suka metode ini karena memungkinkan saya untuk mendapatkan garis secara tidak sinkron.
orad
Mungkin Anda harus menghapus tajuk "solusi yang lebih baik" pada jawaban Anda yang lain dan mengedit yang ini ...
JCH2k
4
string[] lines = input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
MAG TOR
sumber
2

Sedikit terpelintir, tetapi blok iterator untuk melakukannya:

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

Anda kemudian dapat menelepon:

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

Sangat sulit untuk menangani ujung garis campuran dengan benar. Seperti kita ketahui, karakter garis terminasi dapat "Pakan Line" (ASCII 10, \n, \x0A, \u000A), "Carriage Return" (ASCII 13, \r, \x0D, \u000D), atau beberapa kombinasi dari mereka. Kembali ke DOS, Windows menggunakan urutan dua karakter CR-LF \u000D\u000A, jadi kombinasi ini hanya akan memancarkan satu baris. Unix menggunakan satu \u000A, dan sangat lama Mac menggunakan satu \u000Dkarakter. Cara standar untuk memperlakukan campuran karakter ini secara acak dalam satu file teks adalah sebagai berikut:

  • masing-masing dan setiap karakter CR atau LF harus dilewati ke baris berikutnya KECUALI ...
  • ... jika CR segera diikuti oleh LF ( \u000D\u000A) maka keduanya bersama - sama hanya melewati satu baris.
  • String.Empty adalah satu-satunya input yang tidak menghasilkan baris (karakter apa pun memerlukan setidaknya satu baris)
  • Baris terakhir harus dikembalikan walaupun tidak memiliki CR atau LF.

Aturan sebelumnya menjelaskan perilaku StringReader.ReadLine dan fungsi terkait, dan fungsi yang ditunjukkan di bawah ini menghasilkan hasil yang identik. Ini adalah fungsi melanggar garis C # yang efisien yang dengan patuh mengimplementasikan pedoman ini untuk menangani urutan atau kombinasi arbitrer / CR / LF yang sewenang-wenang. Baris yang disebutkan tidak mengandung karakter CR / LF. Baris kosong dipertahankan dan dikembalikan sebagai 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));
}

Catatan: Jika Anda tidak keberatan overhead menciptakan StringReader instance pada setiap panggilan, Anda dapat menggunakan kode C # 7 berikut . Seperti disebutkan, sementara contoh di atas mungkin sedikit lebih efisien, kedua fungsi ini menghasilkan hasil yang sama persis.

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
sumber