读取文本文件最后一行之前的两行

Read two lines before last line of text file

我有一个日志文件,我正试图从中获取一些信息。我需要的信息在最后一行之前的那一行,最后一行 is/could 是空白的。所以如果最后一行是空白,它实际上是最后一行之前的一行或最后一行之前的两行。

我知道如何使用以下方法到达文件的最后一行:

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)。

像这样的东西可能适合你

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

您可能需要使用更好的方法来计算 secondLastIdx

使用 C#8 的 range operator

如果你的数组已经在内存中并且可以使用 C# 8,你可以这样做:

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

没有 C#8。

或者,如 Tim 所述,您可以在索引器上进行算术运算:

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

然后“安全地”获取最后一行。

也许你可以使用这个扩展方法:

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,那么你不能使用Ranges,将return list[^countFromEnd]替换为return list[list.Count - countFromEnd]

File.ReadLines("SomeFile.log").Last(); 将遍历所有行并保留最后一行。这对于大文件来说可能很昂贵。至少它不会将它们全部保留在内存中。

更快的替代方法是读取最后 X 个字节,将它们转换为字符串并将其拆分为行。如果您有 UTF8 文件,这并不像听起来那么容易,因为块可能会丢失第一个字符的第一个字节。 This question asks how to do this 和 UTF8 留作 reader.

的练习

要检索 IEnumerable<T> 中的最后 N 项,您可以使用 .NET Core 引入的 TakeLast 方法:

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

还有SkipLast,所以如果你想要倒数第二行,你可以使用:

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

尽管只有 1 行,TakeLast(2).FirstOrDefault() 就足够了。

对于 .NET Framework,您可以使用类似 this answer's code or this one 的方法来迭代并保留最后 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;
        }
    }
}