0%

python 删除超大目录

通常,使用shutil.rmtree就可以轻易的删除目录,但是量变产生质变,当目录中的文件数量达到数百万时,我们需要更加快速和平稳的方式

优化的核心思想就是要尽可能减少一切不必要的io

通常的删除过程

  1. 读取目录数据,获得全部的文件
  2. 遍历文件,依次获得文件的创建日期等信息(sata)
  3. 删除满足条件的文件

这样的流程看似没有问题,但是实际上在第一步就会由于读目录给系统造成极大的压力,百万级的目录光是统计一遍就会花费大量的时间。此外在第二部,连续的sata查询也会带来极大的压力。

此外python常用的接口包含了过多不必要的io开销,这些不必要的sata查询会导致io的次数增加数倍,让本来就很慢的过程雪上加霜。

例如:

  • os.listdir:一次性读取,大目录下增加磁盘压力,并且会执行很久
  • os.walk:间隙读取,但是存在额外的sata读取开销

参考:https://pythonrepo.com/repo/benhoyt-scandir-python-files

解决方案

  1. 读取目录数据,获得一个文件
  2. 获得文件的创建日期等信息(sata)
  3. 删除满足条件的文件
  4. 休眠一小段时间
  5. 回到第一步

这样的流程有相比之前要更加平稳,读取目录和获得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
# logger = print

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:
# 由于向文件里写文件时会修改文件夹的ctime,所以实际上走不到这个分支
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())