Я обнаружил, что использую 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 '>'
Научиться отпускать (или как я научился жить с бомбой) ...
Спросите себя: чего именно вы боитесь и как вы с этим справитесь, если это произойдет? В приведенном вами примере вы хотите избежать потери данных. Вы справились с этим путем поиска всех комбинаций условий, которые, по вашему мнению, являются ошибкой, и проведения огромного количества записей в журнале. Что-то по-прежнему пойдет не так, и неясно, будет ли большой объем журналов хорошим способом справиться с этим. Наброски того, чего вы пытаетесь достичь:
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
Это нормально быть маленьким параноиком. Но бывают разные виды паранойи :). На этапе разработки я использую много отладочных операторов, чтобы видеть, где я ошибаюсь (если я ошибаюсь). Иногда я оставляю эти операторы, но использую флаг, чтобы контролировать, нужно ли их отображать или нет (в значительной степени флаг отладки). У вас также может быть флаг «многословия», чтобы контролировать, сколько вы ведете журнала.
Другой тип паранойи связан с проверками на вменяемость. Эта паранойя проявляется, когда вы полагаетесь на внешние данные или инструменты - почти все, что не исходит из вашей программы. В этом случае никогда не повредит быть параноиком (особенно с данными, которые вы получаете - никогда не доверяйте им ).
Также нормально быть параноиком, если вы проверяете, успешно ли завершилась конкретная операция. Это всего лишь часть нормальной обработки ошибок. Я заметил, что вы выполняете такие функции, как удаление каталогов и файлов. Это операции, которые потенциально могут потерпеть неудачу, поэтому вы должны разобраться со сценарием, в котором они потерпят неудачу. Если вы просто проигнорируете это, ваш код может оказаться в неопределенном / неопределенном состоянии и потенциально может делать плохие (или, по крайней мере, нежелательные) вещи.
Что касается файлов журнала и файлов отладки, вы можете оставить их, если хотите. Обычно я веду приличное количество журналов; достаточно, чтобы сказать мне, что происходит. Конечно, это субъективно. Ключ в том, чтобы не утонуть в ведении журнала; где информации так много, что ее сложно разобрать. Ведение журнала в целом помогает понять, что делать, если написанный вами скрипт внезапно перестает работать. Вместо того, чтобы разобраться в программе, вы можете получить приблизительное представление о том, в чем проблема, просмотрев свои журналы.
Паранойя определенно может скрыть то, что пытается сделать ваш код. Это очень плохо по нескольким причинам. Скрывает ошибки. Это затрудняет изменение программы, когда вам нужно сделать что-то еще. Это затрудняет отладку.
Предполагая, что Амосс не может вылечить вас от вашей паранойи, вот как я мог бы переписать программу.Обратите внимание:
Каждый блок кода, содержащий много паранойи, разделен на отдельную функцию.
Каждый раз, когда перехватывается исключение, оно повторно возбуждается , пока оно, наконец, не будет перехвачено функцией 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()
Мне это кажется разумным. Это зависит от того, насколько важны ваши данные.
Я часто начинаю с этого и делаю регистрацию необязательной, с флагом, установленным в верхней части файла (или вызывающим), включающим или выключающим регистрацию. У вас также может быть многословие.
Обычно, когда что-то работает какое-то время и больше не находится в разработке, я прекращаю читать журналы и создаю гигантские файлы журналов, которые никогда не читал. Однако, если что-то пойдет не так, хорошо знать, что они есть.
Если можно оставить задание наполовину завершенным из-за ошибки (перемещены только некоторые файлы), если файлы не потеряны, то каталог резервных копий не нужен. Таким образом, вы можете написать значительно более простой код:
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."
Каждый отдельный системный вызов здесь выполняет работу без уничтожения данных, которые вы хотели сохранить. Нет необходимости в резервном каталоге, потому что вам никогда ничего не нужно «восстанавливать».