从经常更新的文件中读取数据

73

我正在Linux系统上用Python编写一个程序。目标是读取一个日志文件,并在找到特定字符串时执行一个bash命令。另一个程序正在不断地向该日志文件中写入内容。

我的问题是:如果我使用open()方法打开文件,那么我的Python文件对象会随着实际文件的写入而更新,还是我需要定时重新打开文件?

更新:感谢迄今为止的回答。我可能应该提到这个文件是由Java EE应用程序写入的,所以我无法控制何时写入数据。我当前有一个程序,每10秒钟重新打开文件并尝试从上次读取的字节位置读取文件。目前它只是打印返回的字符串。我希望文件不需要重新打开,但读取命令可以访问Java应用程序写入文件的数据。

#!/usr/bin/python
import time

fileBytePos = 0
while True:
    inFile = open('./server.log','r')
    inFile.seek(fileBytePos)
    data = inFile.read()
    print data
    fileBytePos = inFile.tell()
    print fileBytePos
    inFile.close()
    time.sleep(10)

感谢在pyinotify和生成器方面给出的建议。我会查看这些内容,寻找更好的解决方案。

9个回答

126

我建议看一下David Beazley的Python生成器技巧,特别是第5部分:处理无限数据。它将实时处理类似于tail -f logfile命令的Python等效操作。

# follow.py
#
# Follow a file like tail -f.

import time
def follow(thefile):
    thefile.seek(0,2)
    while True:
        line = thefile.readline()
        if not line:
            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,

2
如果答案包含 OP 代码的代码示例,我会点赞。 - chtenb
2
这个答案在我看来是错误的,如果写作者将一行拆成两个部分,readline 将会返回两次。但你实际上只想要返回一行。 - Fabian
旋转日志最终导致文件被重命名,然后此代码将无限等待旧的日志文件。我曾经遇到过这个问题,并用以下方法解决了它,也解决了 time.sleep(0.1) 的问题:https://dev59.com/zaLia4cB1Zd3GeqPiGHS#44411621。 - Daniel F
1
thefile.seek(0,2) 做了什么? - Rylan Schaeffer
1
@RylanSchaeffer 0 是偏移量,2 表示相对于文件末尾进行查找。 - Jeff Bauer
显示剩余6条评论

27

“一个交互式会话胜过千言万语。”

>>> f1 = open("bla.txt", "wt")
>>> f2 = open("bla.txt", "rt")
>>> f1.write("bleh")
>>> f2.read()
''
>>> f1.flush()
>>> f2.read()
'bleh'
>>> f1.write("blargh")
>>> f1.flush()
>>> f2.read()
'blargh'

换句话说 - 是的,只需一个 "open"。


14

以下是稍作修改的Jeff Bauer答案,可以抵御文件截断。 如果您的文件正在被logrotate处理,则非常有用。

import os
import time

def follow(name):
    current = open(name, "r")
    curino = os.fstat(current.fileno()).st_ino
    while True:
        while True:
            line = current.readline()
            if not line:
                break
            yield line

        try:
            if os.stat(name).st_ino != curino:
                new = open(name, "r")
                current.close()
                current = new
                curino = os.fstat(current.fileno()).st_ino
                continue
        except IOError:
            pass
        time.sleep(1)


if __name__ == '__main__':
    fname = "test.log"
    for l in follow(fname):
        print "LINE: {}".format(l)

12
看到“while True: while True:”很可怕。 - Pedro Lobito

3

如果你的目标系统是Linux,你可以使用pyinotify来在文件更改时通知你。

还有这个技巧,可能对你很有用。它使用file.seek来实现tail -f的功能。


链接容易失效,而且没有代码示例。这个答案提供的内容不多,有用的部分也不稳定。 - Zim

1
如果您的代码在 while 循环中读取文件:
f = open('/tmp/workfile', 'r')
while(1):
    line = f.readline()
    if line.find("ONE") != -1:
        print "Got it"

你正在从另一个程序以追加模式写入同一文件。一旦文件中添加了“ONE”,就会得到打印输出。你可以采取任何想要的操作。简而言之,你不必定期重新打开文件。

>>> f = open('/tmp/workfile', 'a')
>>> f.write("One\n")
>>> f.close()
>>> f = open('/tmp/workfile', 'a')
>>> f.write("ONE\n")
>>> f.close()

1
这个回答也是错误的,写入内容可能会被拆分成“ON”和“E\n”,导致两行都不匹配。 - Fabian

1

我不是专家,但我认为你需要使用某种观察者模式来被动地监视文件,然后在发生更改时触发重新打开文件的事件。至于如何实际实现这一点,我不知道。

我不认为open()会像你所建议的那样实时打开文件。


0

我有一个类似的用例,并为此编写了以下片段。 虽然有人可能会认为这不是最理想的方法,但它可以完成工作,并且看起来很容易理解。

def reading_log_files(filename):
    with open(filename, "r") as f:
        data = f.read().splitlines()
    return data


def log_generator(filename, period=1):
    data = reading_log_files(filename)
    while True:
        time.sleep(period)
        new_data = reading_log_files(filename)
        yield new_data[len(data):]
        data = new_data


if __name__ == '__main__':
    x = log_generator(</path/to/log/file.log>)
    for lines in x:
        print(lines)
        # lines will be a list of new lines added at the end

希望你觉得这个有用。

这对我的使用情况很有帮助。谢谢 :) - ishallwin
这是一个“Shlemiel the Painter”算法。(阅读https://www.joelonsoftware.com/2001/12/11/back-to-basics/) - Jason S

0

这取决于您想要对文件做什么。有两种潜在的用例:

  1. 从连续更新的文件(如日志文件)中读取附加内容。
  2. 从不断被覆盖的文件(例如*nix系统中的网络统计文件)中读取内容

由于其他人已经详细回答了如何处理方案#1,我想帮助那些需要方案#2的人。基本上,在第n + 1次调用read()之前,您需要使用seek(0)(或任何您想要从中读取的位置)将文件指针重置为0。

您的代码可能类似于以下函数。

def generate_network_statistics(iface='wlan0'):
    with open('/sys/class/net/' + iface + '/statistics/' + 'rx' + '_bytes', 'r') as rx:
        with open('/sys/class/net/' + iface + '/statistics/' + 'tx' + '_bytes', 'r') as tx:
            with open('/proc/uptime', 'r') as uptime:
                while True:
                    receive = int(rx.read())
                    rx.seek(0)
                    transmit = int(tx.read())
                    tx.seek(0)
                    uptime_seconds = int(uptime.read())
                    uptime.seek(0)
                    print("Receive: %i, Transmit: %i" % (receive, transmit))
                    time.sleep(1)

0

即使在文件末尾返回空字符串,也要保持文件句柄打开,并在一些休眠时间后再次尝试读取它。

    import time

    syslog = '/var/log/syslog'
    sleep_time_in_seconds = 1

    try:
        with open(syslog, 'r', errors='ignore') as f:
            while True:
                for line in f:
                    if line:
                        print(line.strip())
                        # do whatever you want to do on the line
                time.sleep(sleep_time_in_seconds)
    except IOError as e:
        print('Cannot open the file {}. Error: {}'.format(syslog, e))

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