在python中拖尾多个日志文件

时间:2011-04-20 02:59:29

标签: python

对我来说这可能是一个愚蠢的练习,但它提出了一些有趣的问题。我有一个来自聊天客户端的日志文件目录,我希望每当其中一个更改时使用notify-osd通知我。

我编写的脚本基本上使用os.popen在每个文件上运行linux tail命令以获取最后一行,然后根据字典查看每行上次运行的行。如果该行更改,则使用pynotify向我发送通知。

这个脚本实际上运行得很好,除了它使用了大量的cpu(可能因为它在每次循环运行时运行尾部大约16次,对于通过sshfs挂载的文件。)

this这样的东西似乎是一个很好的解决方案,但我不知道如何为多个文件实现它。

Here是我写的脚本。请原谅我缺乏评论和不良风格。

编辑:为了澄清,这是桌面上的所有Linux。

3 个答案:

答案 0 :(得分:9)

甚至没有查看您的源代码,有两种方法可以更轻松地执行此操作并处理多个文件。

  1. 除非必须,否则不要打扰跑尾。只需os.stat所有文件并记录上次修改时间。如果上次修改时间不同,则发出通知。

  2. 使用pyinotify呼叫Linux的inotify设施;这将让内核为您执行选项1,并在目录中的任何文件发生更改时回调您。然后将回调转换为您的osd通知。

  3. 现在,可能会有一些棘手的问题,具体取决于当有多条消息时您想要多少通知,以及您是否关心错过消息通知。

    保留使用tail的方法是改为使用tail -f。使用tail -f打开所有文件,然后使用select模块让操作系统在tail -f打开的其中一个文件描述符上有其他输入时告诉您。您的主循环将调用select,然后迭代每个可读描述符以生成通知。 (你可以在不使用tail的情况下执行此操作,并在可读时调用readline()。)

    脚本的其他改进方面:

    • 使用os.listdir和本机Python过滤(例如,使用列表推导)而不是popen和一堆grep过滤器。
    • 更新要定期扫描的缓冲区列表,而不是仅在程序启动时执行此操作。
    • 使用subprocess.popen代替os.popen

答案 1 :(得分:4)

如果你已经在使用pyinotify模块,那么在纯Python中很容易做到这一点(即不需要产生一个单独的进程来拖尾每个文件)。

这是一个由inotify事件驱动的示例,应该使用非常少的cpu。当给定路径发生IN_MODIFY时,我们从文件句柄中读取所有可用数据,并输出找到的所有完整行,缓冲不完整的行,直到有更多数据可用:

import os
import select
import sys
import pynotify
import pyinotify

class Watcher(pyinotify.ProcessEvent):

    def __init__(self, paths):
        self._manager = pyinotify.WatchManager()
        self._notify = pyinotify.Notifier(self._manager, self)
        self._paths = {}
        for path in paths:
            self._manager.add_watch(path, pyinotify.IN_MODIFY)
            fh = open(path, 'rb')
            fh.seek(0, os.SEEK_END)
            self._paths[os.path.realpath(path)] = [fh, '']

    def run(self):
        while True:
            self._notify.process_events()
            if self._notify.check_events():
                self._notify.read_events()

    def process_default(self, evt):
        path = evt.pathname
        fh, buf = self._paths[path]
        data = fh.read()
        lines = data.split('\n')
        # output previous incomplete line.
        if buf:
            lines[0] = buf + lines[0]
        # only output the last line if it was complete.
        if lines[-1]:
            buf = lines[-1]
        lines.pop()

        # display a notification
        notice = pynotify.Notification('%s changed' % path, '\n'.join(lines))
        notice.show()

        # and output to stdout
        for line in lines:
            sys.stdout.write(path + ': ' + line + '\n')
        sys.stdout.flush()
        self._paths[path][1] = buf

pynotify.init('watcher')
paths = sys.argv[1:]
Watcher(paths).run()

用法:

% python watcher.py [path1 path2 ... pathN]

答案 2 :(得分:0)

简单的纯python解决方案(不是最好的,但不会分叉,在空闲时间后吐出4个空行,并且每次都会标记块的来源,如果更改):

#!/usr/bin/env python

from __future__ import with_statement

'''
Implement multi-file tail
'''

import os
import sys
import time


def print_file_from(filename, pos):
    with open(filename, 'rb') as fh:
        fh.seek(pos)
        while True:
            chunk = fh.read(8192)
            if not chunk:
                break
            sys.stdout.write(chunk)


def _fstat(filename):
    st_results = os.stat(filename)
    return (st_results[6], st_results[8])


def _print_if_needed(filename, last_stats, no_fn, last_fn):
    changed = False
    #Find the size of the file and move to  the end
    tup = _fstat(filename)
    # print tup
    if last_stats[filename] != tup:
        changed = True
        if not no_fn and last_fn != filename:
            print '\n<%s>' % filename
        print_file_from(filename, last_stats[filename][0])
        last_stats[filename] = tup
    return changed


def multi_tail(filenames, stdout=sys.stdout, interval=1, idle=10, no_fn=False):
    S = lambda (st_size, st_mtime): (max(0, st_size - 124), st_mtime)
    last_stats = dict((fn, S(_fstat(fn))) for fn in filenames)
    last_fn = None
    last_print = 0
    while 1:
        # print last_stats
        changed = False
        for filename in filenames:
            if _print_if_needed(filename, last_stats, no_fn, last_fn):
                changed = True
                last_fn = filename
        if changed:
            if idle > 0:
                last_print = time.time()
        else:
            if idle > 0 and last_print is not None:
                if time.time() - last_print >= idle:
                    last_print = None
                    print '\n' * 4
            time.sleep(interval)

if '__main__' == __name__:
    from optparse import OptionParser
    op = OptionParser()
    op.add_option('-F', '--no-fn', help="don't print filename when changes",
        default=False, action='store_true')
    op.add_option('-i', '--idle', help='idle time, in seconds (0 turns off)',
        type='int', default=10)
    op.add_option('--interval', help='check interval, in seconds', type='int',
        default=1)
    opts, args = op.parse_args()
    try:
        multi_tail(args, interval=opts.interval, idle=opts.idle,
            no_fn=opts.no_fn)
    except KeyboardInterrupt:
        pass