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

2次阅读

不能用File.ReadAllLines读大日志文件,因其会将整个文件加载到内存导致OutOfMemoryException;应使用FileStream反向扫描换行符,逐字节从末尾向前读取并累计行数,兼容rn和n,注意文件共享、编码及并发写入问题。

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

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

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

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

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

常见坑:

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

封装成可复用的 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();         }         // 跳过 rn 两个字节         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 行中间,你刚好扫到那里,就可能解码失败或得到乱码。解决办法只有两个:

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

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

text=ZqhQzanResources