如何在Python中追踪日志文件?

102
我想在Python中获取类似tail -F的输出,而不会阻塞或锁定。我已经找到了一些非常古老的代码(链接)来实现这个目的,但我认为现在一定有更好的方法或库来做同样的事情。有人知道吗?
理想情况下,我想要像tail.getNewData()这样的东西,每次我需要更多数据时都可以调用它。

1
subprocess.call(["tail", "-F", filename]) - Whymarrh
1
@Avaris,那个答案不是“跟随”尾部。 - Keith
2
你的虚构的 get_new_data 方法(PEP-8 命名)需要返回自上次调用以来的所有数据,还是只返回当前尾部(可能会丢失一些数据)? - Keith
1
Keith:自上次调用以来的所有新数据。 - Eli
上次我需要这个功能时,最终我使用了 tail -F filename | python script.py 命令,因为我不需要 stdin 用于其他任何目的,并且这样可以获得最佳性能。 - Jasen
显示剩余8条评论
14个回答

82

非阻塞

如果您使用的是Linux操作系统(因为Windows不支持在文件上调用select),您可以使用subprocess模块和select模块。

import time
import subprocess
import select

f = subprocess.Popen(['tail','-F',filename],\
        stdout=subprocess.PIPE,stderr=subprocess.PIPE)
p = select.poll()
p.register(f.stdout)

while True:
    if p.poll(1):
        print f.stdout.readline()
    time.sleep(1)

当有新数据可用时,此代码会轮询输出管道并将其打印出来。通常情况下,time.sleep(1)print f.stdout.readline() 将被替换为实用的代码。

阻塞

您可以使用subprocess模块而不需要额外的select模块调用。

import subprocess
f = subprocess.Popen(['tail','-F',filename],\
        stdout=subprocess.PIPE,stderr=subprocess.PIPE)
while True:
    line = f.stdout.readline()
    print line

这也会在新行被添加时打印出来,但它会阻塞直到尾部程序关闭,可能是通过f.kill()


4
在“阻塞”解决方案中,不要使用print line,而是使用sys.stdout.write(line)来处理print会插入的额外换行符。 - Mayank Jaiswal
2
@mork 是否有多余的换行符被打印出来了?无论如何,我相信.strip()也会删除可能很重要的前导空格。 - Matt
原始日志文件中的换行符——通常在逐行迭代时,您更喜欢只有“line”字符串。但如果前导空格很重要——您是正确的,@Mayank Jaiswal的解决方案将是一个不错的选择。 - mork
@mork 我已经有一段时间没有处理这段代码了,但是如果.readline()保留了换行符,而print又添加了一个新的换行符,那么只需要使用sys.stdout.write()代替print就可以轻松解决问题。 - Matt
3
最多每秒只能读取一行日志,如果日志每秒钟增加超过一行,则会出现问题。 - Daniel Waltrip
显示剩余8条评论

55

使用sh模块(pip install sh):

from sh import tail
# runs forever
for line in tail("-f", "/var/log/some_log_file.log", _iter=True):
    print(line)

[更新]

由于使用 _iter=True 的 sh.tail 是一个生成器,因此您可以:

import sh
tail = sh.tail("-f", "/var/log/some_log_file.log", _iter=True)

然后你可以使用以下代码获取新的数据:

new_data = tail.next()
注意,如果尾缓冲区为空,则它将阻塞直到有更多的数据(从您的问题中不清楚您在这种情况下想要做什么)。
[更新]
这会在使用“-F”替换“-f”时工作,但在Python中会被锁定。如果可能的话,我更感兴趣的是拥有一个可以调用以获取新数据的函数。将尾调用放入while True循环中并捕获可能的I/O异常的容器生成器将几乎具有与-F相同的效果。
def tail_F(some_file):
    while True:
        try:
            for line in sh.tail("-f", some_file, _iter=True):
                yield line
        except sh.ErrorReturnCode_1:
            yield None
如果文件变得不可访问,生成器将返回None。然而,如果文件仍然可访问,它仍会阻塞直到有新数据。在这种情况下,我仍然不清楚您想要做什么。
Raymond Hettinger的方法似乎非常不错:
def tail_F(some_file):
    first_call = True
    while True:
        try:
            with open(some_file) as input:
                if first_call:
                    input.seek(0, 2)
                    first_call = False
                latest_data = input.read()
                while True:
                    if '\n' not in latest_data:
                        latest_data += input.read()
                        if '\n' not in latest_data:
                            yield ''
                            if not os.path.isfile(some_file):
                                break
                            continue
                    latest_lines = latest_data.split('\n')
                    if latest_data[-1] != '\n':
                        latest_data = latest_lines[-1]
                    else:
                        latest_data = input.read()
                    for line in latest_lines[:-1]:
                        yield line + '\n'
        except IOError:
            yield ''

如果文件变得不可访问或者没有新的数据,这个生成器会返回''。

[更新]

似乎最后一个答案在运行到数据末尾时会回到文件开头。- Eli

我认为第二种方法将在tail处理结束时输出最后的十行数据,而使用-f时,每当出现I/O错误时就会结束。在类Unix环境中,tail --follow --retry的行为与此并没有太大差异。

也许如果您更新问题,并解释一下您的真实目标(即您想模仿tail--retry的原因),您将获得更好的答案。

最后一个答案实际上没有跟随tail,只是在运行时读取了可用的内容。- Eli

当然,默认情况下,tail会显示最后10行...您可以使用file.seek将文件指针定位到文件末尾,我会留下一个适当的实现作为读者的练习。

在我看来,使用file.read()的方法比基于子进程的解决方案更加优雅。


似乎每次运行到文件末尾之前,倒数第二个答案会回到文件顶部。而最后一个答案并不实际跟随尾部,仅在运行时读取可用内容。 - Eli
1
@Eli:执行 seek(0, 2) 操作会将文件指针移动到文件末尾。 - Paulo Scardine
1
只是好奇:对你来说,file.read()方法有什么更优雅的地方? tail 可以正确处理显示文件的最后10行(即使这些行很大),可以永远读取新行,可以在平台相关的方式下在新行到达时唤醒,还可以在需要时打开新文件。一言以蔽之,该实用程序非常适合其设计目的 - 重新实现它似乎不太优雅。(但我必须承认,sh 模块非常棒。) - nneonneo
sh模块看起来很酷,但我不明白相比subprocess模块你能得到什么好处,特别是因为subprocess模块是Python标准库的一部分,而sh不是。 - Matt
在Hettinger方法中,为什么要使用read()并搜索\n?为什么不使用readline()?在我看来,readline()似乎是一种更平台无关的获取每行的方式。 - Bill Rosmus
显示剩余5条评论

38
纯粹的Python解决方案,使用非阻塞的readline()函数。
我正在改编Ijaz Ahmad Khan的答案,只有在完整写入行时才产生输出(行以换行符结尾),这提供了一个没有外部依赖的Python解决方案。
import time
from typing import Iterator

def follow(file, sleep_sec=0.1) -> Iterator[str]:
    """ Yield each line from a file as they are written.
    `sleep_sec` is the time to sleep after empty reads. """
    line = ''
    while True:
        tmp = file.readline()
        if tmp is not None and tmp != "":
            line += tmp
            if line.endswith("\n"):
                yield line
                line = ''
        elif sleep_sec:
            time.sleep(sleep_sec)


if __name__ == '__main__':
    with open("test.txt", 'r') as file:
        for line in follow(file):
            print(line, end='')

2
Iljaz Ahmad的解决方案不仅更符合Python编程风格,而且还可以避免生成新的进程,节省资源,并根据情况可能实现更好的可扩展性。 - creativecoding
3
@creativecoding 这个答案确实比之前建议生成 tail -f 实例的任何答案都要好得多。已点赞。 - Bklyn
2
"else if"不是Python - 编辑并同意这比shtail更好 - JimmyNJ
我得到了错误:NameError: name 'Iterator' is not defined,为什么? - Malou
2
文件空闲时负载高,但通过将 if tmp is not None 更改为 if tmp != "" 很容易解决。 - personal_cloud
显示剩余3条评论

31

目前唯一可移植的 tail -f 文件的方法是从文件中读取数据,并且在 read 返回 0 后经过 sleep 后再次尝试。不同平台上的 tail 实用程序使用特定于平台的技巧(例如 BSD 上的 kqueue)来高效地追踪一个文件,而不需要使用 sleep

因此,纯粹使用 Python 实现良好的 tail -f 可能不是一个好主意,因为你必须使用最低公共分母的实现(不能使用特定于平台的技巧)。通过使用简单的 subprocess 打开 tail -f 并在单独的线程中迭代行,您可以轻松在Python中实现非阻塞的 tail 操作。

示例实现:

import threading, Queue, subprocess
tailq = Queue.Queue(maxsize=10) # buffer at most 100 lines

def tail_forever(fn):
    p = subprocess.Popen(["tail", "-f", fn], stdout=subprocess.PIPE)
    while 1:
        line = p.stdout.readline()
        tailq.put(line)
        if not line:
            break

threading.Thread(target=tail_forever, args=(fn,)).start()

print tailq.get() # blocks
print tailq.get_nowait() # throws Queue.Empty if there are no lines to read

4
如果OP的主要关注点不是摆脱对外部命令(tail)的依赖,那么他应该遵循Unix传统的方法,编写日志处理应用程序以从标准输入读取并将“tail -F”传送到其中。我看不出为什么添加线程、队列和子进程的复杂性会比传统方法带来任何优势。 - Paulo Scardine
他什么时候说他在写日志处理器? - nneonneo
11
虽然英语不是我的母语,但我猜题目(How can I tail a log file in Python?)的意思可以推断出来。 - Paulo Scardine
你知道在Linux中tail -F是如何高效工作的吗?它使用sleep还是更高效的事件系统? - CMCDragonkai
tail 在 Linux 上使用 inotify 和 select 的组合;请参阅源代码:https://github.com/coreutils/coreutils/blob/master/src/tail.c#L1453 - nneonneo
我发现这非常有用。在我的情况下,日志文件位于运行freeBSD的NAS上,因此我使用了ssh,并让bsd内核使tail -f高效:它确实很高效!可以看到随着它们到达NAS的行出现。需要低延迟,这个方法起作用了。(写入来自VM,将虚拟com端口写入NAS上的原始文件,记录来自科学仪器的数据)。 - RGD2

18

所有使用tail -f的答案都不符合Pythonic风格。

以下是Pythonic的方式:(不使用任何外部工具或库)

def follow(thefile):
     while True:
        line = thefile.readline()
        if not line or not line.endswith('\n'):
            time.sleep(0.1)
            continue
        yield line



if __name__ == '__main__':
    logfile = open("run/foo/access-log","r")
    loglines = follow(logfile)
    for line in loglines:
        print(line, end='')

2
如果一个日志文件在2个系统调用中被追加,那么这种“跟踪”文件的方式有时会返回行的2个部分,而不是整个行本身。 - Ferrybig
1
我已经发布了一个回答来解决@Ferrybig指出的错误:https://dev59.com/yGcs5IYBdhLWcg3w3HkG#54263201 - Isaac Turner
如果另一个Python程序正在使用writer向该文件写入内容,那么在writer停止写入时,我们是否有办法通过编程来停止此操作? - codeslord
是的,您可以使用类似锁定的机制在写入之前获取锁定,并在完成后释放它。 - Ijaz Ahmad

15

所以,这可能来得有点晚,但我再次遇到了同样的问题,现在有一个更好的解决方案。只需使用pygtail

Pygtail读取尚未读取的日志文件行。它甚至可以处理已经被旋转的日志文件。基于logcheck的logtail2 (http://logcheck.org)


请注意,它的行为不完全像tail,但根据需要可能会很有用。 - Haroldo_OK

13

理想情况下,我希望有类似于tail.getNewData()的方法,每次调用它都可以获取更多的数据。

我们已经有一个非常不错的方法。 每当需要更多数据时,只需调用f.read()。它将从前一次读取结束的地方开始读取,并一直读取到数据流的末尾:

f = open('somefile.log')
p = 0
while True:
    f.seek(p)
    latest_data = f.read()
    p = f.tell()
    if latest_data:
        print latest_data
        print str(p).center(10).center(80, '=')

要逐行读取,请使用f.readline()。有时,被读取的文件可能以部分读取的行结尾。使用f.tell()查找当前文件位置,并使用f.seek()将文件指针移回不完整行的开头来处理该情况。有关可工作代码,请参见此ActiveState配方


1
重点是我想要跟踪文件。如果我打开一个文件,f.read() 只会读取到运行时文件的结尾。它不会读取之后添加的任何内容。 - Eli
1
在发布之前,我进行了测试。我只是执行了以下操作: blah = open('some_file', r) while 1: sleep(1) print blah.read()并尝试向文件中写入内容,但没有成功。 - Eli
1
@Eli:你应该在Windows系统中。这是你问题中缺失的重要信息。 - Paulo Scardine
11
@Paulo:这个答案缺少重要信息。如果没有指定操作系统,你需要构建一个通用的工作环境,或者至少是在*nix上运行。你不能假设是Windows系统。 - Eli
为什么不能假设使用Windows?Python更接近于Windows而不是Nix,例如:UTF-16与UTF-8。 - Jasen
能否请您进一步解释一下 str(p).center(10).center(80, '=') 中的 print 所做的事情? - openCivilisation

7
你可以使用“tailer”库:https://pypi.python.org/pypi/tailer/ 它有一个选项可以获取最后几行:
# Get the last 3 lines of the file
tailer.tail(open('test.txt'), 3)
# ['Line 9', 'Line 10', 'Line 11']

它还可以跟踪一个文件:

# Follow the file as it grows
for line in tailer.follow(open('test.txt')):
    print line

如果想要实现类似尾部的行为,这个选项似乎是一个不错的选择。

1
它在文件被删除/重新创建后没有遵循相同的follow(),所以对我没有起作用 :/ - Jose Alban
1
@JoseAlban,库不负责监视文件的删除/创建,可以使用pypi模块“让所有东西自己工作”来解决。 - Pavel Vlasov
@Kentzo的回答涵盖了这个疏忽:https://dev59.com/yGcs5IYBdhLWcg3w3HkG#35570826 - Haroldo_OK

5
另一个选择是tailhead库,它提供了tailhead实用程序的Python版本以及可用于自己模块的API。最初基于tailer模块,其主要优点是能够按路径跟踪文件,即可以处理文件重新创建的情况。此外,它还解决了各种边缘情况的一些错误。

1
Python是“电池包含式”的 - 它有一个很好的解决方案:https://pypi.python.org/pypi/pygtail
读取未被读取的日志文件行。记住上次完成的位置,并从那里继续。
import sys
from pygtail import Pygtail

for line in Pygtail("some.log"):
    sys.stdout.write(line)

24
需要安装包才能获得某种功能,这与“电池内置”(batteries included)的概念恰恰相反。 - bfontaine
2
幸运的是,并非所有包都会默认安装。但是你无需编写(和调试和维护)使用subprocess的任何棘手代码,因为得到更高Karma值的回答已经提供了解决方案。 - Peter M. - stands for Monica
@Eli - 是的,你的回答中提到了pygtail,但没有举例说明它使用起来有多容易。顺便说一句,我已经为你的回答点赞了,所以请不要太难过 :-) - Peter M. - stands for Monica
1
如何在您的Pygtail示例中使用--full-lines选项 - G.ONE
pygtail似乎自2015年以来没有更新,并且仍被报告为Beta版本。 “电池包含”的整个意义在于它是标准库中维护,记录和可靠的所有内容。 - Bob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接