6 Stimmen

Paranoia, übermäßige Protokollierung und Ausnahmebehandlung bei einfachen Skripten, die mit Dateien arbeiten. Ist das normal?

Ich verwende Python für viele Skripte zur Dateiverwaltung, wie das folgende. Bei der Suche nach Beispielen im Netz bin ich überrascht, wie wenig Logging und Ausnahmebehandlung in den Beispielen enthalten sind. Jedes Mal, wenn ich ein neues Skript schreibe, habe ich die Absicht, nicht so zu enden wie das untenstehende, aber wenn es mit Dateien zu tun hat, übernimmt meine Paranoia die Kontrolle und das Endergebnis ist nicht wie die Beispiele, die ich im Netz sehe. Da ich ein Neuling bin, würde ich gerne wissen, ob das normal ist oder nicht. Wenn nicht, wie gehen Sie dann mit den Unbekannten und der Angst vor dem Löschen wertvoller Informationen um?

def flatten_dir(dirname):
    '''Flattens a given root directory by moving all files from its sub-directories and nested 
    sub-directories into the root directory and then deletes all sub-directories and nested 
    sub-directories. Creates a backup directory preserving the original structure of the root
    directory and restores this in case of errors.
    '''
    RESTORE_BACKUP = False
    log.info('processing directory "%s"' % dirname)
    backup_dirname = str(uuid.uuid4())
    try:
        shutil.copytree(dirname, backup_dirname)
        log.debug('directory "%s" backed up as directory "%s"' % (dirname,backup_dirname))
    except shutil.Error:
        log.error('shutil.Error: Error while trying to back up the directory')
        sys.stderr.write('the program is terminating with an error\n')
        sys.stderr.write('press consult the log file\n')
        sys.stderr.flush()
        time.sleep(0.25)
        print 'Press any key to quit this program.'
        msvcrt.getch()
        sys.exit()

    for root, dirs, files in os.walk(dirname, topdown=False):
        log.debug('os.walk passing: (%s, %s, %s)' % (root, dirs, files))
        if root != dirname:
            for file in files:
                full_filename = os.path.join(root, file)
                try:
                    shutil.move(full_filename, dirname)
                    log.debug('"%s" copied to directory "%s"' % (file,dirname))
                except shutil.Error:
                    RESTORE_BACKUP = True
                    log.error('file "%s" could not be copied to directory "%s"' % (file,dirname))
                    log.error('flagging directory "%s" for reset' % dirname)
            if not RESTORE_BACKUP:
                try:
                    shutil.rmtree(root)
                    log.debug('directory "%s" deleted' % root)
                except shutil.Error:
                    RESTORE_BACKUP = True
                    log.error('directory "%s" could not be deleted' % root)
                    log.error('flagging directory "%s" for reset' % dirname)
        if RESTORE_BACKUP:
            break
    if RESTORE_BACKUP:
        RESTORE_FAIL = False
        try:
            shutil.rmtree(dirname)
        except shutil.Error:
            log.error('modified directory "%s" could not be deleted' % dirname)
            log.error('manual restoration from backup directory "%s" necessary' % backup_dirname)
            RESTORE_FAIL = True 
        if not RESTORE_FAIL:
            try:
                os.renames(backup_dirname, dirname)
                log.debug('back up of directory "%s" restored' % dirname)
                print '>'
                print '>******WARNING******'
                print '>There was an error while trying to flatten directory "%s"' % dirname
                print '>back up of directory "%s" restored' % dirname
                print '>******WARNING******'
                print '>'
            except WindowsError:
                log.error('backup directory "%s" could not be renamed to original directory name' % backup_dirname)
                log.error('manual renaming of backup directory "%s" to original directory name "%s" necessary' % (backup_dirname,dirname))
                print '>'
                print '>******WARNING******'
                print '>There was an error while trying to flatten directory "%s"' % dirname
                print '>back up of directory "%s" was NOT restored successfully' % dirname
                print '>no information is lost'
                print '>check the log file for information on manually restoring the directory'
                print '>******WARNING******'
                print '>'
    else:
        try:
            shutil.rmtree(backup_dirname)
            log.debug('back up of directory "%s" deleted' % dirname)
            log.info('directory "%s" successfully processed' % dirname)
            print '>directory "%s" successfully processed' % dirname
        except shutil.Error:
            log.error('backup directory "%s" could not be deleted' % backup_dirname)
            log.error('manual deletion of backup directory "%s" necessary' % backup_dirname)
            print '>'
            print '>******WARNING******'
            print '>directory "%s" successfully processed' % dirname
            print '>cleanup of backup directory "%s" failed' % backup_dirname
            print '>manual cleanup necessary'
            print '>******WARNING******'
            print '>'

8voto

Andrew Punkte 2793

Loslassen lernen (oder wie ich lernte, mit der Bombe zu leben)...

Fragen Sie sich selbst: Wovor genau haben Sie Angst, und wie werden Sie damit umgehen, wenn es passiert? In dem von Ihnen angeführten Beispiel wollen Sie Datenverluste vermeiden. Die Art und Weise, wie Sie damit umgehen, besteht darin, dass Sie nach jeder Kombination von Bedingungen suchen, die Sie für einen Fehler halten, und diese mit einer umfangreichen Protokollierung versehen. Es werden immer noch Dinge schief gehen, und es ist nicht klar, ob eine umfangreiche Protokollierung ein guter Weg ist, um damit umzugehen. Skizzieren Sie, was Sie zu erreichen versuchen:

for each file in a tree
  if file is below the root
    move it into the root
if nothing went wrong
  delete empty subtrees

Was kann bei diesem Prozess alles schief gehen? Nun, es gibt viele Möglichkeiten, wie die Operationen zum Verschieben von Dateien aufgrund des zugrunde liegenden Dateisystems scheitern können. Können wir sie alle aufzählen und nette Wege anbieten, damit umzugehen? Nein... aber im Allgemeinen werden Sie mit allen auf die gleiche Weise umgehen. Manchmal ist ein Fehler einfach nur ein Fehler, unabhängig davon, was er ist.

Wenn also in diesem Fall ein Fehler auftritt, wollen Sie abbrechen und alle Änderungen rückgängig machen. Sie haben sich dafür entschieden, eine Sicherungskopie zu erstellen und diese wiederherzustellen, wenn etwas schief geht. Der wahrscheinlichste Fehler ist jedoch, dass das Dateisystem voll ist. In diesem Fall werden diese Schritte wahrscheinlich fehlschlagen.... Ok, das ist also ein häufiges Problem - wenn Sie sich Sorgen machen, dass zu irgendeinem Zeitpunkt unbekannte Fehler auftreten könnten, wie können Sie dann verhindern, dass Ihr Wiederherstellungspfad schief geht?

Die allgemeine Antwort lautet: Stellen Sie sicher, dass Sie zuerst alle Zwischenarbeiten erledigen und dann einen einzelnen mühsamen (hoffentlich atomaren) Schritt tun. In Ihrem Fall müssen Sie Ihre Wiederherstellung umdrehen. Anstatt eine Kopie als Backup zu erstellen, erstellen Sie eine Kopie des Ergebnisses. Wenn alles klappt, können Sie das neue Ergebnis gegen den alten Originalbaum austauschen. Oder, wenn Sie wirklich paranoid sind, können Sie diesen Schritt einem Menschen überlassen. Der Vorteil dabei ist, dass Sie, wenn etwas schief geht, einfach abbrechen und den von Ihnen erstellten Teilzustand wegwerfen können.

Ihre Struktur wird dann zu :

make empty result directory
for every file in the tree
  copy file into new result
on failure abort otherwise
  move result over old source directory

Übrigens gibt es einen Fehler in Ihrem aktuellen Skript, den dieser Pseudocode deutlicher macht: Wenn Sie Dateien mit identischen Namen in verschiedenen Zweigen haben, überschreiben sie sich in der neuen, reduzierten Version gegenseitig.

Der zweite Punkt über diese Psuedo-Code ist, dass alle die Fehlerbehandlung an der gleichen Stelle ist (dh wrap das neue Verzeichnis und rekursive Kopie in einem einzigen Try-Block und fangen alle Fehler nach ihm), dies löst Ihr ursprüngliches Problem über das große Verhältnis der Protokollierung / Fehlerprüfung zu tatsächlichen Arbeitscode.

backup_dirname = str(uuid.uuid4())
try:
    shutil.mkdir(backup_dirname)
    for root, dirs, files in os.walk(dirname, topdown=False):
        for file in files:
            full_filename = os.path.join(root, file)
            target_filename = os.path.join(backup_dirname,file)
            shutil.copy(full_filename, target_filename)
catch Exception, e:
    print >>sys.stderr, "Something went wrong %s" % e
    exit(-1)
shutil.move(back_dirname,root)      # I would do this bit by hand really

3voto

Vivin Paliath Punkte 90791

Es ist in Ordnung, ein wenig paranoid zu sein. Aber es gibt verschiedene Arten von Paranoia :). Während der Entwicklungsphase verwende ich viele Debug-Anweisungen, damit ich sehen kann, wo ich etwas falsch mache (falls ich etwas falsch mache). Manchmal lasse ich diese Anweisungen drin, verwende aber ein Flag, um zu kontrollieren, ob sie angezeigt werden müssen oder nicht (quasi ein Debug-Flag). Sie könnten auch ein "Verbosity"-Flag verwenden, um zu steuern, wie viel Protokollierung Sie machen.

Die andere Art von Paranoia geht mit einer Überprüfung der Vernunft einher. Diese Paranoia kommt ins Spiel, wenn Sie sich auf externe Daten oder Tools verlassen - so ziemlich alles, was nicht aus Ihrem Programm stammt. In diesem Fall schadet es nie, paranoid zu sein (besonders bei Daten, die Sie erhalten - traue ihm nie ).

Es ist auch in Ordnung, paranoid zu sein, wenn Sie überprüfen, ob ein bestimmter Vorgang erfolgreich abgeschlossen wurde. Das ist nur ein Teil der normalen Fehlerbehandlung. Mir ist aufgefallen, dass Sie Funktionen wie das Löschen von Verzeichnissen und Dateien durchführen. Dies sind Vorgänge, die möglicherweise fehlschlagen können, und deshalb müssen Sie muss mit dem Szenario umgehen, dass sie scheitern. Wenn Sie es einfach ignorieren, könnte Ihr Code in einem unbestimmten/undefinierten Zustand enden und möglicherweise schlechte (oder zumindest unerwünschte) Dinge tun.

Was die Protokolldateien und Debug-Dateien betrifft, so können Sie diese beibehalten, wenn Sie es wünschen. Ich führe in der Regel eine vernünftige Menge an Protokollierung durch; gerade genug, um mir zu sagen, was vor sich geht. Das ist natürlich subjektiv. Das Wichtigste ist, dass Sie sich nicht in der Protokollierung ertränken, d. h. dass es so viele Informationen gibt, dass Sie sie nicht mehr leicht herausfinden können. Die Protokollierung im Allgemeinen hilft Ihnen herauszufinden, was falsch gelaufen ist, wenn ein von Ihnen geschriebenes Skript plötzlich nicht mehr funktioniert. Anstatt das Programm durchzugehen, um es herauszufinden, können Sie sich anhand der Protokolle einen ungefähren Eindruck verschaffen, wo das Problem liegt.

2voto

Jason Orendorff Punkte 39655

Paranoia kann definitiv verdecken, was Ihr Code zu tun versucht. Das ist aus mehreren Gründen eine sehr schlechte Sache. Es verbirgt Fehler. Es macht es schwieriger, das Programm zu ändern, wenn man es für etwas anderes braucht. Es erschwert die Fehlersuche.

Angenommen, Amoss kann Sie nicht von Ihrer Paranoia heilen, dann würde ich das Programm folgendermaßen umschreiben. Beachten Sie das:

  • Jeder Codeblock, der eine Menge Paranoia enthält, wird in eine eigene Funktion aufgeteilt.

  • Jedes Mal, wenn eine Ausnahme abgefangen wird, wird sie Wiedererhöhung bis sie schließlich in der main Funktion. Damit entfällt die Notwendigkeit von Variablen wie RESTORE_BACKUP y RESTORE_FAIL .

  • Das Herzstück des Programms (in flatten_dir ) ist jetzt nur noch 17 Zeilen lang und frei von Paranoia.


def backup_tree(dirname, backup_dirname):
    try:
        shutil.copytree(dirname, backup_dirname)
        log.debug('directory "%s" backed up as directory "%s"' % (dirname,backup_dirname))
    except:
        log.error('Error trying to back up the directory')
        raise

def move_file(full_filename, dirname):
    try:
        shutil.move(full_filename, dirname)
        log.debug('"%s" copied to directory "%s"' % (file,dirname))
    except:
        log.error('file "%s" could not be moved to directory "%s"' % (file,dirname))
        raise

def remove_empty_dir(dirname):
    try:
        os.rmdir(dirname)
        log.debug('directory "%s" deleted' % dirname)
    except:
        log.error('directory "%s" could not be deleted' % dirname)
        raise

def remove_tree_for_restore(dirname):
    try:
        shutil.rmtree(dirname)
    except:
        log.error('modified directory "%s" could not be deleted' % dirname)
        log.error('manual restoration from backup directory "%s" necessary' % backup_dirname)
        raise

def restore_backup(backup_dirname, dirname):
    try:
        os.renames(backup_dirname, dirname)
        log.debug('back up of directory "%s" restored' % dirname)
        print '>'
        print '>******WARNING******'
        print '>There was an error while trying to flatten directory "%s"' % dirname
        print '>back up of directory "%s" restored' % dirname
        print '>******WARNING******'
        print '>'
    except:
        log.error('backup directory "%s" could not be renamed to original directory name' % backup_dirname)
        log.error('manual renaming of backup directory "%s" to original directory name "%s" necessary' % (backup_dirname,dirname))
        print '>'
        print '>******WARNING******'
        print '>There was an error while trying to flatten directory "%s"' % dirname
        print '>back up of directory "%s" was NOT restored successfully' % dirname
        print '>no information is lost'
        print '>check the log file for information on manually restoring the directory'
        print '>******WARNING******'
        print '>'
        raise

def remove_backup_tree(backup_dirname):
    try:
        shutil.rmtree(backup_dirname)
        log.debug('back up of directory "%s" deleted' % dirname)
        log.info('directory "%s" successfully processed' % dirname)
        print '>directory "%s" successfully processed' % dirname
    except shutil.Error:
        log.error('backup directory "%s" could not be deleted' % backup_dirname)
        log.error('manual deletion of backup directory "%s" necessary' % backup_dirname)
        print '>'
        print '>******WARNING******'
        print '>directory "%s" successfully processed' % dirname
        print '>cleanup of backup directory "%s" failed' % backup_dirname
        print '>manual cleanup necessary'
        print '>******WARNING******'
        print '>'
        raise

def flatten_dir(dirname):
    '''Flattens a given root directory by moving all files from its sub-directories and nested 
    sub-directories into the root directory and then deletes all sub-directories and nested 
    sub-directories. Creates a backup directory preserving the original structure of the root
    directory and restores this in case of errors.
    '''
    log.info('processing directory "%s"' % dirname)
    backup_dirname = str(uuid.uuid4())
    backup_tree(dirname, backup_dirname)
    try:
        for root, dirs, files in os.walk(dirname, topdown=False):
            log.debug('os.walk passing: (%s, %s, %s)' % (root, dirs, files))
            if root != dirname:
                for file in files:
                    full_filename = os.path.join(root, file)
                    move_file(full_filename, dirname)
                remove_empty_dir(dirname)
    except:
        remove_tree_for_restore(dirname)
        restore_backup(backup_dirname, dirname)
        raise
    else:
        remove_backup_tree(backup_dirname)

def main(dirname):
    try:
        flatten_dir(dirname)
    except:
        import exceptions
        logging.exception('error flattening directory "%s"' % dirname)
        exceptions.print_exc()
        sys.stderr.write('the program is terminating with an error\n')
        sys.stderr.write('press consult the log file\n')
        sys.stderr.flush()
        time.sleep(0.25)
        print 'Press any key to quit this program.'
        msvcrt.getch()
        sys.exit()

1voto

JAL Punkte 20777

Das scheint mir vernünftig zu sein. Es kommt darauf an, wie wichtig Ihre Daten sind.

Ich fange oft so an und lasse die Protokollierung optional sein, wobei ein Flag am Anfang der Datei (oder vom Aufrufer) gesetzt wird, das die Protokollierung ein- oder ausschaltet. Sie könnten auch eine Ausführlichkeit haben.

Im Allgemeinen höre ich auf, die Protokolle zu lesen, nachdem etwas eine Weile funktioniert hat und nicht mehr in der Entwicklung ist, und es entstehen riesige Protokolldateien, die ich nie lese. Wenn aber doch etwas schief geht, ist es gut zu wissen, dass sie da sind.

0voto

Jason Orendorff Punkte 39655

Wenn es in Ordnung ist, die Arbeit halbfertig zu machen auf Fehler (nur einige Dateien verschoben), solange keine Dateien verloren gehen, ist das Sicherungsverzeichnis unnötig. Sie können also einen wesentlich einfacheren Code schreiben:

import os, logging

def flatten_dir(dirname):
    for root, dirs, files in os.walk(dirname, topdown=False):
        assert len(dirs) == 0
        if root != dirname:
            for file in files:
                full_filename = os.path.join(root, file)
                target_filename = os.path.join(dirname, file)
                if os.path.exists(target_filename):
                    raise Exception('Unable to move file "%s" because "%s" already exists'
                                    % (full_filename, target_filename))
                os.rename(full_filename, target_filename)
            os.rmdir(root)

def main():
    try:
        flatten_dir(somedir)
    except:
        logging.exception('Failed to flatten directory "%s".' % somedir)
        print "ERROR: Failed to flatten directory. Check log files for details."

Jeder einzelne Systemaufruf macht hier Fortschritte, ohne Daten zu zerstören, die Sie behalten wollten. Ein Sicherungsverzeichnis ist nicht erforderlich, da es nie etwas gibt, das Sie "wiederherstellen" müssen.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X