Найкращий спосіб розділити рядок на рядки


143

Як розділити багаторядковий рядок на рядки?

Я знаю цей шлях

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

виглядає трохи некрасиво і втрачає порожні рядки. Чи є краще рішення?



1
Мені подобається це рішення, я не знаю, як зробити це простіше. Другий параметр, звичайно, видаляє порожні місця.
— NappingRabbit

Відповіді:


172
  • Якщо це виглядає некрасиво, просто видаліть непотрібний ToCharArrayдзвінок.

  • Якщо ви хочете розділити їх на \nабо \r, у вас є два варіанти:

    • Використовуйте буквений масив - але це дасть вам порожні рядки для закінчень рядків у стилі Windows \r\n:

      var result = text.Split(new [] { '\r', '\n' });
    • Використовуйте регулярний вираз, як вказує Барт:

      var result = Regex.Split(text, "\r\n|\r|\n");
  • Якщо ви хочете зберегти порожні рядки, чому ви чітко говорите C # відкидати їх? ( StringSplitOptionsпараметр) - використовувати StringSplitOptions.Noneзамість цього.


2
Видалення ToCharArray зробить кодову платформу (NewLine може бути '\ n')
— Костянтин Спірін

1
@Will: з випадковим випадком, що ви посилалися на мене замість Костянтина: Я вважаю ( рішуче ), що код розбору повинен прагнути працювати на всіх платформах (тобто він також повинен читати текстові файли, кодовані на різних платформах, ніж виконуюча платформа ). Тож для розбору, Environment.NewLineнаскільки я стурбований, це недохід. Насправді, з усіх можливих рішень я віддаю перевагу тому, яке використовує регулярні вирази, оскільки тільки воно обробляє всі джерельні платформи правильно.
— Конрад Рудольф

2
@Hamish Ну просто подивіться на документацію перерахувань, або подивіться в оригінальному запитанні! Це StringSplitOptions.RemoveEmptyEntries.
— Конрад Рудольф

8
Як щодо тексту, який містить '\ r \ n \ r \ n'. string.Split поверне 4 порожні рядки, проте при '\ r \ n' він повинен дати 2. Це стає гірше, якщо '\ r \ n' і '\ r' змішані в одному файлі.
— ім’я користувача

1
@SurikovPavel Використовуйте регулярний вираз. Це, безумовно, кращий варіант, оскільки він працює правильно з будь-якою комбінацією закінчень рядків.
— Конрад Рудольф

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

12
Це, на мою суб'єктивну думку, найчистіший підхід.
— примо

5
Будь-яка ідея щодо продуктивності (порівняно з string.Splitчи Regex.Split)?
— Уве

52

Оновлення: Див тут для вирішення Альтернативи / асинхронним.


Це чудово працює і швидше, ніж Regex:

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

Важливо мати "\r\n"спочатку в масиві, щоб він сприймався як один розрив рядка. Вищезазначене дає ті ж результати, що і будь-яке з цих рішень Regex:

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

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

За винятком того, що Regex виявляється приблизно в 10 разів повільніше. Ось мій тест:

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

Вихід:

00: 00: 03,8527616

00: 00: 31.8017726

00: 00: 32,5557128

і ось метод розширення:

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

Використання:

input.GetLines()      // keeps empty lines

input.GetLines(true)  // removes empty lines

Будь ласка, додайте ще детальну інформацію, щоб зробити вашу відповідь кориснішою для читачів.
— Мохіт Джайн

Зроблено. Також додали тест для порівняння його продуктивності з розчином Regex.
— орад

Дещо швидший зразок через менший [\r\n]{1,2}
— зворотний трек

@OmegaMan Це по-різному. Він буде відповідати \n\rабо \n\nяк єдиний розрив рядка, що невірно.
— орд

3
@OmegaMan Як складається кращий Hello\n\nworld\n\nрегістр? Це чітко один рядок з текстом, за ним порожній рядок, за ним інший рядок з текстом, а потім порожній рядок.
— Брандін

36

Ви можете використовувати Regex.Split:

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

Редагувати: додано |\rдо облікового запису для старих термінаторів Mac.


Це не працює в текстових файлах стилю OS X, оскільки вони використовуються лише \rяк закінчення рядка.
— Конрад Рудольф

2
@Konrad Rudolph: AFAIK, '\ r' використовувався в дуже старих системах MacOS і майже ніколи не зустрічається. Але якщо ОП потрібно це врахувати (або якщо я помиляюся), то регулярний вираз можна легко продовжити, щоб врахувати це, звичайно: \ r? \ N | \ r
— Барт Кірс

@Bart: Я не думаю , що ви помиляєшся , але я вже неодноразово стикався всі можливі кінцівки лінії в моїй кар'єрі програміста.
— Конрад Рудольф

@Konrad, ти, мабуть, маєш рацію. Я вважаю, що краще безпечно, ніж шкода.
— Барт Кіерс

1
@ ΩmegaMan: Це втратить порожні рядки, наприклад \ n \ n
— Майк Рософт


4

У мене був інший відповідь , але цей, заснований на Джека відповідь , значно швидше , може бути кращим , оскільки він працює в асинхронному режимі, хоча і дещо повільніше.

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

Використання:

input.GetLines()      // keeps empty lines

input.GetLines(true)  // removes empty lines

Тест:

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

Вихід:

00: 00: 03.9603894

00: 00: 00.0029996

00: 00: 04,8221971


Мені цікаво, чи це тому, що ви насправді не перевіряєте результати перелічувача, і тому він не виконується. На жаль, я лінивий перевірити.
— Джеймс Голвелл

Так, це насправді !! Коли ви додаєте .ToList () до обох викликів, рішення StringReader насправді повільніше! На моїй машині це 6.74s проти 5.10s
— JCH2k

Що має сенс. Я все ще віддаю перевагу цьому методу, тому що він дозволяє мені асинхронно отримувати лінії.
— орад

Можливо, вам слід видалити заголовок "кращого рішення" з іншої відповіді та відредагувати цю ...
— JCH2k


2

Трохи скручений, але ітераторний блок для цього:

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

Потім ви можете зателефонувати:

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

Складно правильно поводитися із змішаними закінченнями рядків. Як ми знаємо, символи завершення рядка може бути « переклад рядка» (ASCII 10, \n, \x0A, \u000A), «Повернення каретки» (ASCII 13, \r, \x0D, \u000D), або який - або їх поєднання. Повертаючись до DOS, Windows використовує двосимвольну послідовність CR-LF \u000D\u000A, тому ця комбінація повинна випромінювати лише один рядок. У Unix використовується один \u000A, і дуже старі Macs використовували один \u000Dсимвол. Стандартний спосіб обробки довільних сумішей цих символів в одному текстовому файлі полягає в наступному:

  • кожен символ CR або LF повинен переходити до наступного рядка ЗА ВСЕ ...
  • ... якщо після одразу після КР негайно слідує LF ( \u000D\u000A), то ці два разом пропускають лише один рядок.
  • String.Empty це єдиний вхід, який не повертає жодних рядків (будь-який символ тягне за собою хоча б один рядок)
  • Останній рядок повинен бути повернутий, навіть якщо в ньому немає ні CR, ні LF.

Попереднє правило описує поведінку StringReader.ReadLine та пов'язаних з ними функцій, а наведена нижче функція дає однакові результати. Це ефективна функція розриву рядків C #, яка достойно реалізує ці вказівки, щоб правильно обробляти будь-яку довільну послідовність або комбінацію CR / LF. Перелічені рядки не містять символів CR / LF. Порожні рядки зберігаються і повертаються як 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));
}

Примітка. Якщо ви не заперечуєте накладні витрати на створення StringReaderпримірника під час кожного виклику, замість цього ви можете використовувати наступний код C # 7 . Як зазначалося, хоча приклад вище може бути дещо ефективнішим, обидві ці функції дають абсолютно однакові результати.

public static IEnumerable<String> Lines(this String s)
{
    using (var tr = new StringReader(s))
        while (tr.ReadLine() is String L)
            yield return L;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.