Прочитать две строки перед последней строкой текстового файла

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

Я знаю, как добраться до последней строки файла, используя:

var lastLine = File.ReadLines("SomeFile.log").Last();

Я также могу использовать Linq для пропуска строк, используя .skipWhile() или .skip(1), но не возвращаясь назад.

Я не знаю, как добраться до нужной мне строки. Это пример последних нескольких строк файла журнала (последняя строка пуста):

2021/05/02 23:47:57:008989 send_status_message(2) Info: "Stream status heartbeat sent: [SY  1.3.2       ]"
2021/05/02 23:47:57:225172 send_status_message(2) Info: "Received heartbeat response: [S               ]"
2021/05/03 00:00:00:045055 set_log_dir(2) Info: "Changing log directory to /abc/def/logs/2021-05-03."
<blank-line>    

Я пытаюсь получить отметку времени в этой строке (например, 2021/05/02 23:47:57:225172).


person NoBullMan    schedule 02.06.2021    source источник
comment
Вы можете изменить свой список, используя метод Reverse()   -  person Jawad    schedule 02.06.2021
comment
@Jawad это будет читать и кэшировать все в памяти. Было бы дешевле использовать ReadAllLines и просто взять последние 2 строки   -  person Panagiotis Kanavos    schedule 02.06.2021
comment
Вам нужны последние две строки или предпоследняя строка?   -  person Tim Schmelter    schedule 02.06.2021
comment
Я не знаю заранее, пуста ли последняя строка или нет. Если это так, мне нужна третья строка снизу; если он не пустой, мне нужен второй снизу.   -  person NoBullMan    schedule 02.06.2021
comment
Кажется, комментарий @Jawad указал мне правильное направление. Кажется, это работает, даже если в моем файле журнала есть пустая последняя строка: var lastLines = File.ReadAllLines(Path_to_Log_File).Reverse().Take(2).Reverse();   -  person NoBullMan    schedule 02.06.2021


Ответы (4)


Возможно, вы могли бы использовать этот метод расширения:

public static class EnumerableExtensions
{
    public static T GetLastItem<T>(this IEnumerable<T> seq, int countFromEnd)
    {
        if(seq is IList<T> list) return list[^countFromEnd];
        using var enumerator = seq.Reverse().GetEnumerator();
        while(enumerator.MoveNext())
        {
            if(--countFromEnd == 0) return enumerator.Current;
        }
        throw new ArgumentOutOfRangeException();
    }
}

Применение:

var secondLastLine = File.ReadLines("SomeFile.log").GetLastItem(2);

Если вы не используете C#8, вы не можете использовать Диапазоны, замените return list[^countFromEnd] на return list[list.Count - countFromEnd].

person Tim Schmelter    schedule 02.06.2021
comment
@PanagiotisKanavos: Да, но OP, кажется, хочет одну строку, предпоследнюю. TakeLast(2) даст вам последние две строки. Но ты прав, я тоже всегда забываю об этом методе - person Tim Schmelter; 02.06.2021
comment
Также есть SkipLast, поэтому TakeLast(2).SkipLast(1). Или TakeLast(2).First() - person Panagiotis Kanavos; 02.06.2021
comment
@PanagiotisKanavos: Да, или TakeLast(2).ElementAtOrDefault(0). - person Tim Schmelter; 02.06.2021
comment
list.Length в разделе использования ответа пришлось изменить на list.Count. Также кажется, что использование var... для С# 8, которое я не использую. - person NoBullMan; 02.06.2021
comment
Большинство ответов, размещенных здесь, работали. Тем не менее, я отметил это как ответ, поскольку он использует расширение, которое я могу повторно использовать в других случаях. - person NoBullMan; 02.06.2021
comment
@NoBullMan: Ты прав, исправил. IList<T> имеет свойство Count, но работает и с массивами. Со всем остальным его нужно перечислить. И да, using var также является функцией C#8. Нужно заменить на using(var ...{}) - person Tim Schmelter; 02.06.2021

File.ReadLines("SomeFile.log").Last(); будет перебирать все строки и сохранять последнюю. Это может быть дорого для больших файлов. По крайней мере, он не держит их всех в памяти.

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

Чтобы получить последние N элементов в IEnumerable<T>, вы можете использовать TakeLast, введенный в .NET Core:

var lastLines = File.ReadLines("SomeFile.log").TakeLast(2);

Также есть SkipLast, поэтому, если вам нужна вторая с последней строки, вы можете использовать:

var secondLast = File.ReadLines("SomeFile.log").TakeLast(2).SkipLast(1);

Однако для одной строки будет достаточно TakeLast(2).FirstOrDefault().

Для .NET Framework вы можете использовать что-то вроде кода этого ответа или этот для повторения и сохранения последних N строк:

public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int count)
{
    if (source == null) { throw new ArgumentNullException("source"); }

    Queue<T> lastElements = new Queue<T>();
    foreach (T element in source)
    {
        lastElements.Enqueue(element);
        if (lastElements.Count > count)
        {
            lastElements.Dequeue();
        }
    }

    return lastElements;
}

Этот код нуждается в небольшом изменении, чтобы стать SkipLast(), возвращая удаленные из очереди элементы, а не отбрасывая их:

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int count)
{
    if (source == null) { throw new ArgumentNullException("source"); }

    Queue<T> lastElements = new Queue<T>();
    foreach (T element in source)
    {
        lastElements.Enqueue(element);
        if (lastElements.Count > count)
        {
            var head=lastElements.Dequeue();
            yield return head;
        }
    }
}
person Panagiotis Kanavos    schedule 02.06.2021

Использование диапазона C#8< /а>

Если у вас уже есть массив в памяти и вы можете использовать C# 8, вы можете сделать это:

var Lines = File.ReadAllLines("SomeFile.log");
var SecondToLast = Lines[^2];

Без C#8.

В качестве альтернативы, как упомянул Тим, вы можете выполнить арифметику в индексаторе:

var Lines = File.ReadAllLines("SomeFile.log");
var SecondToLast = Lines[Lines.Length - 2];

Редактирование на основе комментариев. Судя по вашим комментариям, вы не совсем уверены в том, сколько пустых строк вы получите. Если это так, вам может быть лучше использовать более общий подход, такой как этот:

    static string FirstNotEmpty(string[] Lines, bool BottomUp = false)
    {
        if (BottomUp)
        {
            for (int i = Lines.Length - 1; i >= 0; i--)
            {
                var CurrentLine = Lines[i];
                if (!string.IsNullOrWhiteSpace(CurrentLine))
                    return CurrentLine;
            }
        }
        else
        {
            for (int i = 0; i <= Lines.Length-1; i++)
            {
                var CurrentLine = Lines[i];
                if (!string.IsNullOrWhiteSpace(CurrentLine))
                    return CurrentLine;
            }
        }
        return null; //Or something else.
    }

В вашем случае вы бы назвали это так:

var FirstNotEmptyLine = FirstNotEmpty(Lines, BottomUp: true);

Вы также можете предварительно удалить пустые строки из вашего массива:

var WithoutEmptyLines = Lines.Where(x => !string.IsNullOrWhiteSpace(x));

И тогда смело получайте последнюю строчку.

person Mariano Luis Villa    schedule 02.06.2021
comment
@TimSchmelter Да, спасибо. - person Mariano Luis Villa; 02.06.2021
comment
Если OP использует C # 8 и хочет, чтобы предпоследняя, ​​а не последние две строки, это лучший ответ +1. Но вы можете предоставить альтернативу, если он не может использовать C # 8, который равен Lines[Lines.Length - 2]; (обрабатывает случай, когда есть только одна строка) - person Tim Schmelter; 02.06.2021
comment
WithoutEmptyLines.ElementAt(Lines.Length - 2) возвращает то, что я ищу. Очевидно, невозможно применить индексацию к IEnumerable (Lines [Lines.Length - 2]). - person NoBullMan; 02.06.2021
comment
вы можете добавить Range и Index к более ранним языковым версиям с пакетами nuget. В конце концов, все эти функции этих языков были разработаны и прототипированы в более ранних языковых версиях. Изменить: хотя tfm по-прежнему учитывает пакеты - person Brett Caswell; 02.06.2021

Что-то вроде этого может сделать это для вас

 var lines = System.IO.File.ReadLines(@"SomeFile.log");
 var secondLastIdx = lines.Count() - 2;
 var secondlast = lines.Skip(secondLastIdx ).First();

Возможно, вам понадобится что-то получше, чтобы понять secondLastIdx

person ShanieMoonlight    schedule 02.06.2021
comment
Это будет повторять все строки дважды. Вероятно, было бы дешевле загрузить все строки в память и оставить последние две. - person Panagiotis Kanavos; 02.06.2021