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

Я обнаружил, что использую 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 '>'
6
задан BalusC 10 August 2010 в 21:25
поделиться

5 ответов

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

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

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

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

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

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
8
ответ дан 8 December 2019 в 17:17
поделиться

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

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

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

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

3
ответ дан 8 December 2019 в 17:17
поделиться

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

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

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

  • Каждый раз, когда перехватывается исключение, оно повторно возбуждается , пока оно, наконец, не будет перехвачено функцией 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()
2
ответ дан 8 December 2019 в 17:17
поделиться

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

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

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

1
ответ дан 8 December 2019 в 17:17
поделиться

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

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

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

0
ответ дан 8 December 2019 в 17:17
поделиться
Другие вопросы по тегам:

Похожие вопросы: