Ordnerstrukturen auf Aktualisierung prüfen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Es macht einen Unterschied ob man `and` oder `&` schreibt. Und natürlich schreibst Du Code immer wieder neu und um. Auch mehr als drei mal.
Karlirex
User
Beiträge: 126
Registriert: Dienstag 18. August 2020, 18:27

Ich habe mich mit dem Problem der Aktualisierung nun einige Zeit beschäftigt, versucht dictionarys, sets, list und dataframes mit einander zu vergleichen, um jeweils den Unterschied im Zeitstempel zu erkennen. Leider ohne Erfolg.

Ich bekomme an der Stelle die ganze Zeit das eine print-Ausgabe, welche erstmal nicht da stehen sollte, bis eben eine Datei mit selben Namen aber unterschiedlicher Zeit in den Ordner gefügt wird.
Ich glaube ich übersehe ein Stück an dieser Stelle.

Das Stück Code für die Abfrage nach der letzten accesstime sieht so aus:

Code: Alles auswählen

    for filename in previous_text_files & text_files:
        with filename.stat().st_atime as filename:
            if filename not in file_time:
                print("neu")
        dict1 = {filename: filename.stat().st_atime}
        dict2 = dict(set(dict1.items()) - set(file_time.items()))
Mein gesamtes Skript sieht nun so aus:

Code: Alles auswählen

import os
from pathlib import Path
from time import sleep
import time

FOLDER = Path(r'......')

print("Erster Scan")
previous_text_files = set(FOLDER.rglob("*.txt"))
i = 0

def get_file_time(FOLDER):
    return {
        filename: filename.stat().st_atime
        for filename in FOLDER.rglob("*.txt")
    }

file_time = get_file_time(FOLDER)


while True:
    file_time = get_file_time(FOLDER)
    text_files = set(FOLDER.rglob("*.txt"))
    for filename in text_files - previous_text_files:
        print(f"{filename} ist neu")
    for filename in previous_text_files - text_files:
        print(f"{filename} gelöscht")      
    for filename in previous_text_files & text_files:
        with filename.stat().st_atime as filename:
            if filename not in file_time:
                print("neu")
        dict1 = {filename: filename.stat().st_atime}
        dict2 = dict(set(dict1.items()) - set(file_time.items()))
    if not text_files:
        print("Keine Daten enthalten")
    previous_text_files = text_files
    i = i + 1
    print("Lauf", i)
    sleep(5)

Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

`os` und `time` werden importiert aber nicht benutzt.
FOLDER in `get_file_time` ist keine Konstante, muß also `folder` geschrieben werden.
Du suchst alle Dateien immer zwei mal, einmal mit Änderungszeit und einmal ohne. Dabei ist zweiteres völlig unnötig.
Bei dem with-Statement bekommst Du einen Fehler, wäre gut gewesen, wenn Du den hier auch posten würdest. Was hoffst Du, bewirkt with hier?
Würde das with funktionieren, wäre in `filename` die Änderungszeit gespeichert und die wäre niemals ein Schlüssel in `file_time`. Für was soll `dict1` und `dict2` gut sein?
Und wieder hast Du eine while-Schleife, die eine for-Schleife sein könnte.

Code: Alles auswählen

from pathlib import Path
from time import sleep

FOLDER = Path(r'......')

def get_filenames_with_accesstime(folder):
    return {
        filename: filename.stat().st_atime
        for filename in FOLDER.rglob("*.txt")
    }


print("Erster Scan")
previous_text_files = get_filenames_with_accesstime(FOLDER)
for i in count(1):
    print("Lauf", i)
    text_files = get_filenames_with_accesstime(FOLDER)
    for filename in set(text_files) - set(previous_text_files):
        print(f"{filename} ist neu")
    for filename in set(previous_text_files) - set(text_files):
        print(f"{filename} gelöscht")
    for filename in set(previous_text_files) & set(text_files):
        if previous_text_files[filename] != text_files[filename]:
            print(f"{filename} geändert")
    if not text_files:
        print("Keine Daten enthalten")
    previous_text_files = text_files
    sleep(5)
Karlirex
User
Beiträge: 126
Registriert: Dienstag 18. August 2020, 18:27

Also das with war die Idee, damit die Zeitstempel abzufragen, da ich so schon Programme gesehen habe, die Ordner nur dann öffnen, wenn dort bpsw auch ein with Argument erfüllt ist.
Das dict war die Überlegung die Zeiten so speichern und vergleichen zukönnen.

Wenn ich deinen Code nun so übernehme, laufe ich zwar in keinerlei Fehler, dennoch läuft das Programm an der Schleife mit der Aktualisierung scheinbar vorbei. Denn ich erhalte keine Meldung, wenn ich die neue Datei mit dem geänderten Zugriff in den Ordner überschreibe.

Kannst Du mir eventuell deine if-Abfrage genauer erklären, also warum nun da das [filename] enthalten ist?
Vielleicht finde ich dann auch dort den letzten Fehler, damit das Programm passend läuft.

Vielen Dank, beste Grüße
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Was Wörterbücher sind, und wie man sie verwendet, gehört zu den absoluten Grundlagen.
Wann atime, mtime oder ctime geändert werden, hängt vom Filesystem ab. Wie das unter Windows gehandhabt wird, kann ich gerade nicht sagen. Aber wenn Du Dateien änderst, interessiert dich eher mtime.
Karlirex
User
Beiträge: 126
Registriert: Dienstag 18. August 2020, 18:27

Hallo,
ich erwecke den Thread nochmal zum Leben :D
Meine Ordner sind mit der Zeit deutlich größer geworden.
Dabei habe ich längere Laufzeiten der Funktion festegestellt (aktuell 6min+).

Code: Alles auswählen

def get_filenames_with_accesstime(folder):
    return {
        filename: filename.stat().st_atime
        for filename in FOLDER.rglob("*")
    }
Um es zu beschleunigen habe ich dann multiprocessing versucht:

Code: Alles auswählen

def my_func(FOLDER, queue):
    queue.put({
        filename: filename.stat().st_mtime
        for filename in FOLDER.rglob("*")
    })
queue = multiprocessing.Queue()
previous_text_files = dict()
start = timeit.default_timer()
for i in path_list:
    p1 = multiprocessing.Process(target=my_func, args=(Path(i), queue))
    p1.start()
    previous_text_files.update(queue.get())
    p1.join()
das ist aber auch nur 0,4s schneller.
Gibts es eine andere Möglichkeit die Funktion zu erweitern, habe ich etwas falsch gemacht oder ist aufgrund der Datenmenge das hier einfach limitiert?

Danke für eure Hilfe und Grüße
Karlirex
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Karlirex: Da ist *gar kein* Gewinn zu erwarten, weil da immer nur *ein* zusätzlicher Prozess läuft, während der Hauptprozess auf den *wartet* und nichts tut. Ich würde da erst einmal schauen ob ein `Pool` und `imap_unordered()`, `map()`, oder die `*_async()`-Methoden verwendet werden kann, bevor ich mir da selbst was zur Übertragung von Daten bastele und die Funktion ändern muss, die das ganze ausführt. Selbst dann wären da noch die `*_async()`-Methoden von `Pool`.

Ich wette es wurde schon mal was zu Namen gesagt. `my_*` ist ein sinnloser Präfix, der in 99,9999% nichts aussagt und da nicht hingehört. `FOLDER` wäre eine Konstante, Argumentnamen sind aber nie Konstanten. `i` ist als Name für etwas anderes als ganze Zahlen falsch. Niemand kommt darauf, das es sich in diesem Fall um einen Ordnerpfadnamen handelt. `filename` ist falsch, weil an den Namen nicht nur Dateinamen (eigentlich ja *Pfade*), sondern auch Ordnerpfade gebunden werden. Daraus folgt, dass auch `get_filenames_with_accesstime()` als Name falsch ist.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Karlirex
User
Beiträge: 126
Registriert: Dienstag 18. August 2020, 18:27

__blackjack__ hat geschrieben: Montag 12. Dezember 2022, 11:12 Ich wette es wurde schon mal was zu Namen gesagt. `my_*` ist ein sinnloser Präfix, der in 99,9999% nichts aussagt und da nicht hingehört. `FOLDER` wäre eine Konstante, Argumentnamen sind aber nie Konstanten. `i` ist als Name für etwas anderes als ganze Zahlen falsch. Niemand kommt darauf, das es sich in diesem Fall um einen Ordnerpfadnamen handelt. `filename` ist falsch, weil an den Namen nicht nur Dateinamen (eigentlich ja *Pfade*), sondern auch Ordnerpfade gebunden werden. Daraus folgt, dass auch `get_filenames_with_accesstime()` als Name falsch ist.
Zu my gabs schon Kommentare ja, zum reinen Testen bei Problemen benenne ich es aber "schnell/einfach" oder übernehme von Stackoverflow direkt um zu schauen ob sich was am Problem ändert. Tut sich das, werden die Namen angepasst.
Zur der Sache mit FOLDER, filename, get_filenames_with_accesstime() finde ich ganz interessant, da ich diese Bezeichnung von Sirius übernommen habe und ihr beide ja sehr darauf achtet, dass immer alles korrekt benannt ist, dies auch hier dann doch "falsch" ist, siehe zwei drei Postings vorher von ihm.

Grüße
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Karlirex: Zumindest `FOLDER` war korrekt, weil das dort noch eine Konstante war, als das noch so hiess. Ich mache in der Regel einen Unterschied in der Benennung das `Path`-Objekte auch wirklich `path` im Namen haben um bei `file_path` vs. `filename` zu wissen, dass das eine ein Pfad-Objekt (zu einer Datei) ist, und das andere eine Zeichenkette mit einem Dateinamen. Insbesondere in Programmen/Programmteilen wo beide Varianten vorkommen können, vermeidet das Verwirrung.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Karlirex
User
Beiträge: 126
Registriert: Dienstag 18. August 2020, 18:27

__blackjack__ hat geschrieben: Montag 12. Dezember 2022, 13:01 @Karlirex: Zumindest `FOLDER` war korrekt, weil das dort noch eine Konstante war, als das noch so hiess. Ich mache in der Regel einen Unterschied in der Benennung das `Path`-Objekte auch wirklich `path` im Namen haben um bei `file_path` vs. `filename` zu wissen, dass das eine ein Pfad-Objekt (zu einer Datei) ist, und das andere eine Zeichenkette mit einem Dateinamen. Insbesondere in Programmen/Programmteilen wo beide Varianten vorkommen können, vermeidet das Verwirrung.
Alles klar.

Ich habe mal nach dem Tutorial von Pool gearbeitet und folgendes probiert:

Code: Alles auswählen

    
    from multiprocessing import Pool
    process = Pool(5)
    with process:
        start = timeit.default_timer()
        results = process.map(get_filenames_with_accesstime, path_list)
        end = timeit.default_timer()
dabei wird eine Zeitverbesserung von 8s erzielt. mit einem apply_async / map_async /mehr Workern erhalte ich keinen weiteren Zeitschub. Schade.
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ich würde hier wahrscheinlich noch die `chunksize` manuell auf 1 setzen sofern in `path_list` nicht viele Werte vorkommen die nur ganz wenige Ergebnisse liefern.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
bords0
User
Beiträge: 234
Registriert: Mittwoch 4. Juli 2007, 20:40

Ich hätte noch den Vorschlag, mit os.scandir() zu arbeiten. Nach meiner Erfahrung bzw. Erinnerung ist das schneller, wenn die Filesystem-Zugriffe der Flaschenhals sind. Die DirEntry-Objekte, die man von os.scandir() bekommt, ersparen den zusätzlichen stat()-Aufruf, weil sie die Werte gleich einlesen und cachen. Man muss also nur einmal ans Filesystem ran, und auch nicht für jede Datei einzeln.

Soweit mein Verständnis, bin kein Dateisystem-Experte.
Karlirex
User
Beiträge: 126
Registriert: Dienstag 18. August 2020, 18:27

__blackjack__ hat geschrieben: Montag 12. Dezember 2022, 14:11 Ich würde hier wahrscheinlich noch die `chunksize` manuell auf 1 setzen sofern in `path_list` nicht viele Werte vorkommen die nur ganz wenige Ergebnisse liefern.
Mit chunksize=1 und lediglich einem (den größten Ordner) lande ich bei 418s statt 390s :D


Den Vorschlag mit scandir, versuche ich mal anzugehen.
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Karlirex: Das dann nur mit *einem* Ordner zu machen ist ja ein bisschen witzlos, denn da wird dann ja garantiert nichts parallel ausgeführt, und man hat nur den zusätzlichen Overhead das die Daten zwischen den Prozessen kommuniziert werden müssen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Karlirex
User
Beiträge: 126
Registriert: Dienstag 18. August 2020, 18:27

__blackjack__ hat geschrieben: Dienstag 13. Dezember 2022, 08:47 @Karlirex: Das dann nur mit *einem* Ordner zu machen ist ja ein bisschen witzlos, denn da wird dann ja garantiert nichts parallel ausgeführt, und man hat nur den zusätzlichen Overhead das die Daten zwischen den Prozessen kommuniziert werden müssen.
Naja allein der größte Ordner dauert mit Pool und auch den anderen Ideen besagt 400s.

Code: Alles auswählen

    start = timeit.default_timer()
    #for path in path_list:
    for root, dirs, files in os.walk(path_list):
        for filename in files:
            file_path = os.path.join(root, filename)
    end = timeit.default_timer()
    print("Zeit für einen Durchlauf: ", end-start)
hier die Idee mit walk, das dauert 112s für dazu noch alle anderen Ordner. Ich muss jetzt nur noch herausfinden, wie ich passend an die stats komme und das dann in ein Set so wie zuvor packen.
Mit einem drittel der Zeit kann ich mich nämlcih zu frieden stellen.
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

@Karlirex: sehr gut, Du hast herausgefunden, dass der `stat` Aufruf zwei Drittel der Zeit verbrät. Das hat jetzt nichts mit Deinem Rückschritt von pathlib auf os zu tun.
Willst Du auf Geschwindigkeit optimieren, mußt Du Dich in die low-level-API von os.scandir vertiefen, wie das bords0 ja schon vorgeschlagen hatte.
imonbln
User
Beiträge: 149
Registriert: Freitag 3. Dezember 2021, 17:07

Vielleicht ist der Ansatz, das Filesystem auf Zeitänderungen Änderungen zu scannen, einfach nicht ideal.
Jedes modernes Betriebssystem hat eine Kernel Schnittstelle, welche Events senden kann, wenn sich Dateien ändern.
Unter Linux wäre das zum Beispiel inotify unter Windows gibt es den FileSystemWatcher.

Das Pythonmodul watchdog https://pythonhosted.org/watchdog/ behauptet, die alle hinter einer API zu kapseln.
Vielleicht ist es Erfolgs versprechender, das Module zu benutzen und sich auf die gewünschten Events zu registrieren, statt selbst das Filesystem periodisch zu scannen.
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@imonbln: Kommt auf den Einsatzzweck an. Wenn ich einmal im Monat eine inkrementelle Sicherung machen will, dann ist es eher nicht so sinnvoll den ganzen Monat lang ein Programm laufen zu haben, das hoffentlich alle Änderungen mitbekommt.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
DeaD_EyE
User
Beiträge: 1021
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Da es mich mal interessiert hat, wie schlecht PyInotify funktioniert, habe ich das mal mit asyncio umgesetzt:

Code: Alles auswählen

import asyncio

from pyinotify import (
    IN_CLOSE_WRITE,
    IN_DELETE,
    IN_MOVED_FROM,
    IN_MOVED_TO,
    AsyncioNotifier,
    Event,
    WatchManager,
)


class FSEvent:
    def __init__(
        self,
        directory,
        *,
        recursive=True,
        loop=None,
        create_cb=None,
        delete_cb=None,
        moved_cb=None,
    ):
        self.loop = loop or asyncio.get_running_loop()

        self.create_cb = create_cb
        self.delete_cb = delete_cb
        self.moved_cb = moved_cb

        self._wm = WatchManager()
        self._notifier = AsyncioNotifier(
            self._wm, self.loop, default_proc_fun=self.handler
        )
        self.last_cookie_event = None

        events = IN_CLOSE_WRITE | IN_MOVED_TO | IN_MOVED_FROM | IN_DELETE
        self._wm.add_watch(directory, events, rec=recursive)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_obj, exc_tb):
        self._notifier.stop()

    def handler(self, event: Event):
        if event.mask & IN_MOVED_FROM:
            self.last_cookie_event = event
        elif event.mask & IN_MOVED_TO and event.cookie == self.last_cookie_event.cookie:
            if callable(self.moved_cb):
                asyncio.create_task(self.moved_cb(self.last_cookie_event, event))
            self.last_cookie_event = None
        elif event.mask & IN_CLOSE_WRITE:
            if callable(self.create_cb):
                asyncio.create_task(self.create_cb(event))
        elif event.mask & IN_DELETE:
            if callable(self.delete_cb):
                asyncio.create_task(self.delete_cb(event))


async def create_cb(event: Event):
    print(event.pathname, "geschrieben und geschlossen")


async def delete_cb(event: Event):
    print(event.pathname, "gelöscht")


async def moved_cb(event_from: Event, event_to: Event):
    print(f"{event_from.pathname} nach {event_to.pathname} verschoben")


async def main():
    watcher = FSEvent(
        "/home/andre/Downloads",
        create_cb=create_cb,
        delete_cb=delete_cb,
        moved_cb=moved_cb,
    )

    # verhindert, dass main beendet wird
    while True:
        await asyncio.sleep(10)


if __name__ == "__main__":
    #loop = asyncio.new_event_loop()
    #loop.run_until_complete(main())
    #loop.run_forever()

    # man soll ja asyncio.run nutzen
    asyncio.run(main())


# Ausgabe:
# [andre@andre-Fujitsu-i5 ~]$ python observer.py
# /home/andre/Downloads/test geschrieben und geschlossen
# /home/andre/Downloads/test nach /home/andre/Downloads/test2 verschoben
# /home/andre/Downloads/test2 nach /home/andre/Downloads/test verschoben
# /home/andre/Downloads/test gelöscht
Wenn mehrere Dateien verschoben werden und die Events nicht nacheinander kommen, funktioniert das nicht. Jedenfalls konnte ich das nicht reproduzieren. Möglicherweise kann so ein Szenario aber auftreten.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Karlirex
User
Beiträge: 126
Registriert: Dienstag 18. August 2020, 18:27

Code: Alles auswählen

    
complete_dict_with_time = dict()
    for path in path_list:
        for root, dirs, files in os.walk(path):
            for filename in files:
                file_path = os.path.join(root, filename)
                time = os.stat(file_path).st_mtime
                complete_dict_with_time[os.path.join(root, filename)] = os.stat(file_path).st_mtime
Mein aktueller Ansatz, der ähnlich wie die vorherige Funktion funktioniert. Das Dict könnte man dann wieder als set verknüpfen. Dauert 600s für den GESAMTEN Ordner (ist ein FileServer).
Das Suchintervall sollte recht flott sein, da die Idee dann läuft, sobald mit geänderten/neuen Daten weiterzuverarbeiten.
*muss aber, sollte ich beide Sets wie vorher verwenden, zweimal durchlaufen bevor der Setvergleich kommt. (Also 20min für einen Vergleich, was mMn. doch wieder recht lang ist).

Mit dem Watchdog-Package habe ich mich mal befasst, bin aber nicht sonderlich weit gekommen/warm geworden. Wahrscheinlich, weil ich da keine weitere Nutzung der Dateien habe?
Vllt schau ich mir das aber nochmal an.
Antworten