最稳妥方案是用watchdog库监听目录,结合文件名模式与修改时间动态切换读取目标,并用缓冲区拼接跨readline的多行日志。

用 time.sleep() 轮询文件末尾是最简单但要注意 CPU 占用
Python 没有内置的阻塞式“tail -f”等价物,最直接的做法是循环检查文件大小变化,读新内容。核心逻辑就是:记录上次读到的位置 → 等待 → 检查文件是否增长 → 从上次位置读新增行。
常见错误现象:time.sleep(0.1) 太小导致频繁唤醒、CPU 占用高;sleep(5) 太大导致日志延迟明显;没处理文件被 logrotate 清空或重命名的情况,导致漏读或报错。
- 推荐起始值用
time.sleep(0.5),再根据实际吞吐量调整 - 每次读之前先
os.stat(filepath).st_size获取当前大小,比直接seek(0, 2)更可靠 - 如果日志可能被轮转(比如
app.log→app.log.1),仅靠文件大小不够,得加os.path.getinode()判断是否还是同一个文件
用 select.poll() 监听文件描述符在 Linux 上更高效但不跨平台
Linux 下可以对普通文件 fd 使用 select.poll(),配合 IN_MODIFY 事件实现近似 inotify 的效果——不过注意:普通文件不触发 IN_MODIFY,只有某些特殊文件系统(如 procfs)或内核版本较新时才可能生效。多数情况下它只对管道、socket、终端有效,对磁盘日志文件基本无效。
所以别被网上某些示例误导:用 poll().register(fd, select.POLLIN) 去监听日志文件,大概率会一直阻塞或立刻返回无事件。
立即学习“Python免费学习笔记(深入)”;
- 真正可用的替代方案是用
inotify(需安装pyinotify)或watchdog库监听目录变更 -
watchdog的FileSystemEventHandler.on_modified()可捕获文件内容变化,但注意它不保证“内容已刷盘”,可能读到半截行 - 若坚持用底层机制,
inotify的IN_MOVED_TO+IN_CREATE才能可靠捕获 logrotate 场景
用 watchdog 库处理轮转和多行日志更稳妥
真实生产环境里,日志文件几乎一定会被轮转、压缩、重命名。这时候只盯着一个文件名读,很容易断档。用 watchdog 可以监听整个目录,结合文件名模式(如 *.log)和修改时间,主动发现新文件并续读。
使用场景:你不能假设日志永远写在 app.log,而要接受它可能是 app.log.2024-06-15 或刚被 rename 出来的 app.log。
- 初始化时先按时间排序找到最新日志文件,从末尾开始读(用
file.seek(0, 2)) - 监听
on_created和on_moved,当检测到新文件或旧文件被移走,立即切换读取目标 - 每行日志可能跨多个
readline()调用(尤其 JSON 日志含换行符),建议用缓冲区拼接,直到遇到完整换行再解析 - 注意
watchdog默认用 inotify,不支持 macOS 或 Windows 的原生事件,跨平台需配Observer的不同 backend
用 subprocess.Popen 调用系统 tail -f 是最省事的兜底方案
如果你只需要“把 tail -f 的输出拿进 Python 处理”,而不是非得纯 Python 实现,那直接调用系统命令反而最稳——它天然支持符号链接更新、文件轮转重开、信号中断等 edge case。
性能影响很小,因为只是做一层管道桥接;兼容性上,只要系统有 tail(Linux/macOS 都自带),就不用操心内核版本或文件系统限制。
- 启动时加
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, universal_newlines=True - 用
for line in iter(proc.stdout.readline, ''):流式读取,避免阻塞 - 别忘了处理子进程异常退出:比如
tail被 kill、文件被删导致OSError: [Errno 5] Input/output error - Windows 下没有
tail,可改用Get-Content -Wait(PowerShell),但需注意编码和换行符差异
真正难的不是“怎么读最后一行”,而是“怎么确认这行确实写完了、没被截断、没被轮转跳过”。轮转时机、缓冲策略、编码边界、权限变化,任何一个环节松动都会导致日志丢失或重复。别迷信某一种方法能通吃所有部署环境。










