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


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.