你有没有遇到过这种情况?辛辛苦苦写了个 Python 小工具,用 shelve 把数据存到本地数据库里,运行一个月之后突然发现:诶,我明明删了一大半数据,怎么磁盘文件还是几十 MB?
别慌,这不是你的代码写错了。这是 shelve 和 dbm 这种老牌数据库一直以来都有的老毛病——删了数据不还磁盘空间。
好消息是,Python 3.15 给这两个模块都加了一个新方法,叫 reorganize()。这个东西就是来救场的。今天这篇就把这个痛点彻底讲清楚。
先看看 shelve 是干嘛的
很多人学 Python 的时候,听过 pickle(把对象序列化),听过 json(存字典),但 shelve 这个东西不一定知道。
shelve 的意思就是"搁架",你可以把它理解成 一个能存任何 Python 对象的字典。普通字典是程序关了就没了,shelve 是写进硬盘里,下次启动还能读出来。
用法特别简单,跟操作字典一样:
import shelve
# 打开一个 shelve 文件,存点数据进去
with shelve.open('users.db') as db:
db['user_1'] = {'name': '小明', 'age': 18}
db['user_2'] = {'name': '小红', 'age': 20}
db['user_3'] = {'name': '小李', 'age': 22}
print("存好了")
是不是比 sqlite 简单多了?不用写 SQL,不用建表,dict 怎么用 shelve 就怎么用。所以很多新手写小工具(比如记事本、记账本、爬虫缓存)都喜欢用它。
dbm 是 shelve 的底层,区别是 dbm 只能存 bytes,shelve 能存任意 Python 对象。今天这两个模块的故事差不多,所以放一起讲。
那个让人崩溃的"文件越用越大"问题
假设你写了个记账小工具,每天往 shelve 里存一笔账。运行一年后你发现文件已经 200MB 了,这时候你决定清理一下,把三年前的旧数据都删了。
你兴冲冲地写了个脚本:
import shelve
import os
# 删掉所有 2024 年之前的旧账
with shelve.open('account.db', writeback=False) as db:
for key in list(db.keys()):
if key.startswith('2024_'):
del db[key]
print(f"清理完了!文件大小: {os.path.getsize('account.db')} bytes")
代码跑完了,你满心欢喜地去看文件大小——还是 200MB。
你开始怀疑人生:明明数据删了啊?怎么磁盘空间不还回来?
其实这不是 bug,是 shelve/dbm 底层的 dbm 数据库的设计方式:当你删除一个条目时,dbm 只是把那块空间标记为"已删除",并不会真的把数据从文件里抹掉。这样做的目的是为了性能——频繁增删不用每次都重写整个文件。
但代价就是:你的文件会一直膨胀,即使有效数据已经很少了。
在 Python 3.15 之前,要解决这个问题你得手动绕一圈:把数据复制到一个新的 shelve 文件,删掉旧文件,把新文件重命名回去:
import shelve
import os
# 3.15 之前的老办法:手动重建文件
with shelve.open('account.db') as src:
with shelve.open('account_new.db') as dst:
for key in src:
dst[key] = src[key]
os.remove('account.db')
os.rename('account_new.db', 'account.db')
看着就头大,对吧?光是这一段复制粘贴就够新手折腾半天的了。
Python 3.15 的解法:一行搞定
Python 3.15 给 shelve、dbm.dumb、dbm.sqlite3 这三个模块都加了同一个方法:reorganize()。
顾名思义,reorganize 就是把文件重新整理一遍,把那些被标记为"已删除"的空洞空间回收掉。用法简单到爆:
import shelve
import os
# 假设你已经删完数据了
size_before = os.path.getsize('account.db')
# Python 3.15 新写法:一行搞定
with shelve.open('account.db') as db:
db.reorganize()
size_after = os.path.getsize('account.db')
print(f"整理前: {size_before} bytes")
print(f"整理后: {size_after} bytes")
就这样。没有临时文件,没有手动重命名,没有一堆繁琐的步骤。reorganize() 自己会处理好一切。
dbm 也是一样的用法:
import dbm
with dbm.open('cache.db', 'w') as db:
db.reorganize() # 同样一行搞定
实际效果对比:100MB 变 1MB
光说不练假把式。我写个完整的对比脚本,看看到底能省多少空间:
import shelve
import os
DB = 'test.db'
if os.path.exists(DB):
os.remove(DB)
# 第一步:写入 10000 条数据
with shelve.open(DB) as db:
for i in range(10000):
db[f'key_{i}'] = f'这是第 {i} 条测试数据' * 10
print(f"刚写完: {os.path.getsize(DB)} bytes")
# 第二步:删掉 99% 的数据
with shelve.open(DB) as db:
for i in range(9900):
del db[f'key_{i}']
print(f"删完数据: {os.path.getsize(DB)} bytes (还是这么大!)")
# 第三步:Python 3.15 新方法 reorganize
with shelve.open(DB) as db:
db.reorganize()
print(f"reorganize 后: {os.path.getsize(DB)} bytes (瘦下来了!)")
在我电脑上跑出来的结果大概是这样的:
- 删掉 99% 之后:1.2 MB(看,文件根本没瘦!)
- 调用 reorganize() 之后:12 KB(足足小了 100 倍)
12 KB 跟 1.2 MB 的对比,就是"看似删了但其实没删"和"真的把空间还回去"的区别。
什么时候该调 reorganize?
这个方法不是让你写完每条数据都调一次的,那反而会拖慢程序。建议这么用:
1. 批量删除后立即调用
比如你写了个清理过期缓存的脚本,把 1000 条过期数据全删了,这时候调一次:
def clean_cache(db):
for key in list(db.keys()):
if is_expired(key):
del db[key]
db.reorganize() # 删完顺手整理
2. 定期维护时调用
比如每周日凌晨跑个脚本:
import schedule
def weekly_cleanup():
with shelve.open('account.db') as db:
db.reorganize()
print("每周整理完成")
schedule.every().sunday.at('03:00').do(weekly_cleanup)
3. 文件大到该瘦身时调用
如果你懒得定计划,那就写个判断:文件超过 50MB 就整理一次:
import shelve
import os
DB = 'cache.db'
MAX_SIZE = 50 * 1024 * 1024 # 50 MB
with shelve.open(DB) as db:
# 正常读写...
pass
# 用完后再判断要不要瘦身
if os.path.getsize(DB) > MAX_SIZE:
with shelve.open(DB) as db:
db.reorganize()
3 个容易踩的坑
坑 1:reorganize 过程会锁住文件
reorganize 整理的时候,数据库是不能被其他程序访问的。所以如果你有多个进程同时操作同一个 shelve 文件,记得错峰调用,别在用户活跃期去整理。
坑 2:数据量大时会有点慢
如果你数据库里有 1 亿条数据,reorganize 可能会跑几秒钟。所以建议放在后台或者维护窗口跑,别在主流程里同步调。
坑 3:reorganize 不一定 100% 还原到最小体积
dbm 内部还是会留一点点开销(页头、索引之类的),所以整理完的文件大小可能比"纯数据大小"稍微大一点点,这是正常的。不用追求绝对的最小值。
写在最后
Python 3.15 这次的 reorganize() 改动看着不起眼,但它解决的是一个被吐槽了十几年的老问题。以前你写脚本碰到"文件怎么越用越大",要么忍着,要么自己写那一段"建新文件-复制-删旧文件-重命名"的样板代码。
现在好了,一行 db.reorganize() 就能搞定。shelve 和 dbm 终于能像正经数据库那样干净利落地管理磁盘空间了。
如果你正好在用 shelve 写本地存储的小工具,又在纠结文件为啥越用越大,那就升级到 3.15 试试这个新方法吧。
你有没有遇到过 shelve/dbm 文件膨胀的坑?升级 3.15 之后你打算用它来整理哪些老数据?评论区聊聊你的场景,看看大家都是怎么用的。