通常,使用shutil.rmtree就可以轻易的删除目录,但是量变产生质变,当目录中的文件数量达到数百万时,我们需要更加快速和平稳的方式
优化的核心思想就是要尽可能减少一切不必要的io
通常的删除过程
- 读取目录数据,获得全部的文件
- 遍历文件,依次获得文件的创建日期等信息(sata)
- 删除满足条件的文件
这样的流程看似没有问题,但是实际上在第一步就会由于读目录给系统造成极大的压力,百万级的目录光是统计一遍就会花费大量的时间。此外在第二部,连续的sata查询也会带来极大的压力。
此外python常用的接口包含了过多不必要的io开销,这些不必要的sata查询会导致io的次数增加数倍,让本来就很慢的过程雪上加霜。
例如:
- os.listdir:一次性读取,大目录下增加磁盘压力,并且会执行很久
- os.walk:间隙读取,但是存在额外的sata读取开销
参考:https://pythonrepo.com/repo/benhoyt-scandir-python-files
解决方案
- 读取目录数据,获得一个文件
- 获得文件的创建日期等信息(sata)
- 删除满足条件的文件
- 休眠一小段时间
- 回到第一步
这样的流程有相比之前要更加平稳,读取目录和获得sata为一个一个读取,而且可以根据自己的需要灵活的休眠,面对海量文件不会把硬盘打满,给其他程序留一些io资源。
为了达成间断读取的目标,我们需要使用os.scandir这个接口,相比于os.walk,他会直接返回dirent的信息,而不会再去查询sata
以下的代码,实现了对超时文件和目录的删除,但是并没有实现休眠。
关于sata在不同平台上的差异,请参考:https://docs.python.org/3/library/stat.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| import logging import os import time import traceback from multiprocessing import Process
logger = logging.info
def clean_by_ttl(path, ttl, dry_run): """ path: 待扫描的目录 ttl: 文件生存时间(time to live) dry_run: 是否真正进行删除操作 """ logger(f"cleaner: path {path}, ttl: {ttl}, dry_run: {dry_run}") count = 0 now = time.time() process_dir = [path] delete_dir = [] while len(process_dir) > 0: logger(f"cleaner: enter dir {process_dir[0]}") with os.scandir(process_dir[0]) as it: file_num = 0 delete_num = 0 for entry in it: count += 1 file_num += 1 life_time = now - entry.stat().st_ctime if life_time < ttl: logger(f"cleaner: ({count}) skip {entry.path}, life_time: {life_time}") else: delete_num += 1 if entry.is_file(): if not dry_run: logger(f"cleaner: ({count}) delete file {entry.path}, life_time: {life_time}") os.remove(entry.path) else: logger(f"cleaner: ({count}) mock delete file {entry.path}, life_time: {life_time}") elif entry.is_dir(): logger(f"cleaner: ({count}) discover ttl dir {entry.path}, life_time: {life_time}") process_dir.append(entry.path)
if process_dir[0] != path: if file_num == delete_num: logger(f"cleaner: ({count}) determin delete dir {process_dir[0]}") delete_dir.append(process_dir[0]) else: logger(f"cleaner: ({count}) ttl dir {entry.path} has unttl file, file_num: {file_num}, delete_num: {delete_num}") process_dir = process_dir[1:] for dir in reversed(delete_dir): try: if not dry_run: logger(f"cleaner: delete dir {dir}") os.rmdir(dir) else: logger(f"cleaner: mock delete dir {dir}") except Exception as e: logger(f"cleaner: err={e}") logger(traceback.format_exc())
|