数据住进了数据库、网页也有了脸面,可 shop.db 就静静躺在那个文件夹里——一次误删、一次覆盖、一次磁盘故障,仨月的积累就能归零。今天学一个新概念:导出+导入。把整座仓库”倒”成一份货单存到别处(导出),哪天仓库塌了再照单把货”灌”回来(导入)。代码都是几分钟的活儿,但”没备份”三个字的代价,是任何代码都补不回来的。
一、真实场景:攒了仨月的数据,一个手滑就没了
接着讲你的价格监控工具。它勤勤恳恳干了仨月,shop.db 里躺了上千条价格记录,网页上翻都翻不完。然后某天,你整理文件夹,把 shop.db 当成测试残留顺手删了;或者更隐蔽的:你改代码时动了 DB = "shop.db" 的路径,程序”找不到旧库就悄悄新建了一个空的”(上篇坑2刚打过疫苗),你一周后才发现数据”空了”。
电脑从来不是保险箱。文件会误删、硬盘会坏、勒索病毒会把你的文件加密勒索钱。对程序来说,代码丢了可以重写——AI 还能帮你重写得更快;数据丢了就是真没了——那些价格记录是你仨月一天天攒的,没人能凭记忆重建。
所以正经软件都有备份功能:微信的聊天记录能备份,手机能云备份,你的小系统凭什么裸奔?今天就给它装上后手。
二、新概念:导出 + 导入
今天就这一个新概念,俩动作是一对:
- 导出(export):把数据库里的东西”倒”出来,存成另一个文件;
- 导入(import):从那个文件把东西”灌”回去。
有人要问:直接复制 shop.db 文件不行吗?——行,这是姿势一,30 秒搞定。但姿势二是把数据库导出成一份文本”货单”:把建表语句、每条记录的插入语句,全写成 SQL 语句存进一个 .sql 文件。
用搬家打比方:姿势一是把整个仓库连货架带货整体打包运走(搬 .db 文件),姿势二是抄一份货单——货架怎么搭的(CREATE TABLE)、每件货什么规格(INSERT),全写在单子上。货单的好处:人能看懂、能翻能查、能编辑(比如删掉测试时期的垃圾数据再导入),而且”提货”不挑仓库——哪天你换了台电脑、甚至换了 MySQL,单子照样能提货。坏处也明显:大仓库抄单子慢、文件更大。
两招都学,都很短,配合着用才是完整后手。
三、姿势一:整库复制(30秒保命招)
Python 标准库自带 shutil,复制文件就一行:
# backup_copy.py — 30秒保命招:把整个数据库文件复制一份
import shutil
import time
import os
os.makedirs("backup", exist_ok=True) # 备份文件夹,没有就建
src = "shop.db"
dst = f"backup/shop-{time.strftime('%Y%m%d-%H%M%S')}.db"
shutil.copy2(src, dst) # copy2 连修改时间一起复制
print(f"已备份到 {dst}")
跑完,backup/ 文件夹里多了一个带时间戳的 .db 文件——同款仓库,货架和货一模一样,哪天原库出事,把这个文件改名成 shop.db 放回去就复活了。
三个零件认一下:
os.makedirs("backup", exist_ok=True):先给备份安个家。exist_ok=True是”文件夹已存在也别报错”,程序可以每天反复跑。time.strftime('%Y%m%d-%H%M%S'):生成”20261103-030000″这样的时间戳塞进文件名。为什么必须带它,坑5里讲。shutil.copy2:比普通 copy 多复制一份文件的修改时间,备份文件的时间信息和原件一致,方便日后核对。
注意备份时机:如果程序正开着往里写数据,复制走的可能是”写了一半”的文件(坑4细说)。手动备份时先把程序停了再跑,自动备份放在凌晨没人动的时候(第六节安排上)。
四、姿势二:导出成 SQL 文本(能看懂的备份)
sqlite3 模块自带一个好用的方法 iterdump()——把整个库”念”成一串 SQL 语句,一行一行吐出来:
# backup_dump.py — 把数据库导出成一份人能看懂的 SQL 货单
import sqlite3
import time
import os
os.makedirs("backup", exist_ok=True)
src = "shop.db"
dst = f"backup/shop-{time.strftime('%Y%m%d-%H%M%S')}.sql"
con = sqlite3.connect(src)
with open(dst, "w", encoding="utf-8") as f:
for line in con.iterdump():
f.write(line + "n")
con.close()
print(f"已导出 {dst},用记事本打开看看")
打开生成的 .sql 文件,你会看到这样的”货单”:
BEGIN TRANSACTION;
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL
);
INSERT INTO "products" VALUES(1,'酱油',8.5);
INSERT INTO "products" VALUES(2,'大米',32.0);
INSERT INTO "products" VALUES(3,'面粉',19.9);
COMMIT;
货架怎么搭(CREATE TABLE)、每件货什么规格(INSERT),一目了然。哪天你想确认”备份里到底有没有3月那批数据”,翻单子搜一下就行——二进制的 .db 文件可没法翻。iterdump() 会把库里所有表都倒出来,不用你操心表名。
五、导入:照单把货灌回来
备份不是终点,能恢复的备份才算备份。写 restore.py:
# restore.py — 从 SQL 备份恢复出一个数据库
import sqlite3
import os
backup_file = "backup/shop-20261103-030000.sql" # 换成你实际的备份文件名
target = "shop_restored.db" # 先恢复到新文件,别急着覆盖!
if not os.path.exists(backup_file):
print(f"找不到备份文件 {backup_file}")
raise SystemExit(1)
if os.path.exists(target):
print(f"警告:{target} 已存在,恢复会覆盖它")
raise SystemExit(1)
con = sqlite3.connect(target)
with open(backup_file, encoding="utf-8") as f:
con.executescript(f.read()) # 整份货单一次性执行
rows = con.execute("SELECT COUNT(*) FROM products").fetchone()[0]
con.close()
print(f"恢复完成,{target} 里有 {rows} 条商品记录")
python restore.py 跑完,目录里多了 shop_restored.db。两个关键设计:
target = "shop_restored.db"——先恢复到一个新文件,验证没问题再决定要不要改名顶替 shop.db。上来就覆盖旧库是新手最常犯的险,坑3细说。con.executescript(f.read()):一口气执行整份货单。货单开头有 BEGIN TRANSACTION、结尾有 COMMIT,所以要么全灌进去、要么全不灌,不会”灌了一半”。
恢复完必须验收,三板斧:
- 数条数:
SELECT COUNT(*)和备份前对得上吗?(脚本里已打印) - 抽查几条:
SELECT * FROM products LIMIT 5,看看酱油的价格还是 8.5 吗; - 跑一遍真程序:把 shop_restored.db 改名成 shop.db(顶替前旧库先复制保命),开网页点一点。
六、自动备份:每天凌晨跑,只留最近7份
手动备份的宿命是”忘了跑”。写个自动版,一次跑完两件事:备份 + 清理旧备份。
# auto_backup.py — 自动备份 + 只留最近 7 份
import sqlite3
import time
import glob
import os
os.makedirs("backup", exist_ok=True)
stamp = time.strftime("%Y%m%d-%H%M%S")
dst = f"backup/shop-{stamp}.sql"
# 1. 导出
con = sqlite3.connect("shop.db")
with open(dst, "w", encoding="utf-8") as f:
for line in con.iterdump():
f.write(line + "n")
con.close()
print(f"[{stamp}] 备份完成:{dst}")
# 2. 轮换:只留最近 7 份,旧的清掉
backups = sorted(glob.glob("backup/shop-*.sql"))
for old in backups[:-7]:
os.remove(old)
print(f"清理旧备份:{old}")
两个新面孔其实是老朋友的组合:
glob.glob("backup/shop-*.sql"):按通配符找文件,”backup 里所有 shop- 开头的 .sql”一次全捞出来。第30篇批量文件处理用过它的亲戚。sorted(...)[:-7]:文件名带时间戳,排序后就是按时间排的。[:-7]是”除了最后7份都归我”——最后7份是最近的,前面全删。列表切片,第8篇的基础功。
再让它每天凌晨3点自动跑:Windows 用”任务计划程序”、Linux 用 crontab -e 加一行 0 3 * * * python3 /你的路径/auto_backup.py。装上之后你就再也不会”忘了备份”。
七、五个坑点疫苗(先打再上路)
坑1:备份和数据库放同一个文件夹。 这等于”把备用钥匙和正钥匙挂同一个钥匙圈”——误删整个文件夹、勒索病毒加密整个盘、硬盘损坏,正主和备份一锅端。口诀:备份要住别处——换个盘、移动硬盘、或者同步到网盘(也别只信网盘,网盘还会同步删除呢)。
坑2:备份了仨月,从没恢复过一次。 没演练过恢复的备份等于薛定谔的备份——出事那天才发现货单是残的、脚本早坏了。每月拿备份真恢复一次(第五节的 restore.py 就是现成的演练脚本),10 分钟的事。备份不是目的,能恢复才是。
坑3:直接用备份覆盖 shop.db。 万一你拿错备份文件(比如拿的是上个月的),覆盖的瞬间,这个月的新数据就没了——为救数据反而丢了数据。铁律:先恢复到新文件,验证条数和内容,确认无误再顶替;顶替前,把现有 shop.db 也复制保命一份。
坑4:程序正写着数据,你去备份。 写入进行到一半时复制文件,备份里可能是半截数据(比如转账只记了支出没记收入)。两招避开:手动备份先把程序停了;自动备份排到凌晨没人用的时候。顺带一提,sqlite3 有个专门的 con.backup() 热备份 API,能在程序运行时安全备份,等你哪天数据大到”停不下来”再翻它的牌子。
坑5:备份文件名没有时间戳。 每天都输出成 backup.sql,等于每天覆盖昨天的备份——你永远只有”最新一份”,而最新一份恰恰可能是坏的(坑4那种半截货)。文件名必须带 time.strftime('%Y%m%d-%H%M%S'),旧的让轮换逻辑去清,而不是让新备份顶掉。
八、跟AI协作的正确姿势(备份特供版)
- 把保留策略说成人话。 不说”写个备份脚本”,而说”每天凌晨3点自动把 shop.db 备份成带日期的 SQL 文件,保留最近7份,旧的自动清理”——AI 产出的脚本直接合用,不用来回改。
- 恢复功能必须亲手破坏一次才能验收。 故意把 shop.db 改名藏起来,跑 restore.py,把恢复出来的库接上,看网页数据是不是全回来了。这是全系列唯一需要”搞一次破坏”来验收的功能,别偷懒只看脚本”长得对”。
- 让AI顺手写个体检。 “检查 backup/ 里最新的备份:文件大小不能是0,商品条数和昨天比不能骤降一半以上,否则打印警告”——备份脚本也会坏,给备份本身装个哨。
九、今日小练习
练习1:双保险备份脚本。 写一个 backup_all.py,一次干两件事:把 shop.db 整体复制到 backup/db/,同时导出 SQL 货单到 backup/sql/,两边文件名都带同一个时间戳。
提示:shutil.copy2 + iterdump() 组合;先 os.makedirs("backup/db", exist_ok=True) 和 os.makedirs("backup/sql", exist_ok=True) 把家安好;时间戳算一次存进变量,两个文件名共用,方便日后对照”哪份 .db 对应哪份 .sql”。
练习2:给 restore.py 加一道安全门。 恢复前先检查目标文件是不是已存在,存在就问一句”已存在,确定覆盖吗?(y/n)”,输 y 才继续,输别的都退出。
提示:os.path.exists + input();用户可能乱输——第10篇的错误处理在这等着,不是 y/n 的输入就当”不覆盖”处理,宁可保守。
练习3(坑点自考):先诊断,再对答案。 下面每段都有毛病,不看文章说出病在哪、怎么改:①备份脚本的输出目录就设在 shop.db 所在的文件夹里,文件名 backup.db;②restore.py 里 target = "shop.db",直接恢复覆盖,跑完发现恢复的是上个月的备份,这个月的数据没了;③备份脚本勤勤恳恳跑了仨月,今天要恢复时发现 backup/ 里只有一个文件,还是昨天的。
提示:①正主和备胎同居的下场;②顶替之前,旧库谁来保命、新恢复的要不要先验证?③想想文件名不带时间戳 + 每天覆盖 = 什么后果。
下篇预告
下一篇(第58篇):数据迁移 — CSV和数据库互相搬家。 别忘了你还有第25到36篇攒下的CSV家当,数据库这边又攒了一堆新数据,两边怎么互通?把CSV搬进数据库、把数据库倒成CSV——导出导入的功夫今天练了一半,下一篇补全另一半,让你的数据想住哪儿住哪儿。