Python 备份 SQLite:为什么复制了 .db,恢复后还是少数据?

备份文件已经生成,打开也没报错,最近新增的记录却不见了。

如果你的 Python 小工具、桌面应用或个人项目用了 SQLite,先看看备份脚本是不是只有一行文件复制。数据库仍在运行时,尤其开启 WAL 模式后,这一步可能没有拿到完整的数据状态。

更容易误判的是:这种副本甚至可能通过数据库结构检查。

1. 已经提交的数据,不一定都在 .db 里

WAL 是 SQLite 的一种日志模式。发生修改时,新内容可以先写入 notes.db-wal;后续通过 checkpoint,也就是检查点操作,再写回 notes.db 主文件。

因此,程序执行了 commit(),不意味着相关内容已经全部写回主文件。SQLite 官方也明确提醒:将数据库与对应 WAL 分离,可能丢失已提交的事务,甚至造成损坏。SQLite WAL 文档

为演示这个问题,配套实验先写入第一条记录并完成 checkpoint,再提交第二条记录,让它留在 WAL 中,保持数据库连接打开。实际运行结果如下:

检查对象 查询到的记录数 quick_check 结果
源数据库 2 ok
只复制 .db 的副本 1 ok
使用 backup() 的副本 2 ok

这里故意控制了写入顺序,方便复现;不是说每次复制都会刚好少一条。

也别把处理方式改成"依次复制 .db-wal-shm"。应用还在写入时,几个文件的复制时点仍可能不同。对于运行中的数据库,优先使用 SQLite 提供的备份接口。

2. 用 Python 标准库完成备份

Python 的 sqlite3.Connection.backup() 支持在数据库被其他客户端访问时创建备份,不需要额外安装第三方包。Python sqlite3 文档

下面是一份完整脚本,适用于 Python 3.10 及以上。保存为 backup_sqlite.py

复制代码
python 复制代码
import argparse
import sqlite3
import tempfile
import time
from contextlib import closing
from datetime import datetime, timezone
from pathlib import Path


def backup_sqlite(source, output_dir):
    source = Path(source).resolve(strict=True)
    if not source.is_file() or source.stat().st_size == 0:
        raise ValueError("源文件必须是已有的非空 SQLite 数据库")

    output_dir = Path(output_dir).resolve()
    output_dir.mkdir(parents=True, exist_ok=True)
    prefix = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ-")
    folder = Path(tempfile.mkdtemp(prefix=prefix, dir=output_dir))
    partial = folder / "backup.partial"
    deadline = time.monotonic() + 60

    def progress(status, remaining, total):
        if status != sqlite3.SQLITE_DONE and time.monotonic() > deadline:
            raise TimeoutError("备份等待过久,请在低峰期重试")

    # mode=ro 避免路径写错时自动新建空源库。
    with closing(sqlite3.connect(source.as_uri() + "?mode=ro", uri=True)) as src:
        with closing(sqlite3.connect(partial)) as dst:
            src.backup(dst, pages=256, progress=progress, sleep=0.1)
            # 只调整备份副本,便于作为独立文件保存。
            mode = dst.execute("PRAGMA journal_mode=DELETE").fetchone()[0]
            if mode != "delete":
                raise RuntimeError("无法将备份副本切换为独立文件模式")

    # 关闭后重新打开副本,检查实际写出的文件。
    with closing(sqlite3.connect(partial.as_uri() + "?mode=ro", uri=True)) as check:
        if check.execute("PRAGMA quick_check").fetchall() != [("ok",)]:
            raise RuntimeError("副本结构检查失败")

    result = folder / "backup.db"
    partial.rename(result)
    return result


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="备份 SQLite,不覆盖已有备份")
    parser.add_argument("source", help="源数据库路径")
    parser.add_argument("output_dir", help="备份保存目录")
    args = parser.parse_args()
    try:
        result = backup_sqlite(args.source, args.output_dir)
    except (OSError, sqlite3.Error, ValueError, RuntimeError) as exc:
        parser.exit(1, f"备份失败:{exc}\n")
    print(f"备份完成:{result}")

在 Windows PowerShell 中,切换到脚本所在目录,再执行;把源路径换成你已有的数据库:

复制代码
复制代码
py .\backup_sqlite.py ".\data\notes.db" ".\backups"

没有 py 命令、但已经配置好 Python 时,可以把它换成 python。macOS/Linux 使用:

复制代码
复制代码
python3 ./backup_sqlite.py ./data/notes.db ./backups

成功后,终端会显示实际副本路径。每次都创建带 UTC 时间和随机后缀的新目录,旧备份保留。

这段代码还有两个容易忽略的细节:源库按 mode=ro 打开;副本先使用 .partial 文件名,关闭连接、重新打开并完成检查后,才改名为 backup.db。失败留下的 .partial 文件不能当成有效备份。

with sqlite3.connect(...) 本身不负责关闭连接,所以这里使用 contextlib.closing(),避免文件仍被占用时就尝试改名。Python 连接上下文管理说明

3. 为什么检查结果是 ok,仍然要看业务数据?

前面的实验已经给出了答案:旧副本可以在结构上完全正常,同时缺少最新记录。

quick_check 做的是结构检查,而且没有覆盖唯一约束及索引内容与表内容的一致性。更完整的结构检查可以使用 integrity_check;外键还需另外检查。它们都不能替你判断"某笔订单或某条笔记本来应该存在"。SQLite PRAGMA 文档

我更建议定期把副本放进隔离的测试环境:打开关键页面,核对几条已知记录,再试一次常用查询。涉及近期数据,要明确这份备份对应的时间范围,不能拿备份后新增的记录要求它补齐。

配套代码里的 demo_wal.py 可以复现前面的表格:

复制代码
py .\demo_wal.py

它只在临时目录创建模拟数据库,结束后自动清理,不读取你的真实业务库。demo_wal.pybackup_sqlite.py 需要放在同一目录。

4. 这份脚本适合用到哪里?

适合本机磁盘上的普通 SQLite 数据库,例如个人项目、小工具和桌面应用的数据文件。它备份的是当前连接的主数据库;应用附件、配置文件,以及额外附加的数据库需要分别规划。

在线备份也有成本。其他连接持续高频写入时,复制过程可能重启,严重时长时间无法结束。上面的脚本会在复制进度回调中检查 60 秒阈值;这是分步检查,不是覆盖磁盘阻塞和后续校验的整个脚本硬超时。正式安排定时任务时,还要配置任务超时和失败告警。SQLite 在线备份说明

源库使用 WAL 且权限受限时,还需要能够读取相关文件,或具备 SQLite 正常创建辅助文件所需的目录权限。遇到打不开的问题,应检查权限,不要删除 WAL,也不要把仍在变化的数据库标成 immutable 来强行读取。

另外,同一块硬盘上的备份只能应对部分误操作,防不了硬盘损坏。确认副本可用后,再保存到另一存储位置,并妥善控制访问权限。

我会把"备份完成"的判断放在最后:拿到一致的数据库副本,再确认它能恢复需要的数据。 文件复制进度走到 100%,只能证明复制操作结束了。

相关推荐
李航19831 小时前
用 DeepDraw 几何引擎开发建筑设计软件(六):创建画线工具
python·软件构建
AiNightVision1 小时前
AI-ISP微光全彩夜视技术深度解析:如何在0.001Lux下实现全彩成像
人工智能·计算机视觉·车载系统·自动驾驶·无人机·智能家居·智能硬件
沐言人生1 小时前
1.3k星!开源「AI健康数据引擎」,把体检报告、智能穿戴设备和基因数据翻译成同一种语言
人工智能
明志数科1 小时前
机器人商业化验证中 真工业场景数据与训练场数据的差异分析
人工智能·机器学习
Mr数据杨1 小时前
公寓价格预测实战案例 从 Kaggle 房价回归到可落地估值建模
人工智能·数据分析·kaggle竞赛
VIP_CQCRE1 小时前
Cursor 接入 Ace Data Cloud MCP:把 AI 编程编辑器升级成全能创作工作台
ai·开发工具·cursor·mcp·ace data cloud
Mark_ZP1 小时前
【AI】RRF(倒数排名融合)说明
ai
狗都不学爬虫_1 小时前
AI逆向 - 天御无感点击验证(补+纯)
爬虫·python·网络爬虫
人工智能AI技术1 小时前
基于定时器的手搓任务调度器:树莓派Pico实战
人工智能