C#读取大文件最后几行 C#如何高效获取日志文件的最新内容

来源:这里教程网 时间:2026-02-21 17:42:09 作者:

为什么不能用
File.ReadAllLines
读大日志文件

直接加载整个文件到内存会触发

OutOfMemoryException
,尤其当日志超几百 MB 甚至 GB 时。Windows 下单个 .NET 进程在 32 位模式下默认只能用 2GB 虚拟地址空间,64 位虽宽裕但依然扛不住几十 GB 的日志。更关键的是,你只关心最后几行,却把前面几百万行全读进来再丢弃——纯属浪费 CPU 和 GC 压力。

FileStream
从文件末尾反向扫描换行符

核心思路是:打开文件流,

Seek
到末尾,然后逐字节往前读,统计
'\n'
(Unix/Linux)或
"\r\n"
(Windows)出现的次数,直到凑够目标行数或碰到文件开头。注意必须用
FileStream
+
BinaryReader
或手动
ReadByte
,不能用
StreamReader
,因为它内部缓冲机制会破坏反向定位逻辑。

常见坑:

File.OpenRead(path)
默认不支持
Seek
,得用
FileMode.Open
+
FileAccess.Read
+
FileShare.Read
显式打开
Windows 日志可能含
\r\n
,Linux 是
\n
,需兼容判断;若文件以
\r\n
结尾,末尾可能多出一个空行,要跳过
文件编码不确定时,别假设是 UTF8;建议按字节处理换行符,行内容再用
Encoding.Default
Encoding.UTF8
解码(日志通常不带 BOM,
UTF8
更安全)

封装成可复用的
ReadTailLines
方法

下面是一个轻量实现,返回

IEnumerable<string></string>
,支持延迟执行、避免一次性分配大数组:

public static IEnumerable<string> ReadTailLines(string path, int lineCount = 10)
{
    using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan);
    if (fs.Length == 0) yield break;
<pre class="brush:php;toolbar:false;">var buffer = new byte[1];
long pos = fs.Length - 1;
int linesFound = 0;
var lineBuilder = new List<byte>();
while (pos >= 0 && linesFound < lineCount)
{
    fs.Seek(pos, SeekOrigin.Begin);
    fs.Read(buffer, 0, 1);
    if (buffer[0] == '\n' || (buffer[0] == '\r' && pos > 0 && fs.ReadByte() == '\n'))
    {
        if (lineBuilder.Count > 0)
        {
            linesFound++;
            yield return Encoding.UTF8.GetString(lineBuilder.AsReadOnly().ToArray());
            lineBuilder.Clear();
        }
        // 跳过 \r\n 两个字节
        if (buffer[0] == '\r') pos--;
    }
    else if (pos == 0 || buffer[0] == '\r')
    {
        // 文件开头或孤立 \r,也视为一行结束
        if (lineBuilder.Count > 0 || pos == 0)
        {
            linesFound++;
            if (pos == 0 && buffer[0] != '\n' && buffer[0] != '\r')
                lineBuilder.Add(buffer[0]);
            yield return Encoding.UTF8.GetString(lineBuilder.AsReadOnly().ToArray());
        }
    }
    else
    {
        lineBuilder.Insert(0, buffer[0]);
    }
    pos--;
}
// 处理第一行(文件开头没换行的情况)
if (linesFound < lineCount && lineBuilder.Count > 0)
{
    yield return Encoding.UTF8.GetString(lineBuilder.ToArray());
}

}

生产环境要注意文件被其他进程写入

日志文件常被追加写入,

FileStream
打开时若没加
FileShare.Write
,可能抛
IOException
;但加了之后,需接受“读到半截行”的风险——比如某次写入正在写入第 1001 行中间,你刚好扫到那里,就可能解码失败或得到乱码。解决办法只有两个:

捕获
DecoderFallbackException
ArgumentException
,对异常行用
Encoding.GetChars
加容错解码
更稳妥的做法是:先
GetLastWriteTime
记录时间戳,再用
FileSystemWatcher
监听
Changed
事件,仅当文件大小增长且修改时间更新后才重新读尾部——这适合轮询场景,避免高频扫描

真正难处理的是多进程并发写同一个日志文件(如多个服务实例共用一个 log.txt),此时连文件长度都可能不准;这种架构本身就有问题,优先考虑换成按日期分片或用

ConcurrentQueue
+ 单独日志线程落盘。

相关推荐