Построчное чтение файла на C #

Я пытаюсь прочитать некоторые текстовые файлы, где нужно обработать каждую строку. На данный момент я просто использую StreamReader, а затем читаю каждую строку по отдельности.

Мне интересно, есть ли более эффективный способ (с точки зрения LoC и удобочитаемости) сделать это с помощью LINQ без ущерба для операционной эффективности. Примеры, которые я видел, включают загрузку всего файла в память, а затем его обработку. Однако в данном случае я не думаю, что это было бы очень эффективно. В первом примере файлы могут достигать примерно 50 КБ, а во втором примере не все строки файла должны быть прочитаны (размеры обычно <10 КБ).

Вы можете возразить, что в настоящее время это не имеет особого значения для этих небольших файлов, однако я считаю, что такой подход приводит к неэффективному коду.

Первый пример:

// Open file
using(var file = System.IO.File.OpenText(_LstFilename))
{
    // Read file
    while (!file.EndOfStream)
    {
        String line = file.ReadLine();

        // Ignore empty lines
        if (line.Length > 0)
        {
            // Create addon
            T addon = new T();
            addon.Load(line, _BaseDir);

            // Add to collection
            collection.Add(addon);
        }
    }
}

Второй пример:

// Open file
using (var file = System.IO.File.OpenText(datFile))
{
    // Compile regexs
    Regex nameRegex = new Regex("IDENTIFY (.*)");

    while (!file.EndOfStream)
    {
        String line = file.ReadLine();

        // Check name
        Match m = nameRegex.Match(line);
        if (m.Success)
        {
            _Name = m.Groups[1].Value;

            // Remove me when other values are read
            break;
        }
    }
}

person Luca Spiller    schedule 13.08.2009    source источник
comment
50 КБ даже не достаточно велик, чтобы поместить его в кучу больших объектов. Потоковая передача имеет смысл, когда размер ваших файлов составляет мегабайт (или больше), а не килобайты.   -  person Joe Chung    schedule 13.08.2009


Ответы (5)


Вы можете довольно легко написать программу чтения строк на основе LINQ, используя блок итератора:

static IEnumerable<SomeType> ReadFrom(string file) {
    string line;
    using(var reader = File.OpenText(file)) {
        while((line = reader.ReadLine()) != null) {
            SomeType newRecord = /* parse line */
            yield return newRecord;
        }
    }
}

или сделать Джона счастливым:

static IEnumerable<string> ReadFrom(string file) {
    string line;
    using(var reader = File.OpenText(file)) {
        while((line = reader.ReadLine()) != null) {
            yield return line;
        }
    }
}
...
var typedSequence = from line in ReadFrom(path)
                    let record = ParseLine(line)
                    where record.Active // for example
                    select record.Key;

тогда у вас есть ReadFrom(...) как лениво вычисляемая последовательность без буферизации, идеально подходящая для Where и т. д.

Обратите внимание, что если вы используете OrderBy или стандартный GroupBy, ему придется буферизовать данные в памяти; Если вам нужна группировка и агрегирование, в PushLINQ есть необычный код, позволяющий выполнять агрегирование данных, но отбрасывать их (без буферизации). Объяснение Джона здесь.

person Marc Gravell    schedule 13.08.2009
comment
Ба, разделение задач - выделите чтение строки в отдельный итератор и используйте нормальную проекцию :) - person Jon Skeet; 13.08.2009
comment
Намного приятнее ... хотя по-прежнему зависит от файла;) - person Jon Skeet; 13.08.2009
comment
Не думаю, что твои примеры составят компиляцию. file уже определен как строковый параметр, поэтому вы не можете сделать это объявление в блоке using. - person Justin R.; 07.08.2010
comment
Отличная техника, спасибо, Марк! Если это поможет кому-то, я написал сообщение в блоге об использовании этого для чтения csv в linqpad: developertipoftheday.com/2012/10/read-csv-in-linqpad.html - person Alex KeySmith; 30.10.2012
comment
@JonSkeet ссылка, предоставленная Марком выше, более недействительна, не могли бы вы предоставить новую ссылку - person Mrinal Kamboj; 05.06.2015
comment
Я не понимал и не использовал yield до сегодняшнего дня - несмотря на 20 лет программирования на C #. Это такой хороший и простой пример! Спасибо и за это ;-) - person Tillito; 10.02.2017
comment
Framework теперь включает File.ReadLines () который делает то же самое, что и код выше static IEnumerable<string> ReadFrom(string file), верно? - person Stéphane Gourichon; 18.03.2019

Проще прочитать строку и проверить, является ли она нулевой, чем постоянно проверять наличие EndOfStream.

Однако у меня также есть класс LineReader в MiscUtil, который делает все это намного проще - в основном он предоставляет файл (или Func<TextReader> как IEnumerable<string>, который позволяет вам выполнять над ним работу LINQ. Таким образом, вы можете делать такие вещи, как:

var query = from file in Directory.GetFiles("*.log")
            from line in new LineReader(file)
            where line.Length > 0
            select new AddOn(line); // or whatever

В основе LineReader лежит реализация IEnumerable<string>.GetEnumerator:

public IEnumerator<string> GetEnumerator()
{
    using (TextReader reader = dataSource())
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }
}

Почти весь остальной исходный код просто предоставляет гибкие способы настройки dataSource (это Func<TextReader>).

person Jon Skeet    schedule 13.08.2009
comment
Как закрыть файл? И освободить ресурс? - person ca9163d9; 28.08.2014
comment
@ dc7a9163d9: Оператор using это уже делает - вызов dataSource() откроет файл, и поэтому он будет удален в конце оператора using. - person Jon Skeet; 28.08.2014

Начиная с .NET 4.0, File.ReadLines() доступен.

int count = File.ReadLines(filepath).Count(line => line.StartsWith(">"));
person user7610    schedule 10.04.2019

ПРИМЕЧАНИЕ. Вам нужно остерегаться решения IEnumerable<T>, так как оно приведет к тому, что файл будет открыт на время обработки.

Например, с ответом Марка Гравелла:

foreach(var record in ReadFrom("myfile.csv")) {
    DoLongProcessOn(record);
}

файл останется открытым в течение всей обработки.

person kͩeͣmͮpͥ ͩ    schedule 13.08.2009
comment
Верно, но файл открыт в течение длительного времени, но отсутствие буферизации часто лучше, чем большой объем памяти, загруженный в течение длительного времени. - person Marc Gravell; 13.08.2009
comment
Это правда, но в основном у вас есть три варианта: загрузить лот за один раз (не работает для больших файлов); держите файл открытым (как вы упомянули); регулярно открывать файл повторно (возникает ряд проблем). Во многих, многих случаях я считаю, что потоковая передача и сохранение файла в открытом состоянии - лучшее решение. - person Jon Skeet; 13.08.2009
comment
Да, вероятно, лучше оставить файл открытым, но вам просто нужно избегать последствий - person kͩeͣmͮpͥ ͩ; 13.08.2009
comment
Извините за опечатку в имени Марк - person kͩeͣmͮpͥ ͩ; 13.08.2009
comment
Об этом определенно следует помнить как о потенциально неожиданном побочном эффекте, но я также согласен с Джоном в том, что это звучит как лучшее решение. - person Mark LeMoine; 17.08.2010

Спасибо всем за ответы! Я решил пойти со смесью, в основном сосредоточившись на Марке, поскольку мне нужно будет только читать строки из файла. Думаю, можно утверждать, что разлука нужна везде, но хех, жизнь слишком коротка!

Что касается сохранения файла открытым, то в данном случае это не будет проблемой, поскольку код является частью настольного приложения.

Наконец, я заметил, что все вы использовали строчные буквы. Я знаю, что в Java есть разница между строкой с заглавной и без заглавной буквы, но я думал, что в C # строчная строка была просто ссылкой на строку с заглавной буквы?

public void Load(AddonCollection<T> collection)
{
    // read from file
    var query =
        from line in LineReader(_LstFilename)
        where line.Length > 0
        select CreateAddon(line);

    // add results to collection
    collection.AddRange(query);
}

protected T CreateAddon(String line)
{
    // create addon
    T addon = new T();
    addon.Load(line, _BaseDir);

    return addon;
}

protected static IEnumerable<String> LineReader(String fileName)
{
    String line;
    using (var file = System.IO.File.OpenText(fileName))
    {
        // read each line, ensuring not null (EOF)
        while ((line = file.ReadLine()) != null)
        {
            // return trimmed line
            yield return line.Trim();
        }
    }
}
person Luca Spiller    schedule 13.08.2009
comment
Почему вы передаете коллекцию в метод Load? По крайней мере, назовите его LoadInto, если вы собираетесь это сделать;) - person kͩeͣmͮpͥ ͩ; 20.08.2009