Паранойя, чрезмерное ведение журнала и обработка исключений в простых сценариях, работающих с файлами.Это нормально? - PullRequest
6 голосов
/ 04 августа 2010

Я использую python для множества скриптов управления файлами, как показано ниже. При поиске примеров в сети, я удивляюсь тому, как мало примеров регистрации и обработки исключений. Каждый раз, когда я пишу новый сценарий, я не собираюсь заканчивать тем, что показан ниже, но если он имеет дело с файлами, то не имеет значения, какая у меня паранойя, и конечный результат не похож на примеры, которые я вижу в сети. Поскольку я новичок, я хотел бы знать, нормально это или нет. Если нет, то как вы справляетесь с неизвестными и страхами удаления ценной информации?

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 '>'

Ответы [ 5 ]

8 голосов
/ 04 августа 2010

Учимся отпускать (или как я научился жить с бомбой) ...

Спросите себя: чего именно вы боитесь, и как вы справитесь с этим, если это произойдет? В приведенном вами примере вы хотите избежать потери данных. Способ, которым вы справились с этим, заключается в поиске каждой комбинации условий, которые вы считаете ошибкой, и размещении огромного количества записей в ней. Все по-прежнему пойдет не так, и неясно, что большое количество журналов будет хорошим способом справиться с этим. Набросок того, чего вы пытаетесь достичь:

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

Так что же может пойти не так в этом процессе? Ну, есть много способов, с помощью которых операции перемещения файлов могут прекратиться из-за базовой файловой системы. Можем ли мы перечислить их все и предоставить хорошие способы борьбы с ними? Нет ... но в целом вы будете иметь дело с ними одинаково. Иногда ошибка - это просто ошибка, независимо от того, что это такое.

Так что в этом случае, если возникает какая-либо ошибка, вы хотите отменить и отменить любые изменения. Вы решили это сделать, создав резервную копию и восстановив ее, если что-то пойдет не так. Но наиболее вероятной ошибкой является переполнение файловой системы, и в этом случае эти шаги могут завершиться ошибкой ... Хорошо, это достаточно распространенная проблема - если вы в какой-то момент беспокоитесь о неизвестных ошибках, как вы можете остановить свою работу? путь восстановления от неправильного?

Общий ответ - сначала убедитесь, что вы выполняете какую-либо промежуточную работу, а затем делаете один неприятный (возможно, атомарный) шаг. В вашем случае вам нужно перевернуть ваше восстановление вокруг. Вместо того, чтобы создавать копию в качестве резервной копии, создайте копию результата. Если все получится, вы можете поменять новый результат на старое оригинальное дерево. Или, если вы действительно параноик, вы можете оставить этот шаг для человека. Преимущество здесь в том, что если что-то пойдет не так, вы можете просто прервать и выбросить созданное вами частичное состояние.

Ваша структура становится:

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

Кстати, в вашем текущем скрипте есть ошибка, которая делает этот псевдо-код более очевидным: если у вас есть файлы с одинаковыми именами в разных ветвях, они будут перезаписывать друг друга в новой плоской версии.

Второй момент, касающийся этого псевдо-кода, заключается в том, что вся обработка ошибок находится в одном и том же месте (т. Е. Оборачивает каталог make new и рекурсивную копию в один блок try и перехватывает все ошибки после него), это решает исходную проблему вопрос о большом соотношении регистрации / проверки ошибок к фактическому рабочему коду.

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
3 голосов
/ 04 августа 2010

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

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

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

Что касается файлов журнала и файлов отладки, вы можете оставить их, если хотите. Я обычно выполняю приличное количество регистрации; достаточно, чтобы рассказать мне, что происходит. Конечно, это субъективно. Ключ должен удостовериться, что Вы не утопаете в регистрации; где так много информации, что ее нелегко найти. В целом, ведение журнала помогает понять, что происходит, когда неправильно написанный сценарий внезапно перестает работать. Вместо того, чтобы пройтись по программе, чтобы выяснить это, вы можете получить приблизительное представление о том, где проблема, просматривая ваши журналы.

2 голосов
/ 04 августа 2010

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

Предполагая, что Amoss не может вылечить вас от вашей паранойи, вот как я могу переписать программу.Обратите внимание:

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

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

  • Суть программы (в flatten_dir) теперь составляет всего 17 строк и не содержит паранойи.


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()
1 голос
/ 04 августа 2010

Это кажется мне разумным. Это зависит от того, насколько важны ваши данные.

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

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

0 голосов
/ 04 августа 2010

Если можно оставить работу наполовину завершенной из-за ошибки (перемещены только некоторые файлы), пока файлы не потеряны, то каталог резервного копирования не нужен. Таким образом, вы можете написать значительно более простой код:

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."

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

...