Как посмотреть файл на предмет изменений?


323

У меня есть файл журнала, записываемый другим процессом, который я хочу наблюдать за изменениями. Каждый раз, когда происходит изменение, я хотел бы прочитать новые данные, чтобы выполнить их обработку.

Какой лучший способ сделать это? Я надеялся, что из библиотеки PyWin32 будет какая-то зацепка. Я нашел win32file.FindNextChangeNotificationфункцию, но не знаю, как попросить ее посмотреть конкретный файл.

Если кто-нибудь сделал что-то подобное, я был бы очень рад услышать, как ...

[Править] Я должен был упомянуть, что я был за решение, которое не требует опроса.

[Править] Проклятия! Кажется, это не работает на подключенном сетевом диске. Я предполагаю, что Windows не «слышит» никаких обновлений файла, как это происходит на локальном диске.


1
в Linux для этого можно использовать straceконтрольные writeвызовы
test30

В ответе @ simao используется python-watchdog. Python-Watchdog имеет отличную документацию - здесь есть ссылка на документацию ["QuickStart"], которая предоставляет минимальный пример кода, который просматривает текущий рабочий каталог.
Тревор Бойд Смит

Ответы:


79

Вы уже просмотрели документацию, доступную по адресу http://timgolden.me.uk/python/win32_how_do_i/watch_directory_for_changes.html ? Если вам нужно, чтобы он работал только под Windows, 2-й пример кажется именно тем, что вы хотите (если вы поменяете путь к каталогу с тем файлом, который вы хотите просмотреть).

В противном случае опрос, вероятно, будет единственным действительно независимым от платформы вариантом.

Примечание: я не пробовал ни одно из этих решений.


5
Этот ответ специфичен для Windows, но, похоже, здесь также были опубликованы некоторые кросс-платформенные решения этой проблемы.
Андерсон Грин

Есть ли эталонный тест, если этот процесс медленнее, чем его реализация на родном языке, таком как с ++?
user1767754

Лучше вставить соответствующий контент из цитируемых источников, так как они могут устареть.
Триларион,

2
(1.) В конце этого ответа содержится сильный отказ от ответственности ... «Я не пробовал ни одно из этих решений». (2.) этот ответ является более или менее ответом «только ссылка» (3.) в ответе упоминается «опрос», но он не дает ничего полезного после этого ... где ответ @ Дистана действительно дает некоторую полезную информацию об опросе
Тревор Бойд Смит

283

Вы пытались использовать Watchdog ?

Библиотека Python API и утилиты оболочки для мониторинга событий файловой системы.

Мониторинг каталогов стал проще с

  • Кроссплатформенный API.
  • Инструмент оболочки для запуска команд в ответ на изменения каталога.

Начните быстро с простого примера в Quickstart ...


56
Устанавливается с easy_install? Проверьте. Бесплатная лицензия? Проверьте . Решает проблему на больших платформах? Проверьте . Я поддерживаю этот ответ. Единственное примечание: пример на странице проекта не работает "из коробки". Вместо этого используйте тот на их github .
Инамати

6
Мы используем сторожевой таймер. Мы можем переключиться на QFileSystemWatcher. Просто честное предупреждение - сторожевой таймер хорош, но далеко не идеален на всех платформах (в настоящее время). Каждая ОС имеет свои особенности. Так что, если вы не посвятите себя совершенствованию, вы будете рвать на себе волосы. Если вы просто хотите посмотреть 10 файлов или около того, я бы опросил. Кэширование диска ОС очень развито, и Watchdog все равно использует API-интерфейсы опроса. Это в основном для просмотра огромных структур папок ИМХО.
SilentSteel

3
Моя единственная претензия к сторожевой тайме заключается в том, что у нее много зависимостей. Меньше, чем PyQt, конечно, но он не работает и не похож на минимальный, лучший метод, решение «один-на-один-и-делает-это правильно».
АндреасТ

1
Правильно ли здесь @denfromufa? Действительно ли сторожевой таймер блокирует файлы, чтобы их нельзя было одновременно отредактировать для сторожевого таймера? Я с трудом могу в это поверить, это было бы совершенно бесполезно.
Мишель Мюллер

1
@ MichelMüller Я только что проверил этот пример (см. Ссылку ниже), и он работает! не уверен, что было не так раньше, но этот ответ не дает никакого примера. stackoverflow.com/a/18599427/2230844
denfromufa

93

Если опрос достаточно хорош для вас, я просто посмотрю, не изменится ли статистика файла «измененное время». Чтобы прочитать это:

os.stat(filename).st_mtime

(Также обратите внимание, что решение событий собственных изменений Windows не работает при любых обстоятельствах, например, на сетевых дисках.)

import os

class Monkey(object):
    def __init__(self):
        self._cached_stamp = 0
        self.filename = '/path/to/file'

    def ook(self):
        stamp = os.stat(self.filename).st_mtime
        if stamp != self._cached_stamp:
            self._cached_stamp = stamp
            # File has changed, so do something...

1
Как вы можете сделать это на интервале?
Допатраман

2
@dopatraman Вот как вы можете сделать это на интервале `import sys import time pub = Monkey (), в то время как True: try: time.sleep (1) pub.watch (), за исключением KeyboardInterrupt: print ('\ nDone'), кроме : print (f'Неуправляемая ошибка: {sys.exc_info () [0]} ') `
Влад Безден

Отличное простое решение! Я добавил проверку , чтобы сохранить его от отчетности файла изменен при первом запуске: if self._cached_stamp is not None.
Нумен

50

Если вы хотите мультиплатформенное решение, проверьте QFileSystemWatcher . Вот пример кода (не санированный):

from PyQt4 import QtCore

@QtCore.pyqtSlot(str)
def directory_changed(path):
    print('Directory Changed!!!')

@QtCore.pyqtSlot(str)
def file_changed(path):
    print('File Changed!!!')

fs_watcher = QtCore.QFileSystemWatcher(['/path/to/files_1', '/path/to/files_2', '/path/to/files_3'])

fs_watcher.connect(fs_watcher, QtCore.SIGNAL('directoryChanged(QString)'), directory_changed)
fs_watcher.connect(fs_watcher, QtCore.SIGNAL('fileChanged(QString)'), file_changed)

6
Я думаю, что это, пожалуй, лучший ответ из всех, учитывая, что они либо а) полагаются на объект FileSystemwatcher Win32 и не могут быть портированы, либо б) опрашивают файл (что плохо для производительности и не будет масштабироваться). Жаль, что в Python нет встроенного средства, поскольку PyQt - огромная зависимость, если все, что вы используете, это класс QFileSystemWatcher.
CadentOrange

4
Мне нравится это решение. Я хотел отметить, что для работы вам понадобится экземпляр QApplication, я добавил «app = QtGui.QApplication (sys.argv)» прямо под импортом, а затем «app.exec_ ()» после сигнальных соединений.
spencewah

Просто тестируя это на Linux-боксе, я вижу, что вызывается метод directory_changed, но не file_changed.
Кен Киндер

@CadentOrange, если вы этого не сделаете , как зависимость PyQt, пакет является правильный ответwatchdog
Майк Пеннингтон

почему бы не использовать PySideдля этого, а не PyQtдля такого маленького использования.
Ciasto piekarz,

29

Он не должен работать на Windows (может быть, с Cygwin?), Но для пользователя Unix, вы должны использовать системный вызов "fcntl". Вот пример в Python. Это в основном тот же код, если вам нужно написать его на C (те же имена функций)

import time
import fcntl
import os
import signal

FNAME = "/HOME/TOTO/FILETOWATCH"

def handler(signum, frame):
    print "File %s modified" % (FNAME,)

signal.signal(signal.SIGIO, handler)
fd = os.open(FNAME,  os.O_RDONLY)
fcntl.fcntl(fd, fcntl.F_SETSIG, 0)
fcntl.fcntl(fd, fcntl.F_NOTIFY,
            fcntl.DN_MODIFY | fcntl.DN_CREATE | fcntl.DN_MULTISHOT)

while True:
    time.sleep(10000)

3
Работает как шарм с ядром Linux 2.6.31 в файловой системе ext4 (в Ubuntu 10.04), но только для каталогов - при IOError «не каталог», если я использую его с файлом.
Дэвид Андерхилл

1
ЗДОРОВО! То же самое для меня, работает только для каталога и смотреть файлы в этом каталоге. Но он не будет работать для измененных файлов в подкаталогах, поэтому, похоже, вам нужно пройтись по подкаталогам и просмотреть все из них. (или есть лучший способ сделать это?)
lfagundes

20

Проверьте pyinotify .

inotify заменяет dnotify (из более раннего ответа) в новых linux и позволяет осуществлять мониторинг на уровне файлов, а не на уровне каталогов.


5
Не ставить под сомнение этот ответ, но после прочтения этой статьи я бы сказал, что это может быть не столь гламурное решение, как мысль. serpentine.com/blog/2008/01/04/why-you-should-not-use-pyinotify
NuclearPeon

1
У pyinotify есть много недостатков, начиная с очень непитоничного кода и заканчивая потреблением памяти. Лучше искать другие варианты ..
Тайто

13

Ну, после небольшого взлома сценария Тима Голдена, у меня есть следующее, которое, кажется, работает довольно хорошо:

import os

import win32file
import win32con

path_to_watch = "." # look at the current directory
file_to_watch = "test.txt" # look for changes to a file called test.txt

def ProcessNewData( newData ):
    print "Text added: %s"%newData

# Set up the bits we'll need for output
ACTIONS = {
  1 : "Created",
  2 : "Deleted",
  3 : "Updated",
  4 : "Renamed from something",
  5 : "Renamed to something"
}
FILE_LIST_DIRECTORY = 0x0001
hDir = win32file.CreateFile (
  path_to_watch,
  FILE_LIST_DIRECTORY,
  win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE,
  None,
  win32con.OPEN_EXISTING,
  win32con.FILE_FLAG_BACKUP_SEMANTICS,
  None
)

# Open the file we're interested in
a = open(file_to_watch, "r")

# Throw away any exising log data
a.read()

# Wait for new data and call ProcessNewData for each new chunk that's written
while 1:
  # Wait for a change to occur
  results = win32file.ReadDirectoryChangesW (
    hDir,
    1024,
    False,
    win32con.FILE_NOTIFY_CHANGE_LAST_WRITE,
    None,
    None
  )

  # For each change, check to see if it's updating the file we're interested in
  for action, file in results:
    full_filename = os.path.join (path_to_watch, file)
    #print file, ACTIONS.get (action, "Unknown")
    if file == file_to_watch:
        newText = a.read()
        if newText != "":
            ProcessNewData( newText )

Вероятно, это можно сделать с дополнительной проверкой ошибок, но для простого просмотра файла журнала и некоторой обработки его перед выпуском на экран это работает хорошо.

Спасибо всем за ваш вклад - отличный материал!


10

Для просмотра отдельного файла с опросом и минимальными зависимостями, вот полностью выделенный пример, основанный на ответе Дистана (выше):

import os
import sys 
import time

class Watcher(object):
    running = True
    refresh_delay_secs = 1

    # Constructor
    def __init__(self, watch_file, call_func_on_change=None, *args, **kwargs):
        self._cached_stamp = 0
        self.filename = watch_file
        self.call_func_on_change = call_func_on_change
        self.args = args
        self.kwargs = kwargs

    # Look for changes
    def look(self):
        stamp = os.stat(self.filename).st_mtime
        if stamp != self._cached_stamp:
            self._cached_stamp = stamp
            # File has changed, so do something...
            print('File changed')
            if self.call_func_on_change is not None:
                self.call_func_on_change(*self.args, **self.kwargs)

    # Keep watching in a loop        
    def watch(self):
        while self.running: 
            try: 
                # Look for changes
                time.sleep(self.refresh_delay_secs) 
                self.look() 
            except KeyboardInterrupt: 
                print('\nDone') 
                break 
            except FileNotFoundError:
                # Action on file not found
                pass
            except: 
                print('Unhandled error: %s' % sys.exc_info()[0])

# Call this function each time a change happens
def custom_action(text):
    print(text)

watch_file = 'my_file.txt'

# watcher = Watcher(watch_file)  # simple
watcher = Watcher(watch_file, custom_action, text='yes, changed')  # also call custom action function
watcher.watch()  # start the watch going

2
Вы могли бы сделать watch_fileи _cached_stampв списки, и итерация по ним в цикле. Хотя не очень хорошо масштабируется для большого количества файлов
4Oh4

Разве это не запускает действие при каждом запуске? _cached_stamp устанавливается в 0 и затем сравнивается с os.stat (self.filename) .st_mtime. _cached_stamp должен быть установлен в os.stat (self.filename) .st_mtime в конструкторе, нет?
Seanonymous

1
call_func_on_change() будет запущен при первом запуске look() , но затем _cached_stampобновляется, поэтому не будет запускаться снова, пока значение не os.stat(self.filename).st_mtime. _cached_stampизменится.
4O4

1
Вы можете установить значение _cached_stampв конструкторе, если вы не хотитеcall_func_on_change() чтобы его вызывали при первом запуске
4Oh4

Я использовал ваш скрипт для вызова какой-либо функции при изменении файла. Моя функция не принимает никаких аргументов в отличие от вашей. Я думал, что для того, чтобы это работало, мне нужно удалить * args, ** kwargs. Выглядело это так (я ставлю только строки с изменениями):self.call_func_on_change(self) def custom_action(): watcher = Watcher(watch_file, custom_action()) Но это не сработало Действие был вызван только во время первой итерации: Файл изменился да, изменен файл изменен файл изменен файл изменен Он начал работать , когда я держал * арг и называем это: watcher = Watcher(watch_file, custom_action)Я борьба задаться вопросом , почему?
zwornik

7

Проверьте мой ответ на аналогичный вопрос . Вы можете попробовать тот же цикл в Python. Эта страница предлагает:

import time

while 1:
    where = file.tell()
    line = file.readline()
    if not line:
        time.sleep(1)
        file.seek(where)
    else:
        print line, # already has newline

Также смотрите вопрос tail () в файле с Python .


Вы можете sys.stdout.write (строка). Ваш код не работает, если файл урезан. Python имеет встроенную функцию file ().
JFS

Я разместил измененную версию вашего кода. Вы можете включить его в свой ответ, если он работает на вас.
JFS

7

Самое простое решение для меня - использовать инструмент watchdo от watchdog

Из https://pypi.python.org/pypi/watchdog у меня теперь есть процесс, который ищет файлы sql в каталоге и выполняет их при необходимости.

watchmedo shell-command \
--patterns="*.sql" \
--recursive \
--command='~/Desktop/load_files_into_mysql_database.sh' \
.

6

Ну, так как вы используете Python, вы можете просто открыть файл и продолжить чтение строк из него.

f = open('file.log')

Если прочитанная строка не пуста , вы обрабатываете ее.

line = f.readline()
if line:
    // Do what you want with the line

Возможно, вам не хватает того, чтобы продолжать звонить readline в EOF. В этом случае он просто будет возвращать пустую строку. И когда что-то добавляется в файл журнала, чтение будет продолжаться с того места, где оно остановилось, как вам нужно.

Если вы ищете решение, которое использует события или определенную библиотеку, укажите это в своем вопросе. В противном случае, я думаю, что это решение просто отлично.


6

Вот упрощенная версия кода Кендера, который, похоже, выполняет ту же самую задачу и не импортирует весь файл:

# Check file for new data.

import time

f = open(r'c:\temp\test.txt', 'r')

while True:

    line = f.readline()
    if not line:
        time.sleep(1)
        print 'Nothing New'
    else:
        print 'Call Function: ', line

6

Это еще одна модификация скрипта Тима Голдана, которая работает на Unix-типах и добавляет простой наблюдатель для модификации файла с использованием dict (file => time).

использование: whatName.py path_to_dir_to_watch

#!/usr/bin/env python

import os, sys, time

def files_to_timestamp(path):
    files = [os.path.join(path, f) for f in os.listdir(path)]
    return dict ([(f, os.path.getmtime(f)) for f in files])

if __name__ == "__main__":

    path_to_watch = sys.argv[1]
    print('Watching {}..'.format(path_to_watch))

    before = files_to_timestamp(path_to_watch)

    while 1:
        time.sleep (2)
        after = files_to_timestamp(path_to_watch)

        added = [f for f in after.keys() if not f in before.keys()]
        removed = [f for f in before.keys() if not f in after.keys()]
        modified = []

        for f in before.keys():
            if not f in removed:
                if os.path.getmtime(f) != before.get(f):
                    modified.append(f)

        if added: print('Added: {}'.format(', '.join(added)))
        if removed: print('Removed: {}'.format(', '.join(removed)))
        if modified: print('Modified: {}'.format(', '.join(modified)))

        before = after

Обновлено для поддержки Python3
Ronedg

4

Как вы можете видеть в статье Тима Голдена , на которую указывает Хорст Гутманн , WIN32 относительно сложен и следит за каталогами, а не за одним файлом.

Я хотел бы предложить вам взглянуть на IronPython , который является реализацией Python .NET . С IronPython вы можете использовать все функциональные возможности .NET, включая

System.IO.FileSystemWatcher

Который обрабатывает отдельные файлы с простым интерфейсом событий .


@ Ciasto, потому что тогда у вас должен быть доступен Iron Python, а не простая установка Python.
Джон Кейдж

1

Это пример проверки файла на наличие изменений. Тот, который не может быть лучшим способом сделать это, но это, безусловно, короткий путь.

Удобный инструмент для перезапуска приложения, когда были внесены изменения в исходный код. Я сделал это во время игры с pygame, чтобы видеть эффекты, происходящие сразу после сохранения файла.

При использовании в pygame убедитесь, что содержимое цикла while размещено в вашем игровом цикле, также как и в обновлении. В противном случае ваше приложение застрянет в бесконечном цикле, и вы не увидите обновления вашей игры.

file_size_stored = os.stat('neuron.py').st_size

  while True:
    try:
      file_size_current = os.stat('neuron.py').st_size
      if file_size_stored != file_size_current:
        restart_program()
    except: 
      pass

В случае, если вы хотели перезапустить код, который я нашел в Интернете. Вот. (Не относится к вопросу, хотя это может пригодиться)

def restart_program(): #restart application
    python = sys.executable
    os.execl(python, python, * sys.argv)

Получайте удовольствие, заставляя электроны делать то, что вы от них хотите.


Похоже, использование .st_mtimeвместо .st_sizeбыло бы более надежным и столь же коротким способом сделать это, хотя ОП указал, что он не хотел делать это путем опроса.
Мартино

1
ACTIONS = {
  1 : "Created",
  2 : "Deleted",
  3 : "Updated",
  4 : "Renamed from something",
  5 : "Renamed to something"
}
FILE_LIST_DIRECTORY = 0x0001

class myThread (threading.Thread):
    def __init__(self, threadID, fileName, directory, origin):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.fileName = fileName
        self.daemon = True
        self.dir = directory
        self.originalFile = origin
    def run(self):
        startMonitor(self.fileName, self.dir, self.originalFile)

def startMonitor(fileMonitoring,dirPath,originalFile):
    hDir = win32file.CreateFile (
        dirPath,
        FILE_LIST_DIRECTORY,
        win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE,
        None,
        win32con.OPEN_EXISTING,
        win32con.FILE_FLAG_BACKUP_SEMANTICS,
        None
    )
    # Wait for new data and call ProcessNewData for each new chunk that's
    # written
    while 1:
        # Wait for a change to occur
        results = win32file.ReadDirectoryChangesW (
            hDir,
            1024,
            False,
            win32con.FILE_NOTIFY_CHANGE_LAST_WRITE,
            None,
            None
        )
        # For each change, check to see if it's updating the file we're
        # interested in
        for action, file_M in results:
            full_filename = os.path.join (dirPath, file_M)
            #print file, ACTIONS.get (action, "Unknown")
            if len(full_filename) == len(fileMonitoring) and action == 3:
                #copy to main file
                ...

1

Вот пример, предназначенный для просмотра входных файлов, которые записывают не более одной строки в секунду, но обычно намного меньше. Цель состоит в добавлении последней строки (самой последней записи) в указанный выходной файл. Я скопировал это из одного из моих проектов и просто удалил все ненужные строки. Вам придется заполнить или изменить недостающие символы.

from PyQt5.QtCore import QFileSystemWatcher, QSettings, QThread
from ui_main_window import Ui_MainWindow   # Qt Creator gen'd 

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        Ui_MainWindow.__init__(self)
        self._fileWatcher = QFileSystemWatcher()
        self._fileWatcher.fileChanged.connect(self.fileChanged)

    def fileChanged(self, filepath):
        QThread.msleep(300)    # Reqd on some machines, give chance for write to complete
        # ^^ About to test this, may need more sophisticated solution
        with open(filepath) as file:
            lastLine = list(file)[-1]
        destPath = self._filemap[filepath]['dest file']
        with open(destPath, 'a') as out_file:               # a= append
            out_file.writelines([lastLine])

Конечно, охватывающий класс QMainWindow не является строго обязательным, т.е. Вы можете использовать QFileSystemWatcher в одиночку.



0

Кажется, никто не опубликовал fswatch . Это кроссплатформенный наблюдатель файловой системы. Просто установите его, запустите и следуйте инструкциям.

Я использовал его с программами Python и Golang, и он просто работает.


0

related @ 4Oh4 solution - плавное изменение списка файлов для просмотра;

import os
import sys
import time

class Watcher(object):
    running = True
    refresh_delay_secs = 1

    # Constructor
    def __init__(self, watch_files, call_func_on_change=None, *args, **kwargs):
        self._cached_stamp = 0
        self._cached_stamp_files = {}
        self.filenames = watch_files
        self.call_func_on_change = call_func_on_change
        self.args = args
        self.kwargs = kwargs

    # Look for changes
    def look(self):
        for file in self.filenames:
            stamp = os.stat(file).st_mtime
            if not file in self._cached_stamp_files:
                self._cached_stamp_files[file] = 0
            if stamp != self._cached_stamp_files[file]:
                self._cached_stamp_files[file] = stamp
                # File has changed, so do something...
                file_to_read = open(file, 'r')
                value = file_to_read.read()
                print("value from file", value)
                file_to_read.seek(0)
                if self.call_func_on_change is not None:
                    self.call_func_on_change(*self.args, **self.kwargs)

    # Keep watching in a loop
    def watch(self):
        while self.running:
            try:
                # Look for changes
                time.sleep(self.refresh_delay_secs)
                self.look()
            except KeyboardInterrupt:
                print('\nDone')
                break
            except FileNotFoundError:
                # Action on file not found
                pass
            except Exception as e:
                print(e)
                print('Unhandled error: %s' % sys.exc_info()[0])

# Call this function each time a change happens
def custom_action(text):
    print(text)
    # pass

watch_files = ['/Users/mexekanez/my_file.txt', '/Users/mexekanez/my_file1.txt']

# watcher = Watcher(watch_file)  # simple



if __name__ == "__main__":
    watcher = Watcher(watch_files, custom_action, text='yes, changed')  # also call custom action function
    watcher.watch()  # start the watch going


-2

Я не знаю какой-либо конкретной функции Windows. Вы можете попробовать получить хэш MD5 файла каждую секунду / минуту / час (зависит от того, насколько быстро он вам нужен) и сравнить его с последним хешем. Когда он отличается, вы знаете, что файл был изменен, и вы читаете самые новые строки.


-6

Я бы попробовал что-то подобное.

    try:
            f = open(filePath)
    except IOError:
            print "No such file: %s" % filePath
            raw_input("Press Enter to close window")
    try:
            lines = f.readlines()
            while True:
                    line = f.readline()
                    try:
                            if not line:
                                    time.sleep(1)
                            else:
                                    functionThatAnalisesTheLine(line)
                    except Exception, e:
                            # handle the exception somehow (for example, log the trace) and raise the same exception again
                            raw_input("Press Enter to close window")
                            raise e
    finally:
            f.close()

Цикл проверяет, есть ли новая строка (строки) с момента последнего чтения файла - если он есть, он читается и передается functionThatAnalisesTheLineфункции. Если нет, скрипт ждет 1 секунду и повторяет процесс.


4
-1: Открытие файла и чтение строк не очень хорошая идея, когда файлы могут иметь размер 100 МБ. Вы должны запустить его для каждого файла тоже, что было бы плохо, если вы хотите посмотреть тысячи файлов.
Джон Кейдж

1
В самом деле? Открытие файла для изменений?
Farsheed
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.