python实现tail-f需用seek(0,2)定位末尾,循环readline()并检查inode与大小变化以应对logrotate;需捕获ioerror/oserror,避免因文件重命名或截断导致漏读或卡死。

用 tail -f 的思路在 Python 里读取追加内容
Python 没有内置的“实时 tail”功能,但核心逻辑很简单:打开文件、移动到末尾、循环检查是否有新行。关键不是“怎么读”,而是“怎么判断有新数据”——不能靠轮询 os.stat().st_size 然后反复 readline(),那样在日志滚动或文件被截断时会丢数据或卡住。
实际生产中推荐用 watchdog 库监听文件变更事件,但若只跟踪单个正在追加的日志文件(比如 Nginx access.log),用底层 I/O 控制更轻量、更可控:
- 用
file.seek(0, 2)定位到文件末尾(2表示os.SEEK_END) - 每次循环前用
file.tell()记下当前位置,再尝试file.readline() - 如果返回空字符串,说明没新行,
time.sleep(0.1)后重试;否则立即处理该行 - 必须捕获
IOError和OSError,因为文件可能被 logrotate 清空或重命名
处理日志轮转(logrotate)时的文件丢失问题
Linux 下 tail -f 能感知文件被重命名或清空,Python 默认做不到。典型错误是:logrotate 执行 copytruncate 后,你的脚本还在原 inode 上读,但文件已被清空,tell() 位置远大于当前大小,导致永远读不到新内容。
安全做法是定期检查文件是否“变小了”或 inode 是否变化:
- 每次循环开始前调用
os.stat(filepath),对比st_ino和st_size - 如果
st_size ,说明文件被截断,应 <code>file.seek(0, 2)重置到新末尾 - 如果
st_ino != last_ino,说明文件被替换(如mv access.log access.log.1 && touch access.log),需关闭旧句柄、重新open() - 不要依赖
file.closed判断,要主动try/except捕获IOError: [Errno 9] Bad file descriptor
用 select.poll() 替代轮询提升响应性(Linux only)
纯 sleep 轮询有延迟,且浪费 CPU。Linux 下可借助 select.poll() 监听文件描述符的可读事件,做到“有数据立刻触发”,效果接近 tail -f 的零延迟:
import select fd = file.fileno() poller = select.poll() poller.register(fd, select.POLLIN) # ……然后 poller.poll(timeout_ms) 返回非空即有新数据
注意:POLLIN 对普通文件**不一定生效**(POSIX 不保证),它在 inotify 或 FIFO 上更可靠;但实测多数 ext4 上对追加写入的普通日志文件也有效。若不可用,回退到 sleep 轮询即可,不必强求。
- 仅限 Linux,macOS 用
kqueue,Windows 无等效机制 - 必须确保文件以
open(..., buffering=1)(行缓冲)或0(无缓冲)打开,否则内核事件可能被 Python 缓冲层拦截 -
poll()返回后仍要调用readline(),不能直接读 raw bytes,否则会切碎 UTF-8 字符
别忽略编码和换行符的隐式陷阱
你看到的“一行”,Python 可能读不全。常见坑:
- 日志里含中文或 emoji 时,若用
open(..., encoding='gbk')而文件是 UTF-8,readline()会抛UnicodeDecodeError并中断整个跟踪 - 某些程序(如 Java 的 Log4j)写日志时不带换行符(
\n),或用\r\n,而readline()默认只认\n,导致最后一行一直卡在缓冲区 - 解决方案:用
open(..., newline='', encoding='utf-8', errors='replace')——newline=''让readline()原样返回含\r\n的行,errors='replace'防止解码失败崩溃
真正难的不是“怎么实现 tail”,而是当文件被并发写入、轮转、编码混杂、权限突变时,你的脚本能安静地跟下去,而不是某天凌晨三点默默停掉。这些边界条件,比主逻辑更值得花时间写测试。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











