Python sqlite3 怎样用 URI 参数打开只读连接
来源:17golang原创
时间:2026-10-20 01:53:10 159浏览 收藏
如果程序只是读取一份已经准备好的 SQLite 数据库,普通的 sqlite3.connect("data/app.db") 并不能表达“文件必须存在、连接不能写入”这两个要求。SQLite 默认可能创建缺失文件,排查配置错误时就容易把一个空库当成成功连接。
更明确的写法是使用 URI 参数:sqlite3.connect("file:data/app.db?mode=ro", uri=True)。其中 file: 表示 SQLite URI,mode=ro 表示只读打开,uri=True 告诉 Python 把第一个参数按 URI 解释。文件不存在时连接应直接失败,连接建立后执行写操作也会被拒绝。
官方文档:https://docs.python.org/3/library/sqlite3.html
uri=True不是只读开关;没有它,mode=ro只是普通字符串的一部分。mode=ro要求目标数据库已经存在,不会因为路径写错而悄悄创建新库。- 只读连接仍然可以执行
SELECT;真正的INSERT、UPDATE或 DDL 会在写入阶段失败。
先把只读连接拆成三个条件
这个问题常常不是 Python 语法错了,而是三个层次混在了一起。file: 是 SQLite URI 的方案前缀,负责让后面的路径和查询参数组成一个 URI;mode=ro 是 SQLite 的打开模式,负责要求只读;uri=True 是 Python sqlite3.connect() 的参数,负责启用 URI 解析。
因此,下面两种写法的含义不同:
import sqlite3
# 这里明确启用 URI 解析;mode=ro 才会被 SQLite 当作只读参数。
conn = sqlite3.connect("file:data/app.db?mode=ro", uri=True)
conn.close() # 连接用完后主动释放文件句柄。
import sqlite3
# 没有 uri=True 时,不要把整个字符串误认为已经启用了只读模式。
conn = sqlite3.connect("file:data/app.db?mode=ro")
conn.close() # 这不是本文要求的 URI 只读连接。

用 mode=ro 打开已经存在的数据库
最小可用代码可以先固定一个相对路径:
from pathlib import Path
import sqlite3
db_uri = "file:data/app.db?mode=ro"
conn = None
try:
# uri=True 让 SQLite 读取 mode=ro,而不是把它当成普通文件名。
conn = sqlite3.connect(db_uri, uri=True)
rows = conn.execute("SELECT id, name FROM users ORDER BY id").fetchall()
for row in rows:
print(row)
except sqlite3.OperationalError as exc:
# 连接阶段或查询阶段的 SQLite 错误都保留原始信息,便于定位路径和权限问题。
raise RuntimeError(f"只读数据库访问失败: {exc}") from exc
finally:
if conn is not None:
conn.close() # 只读不等于不占用句柄,连接仍然需要关闭。
示例中的 Path 只是提醒路径应由配置决定;真正传给 connect() 的值仍是带 file: 前缀和查询串的 URI。读取失败时保留原始 OperationalError,比只输出“数据库不可用”更容易区分文件路径、权限和 SQL 问题。
文件不存在时,为什么应该在连接阶段失败
普通路径连接的一个常见误区是:程序启动后看到一个新的 app.db,就以为部署成功。实际上,路径拼写错误、挂载目录没有准备好、工作目录改变,都可能让程序打开了一个刚创建的空文件。
mode=ro 把这个边界前移到连接阶段。目标不存在时,SQLite 不应创建数据库,而是返回打开失败的 OperationalError。这正是只读数据目录适合使用 URI 的原因:缺文件会暴露出来,而不会被“自动初始化”掩盖。
import sqlite3
def open_existing_readonly(path: str) -> sqlite3.Connection:
# 只读连接要求部署先准备好数据库文件;不会替缺失文件创建空库。
uri = f"file:{path}?mode=ro"
return sqlite3.connect(uri, uri=True)
try:
conn = open_existing_readonly("data/app.db")
except sqlite3.OperationalError as exc:
# 这里优先检查工作目录、挂载路径和文件权限,而不是先重试查询。
raise RuntimeError(f"无法打开既有 SQLite 文件: {exc}") from exc
else:
conn.close() # 成功建立连接后也要释放资源。
这段代码适合表达“文件必须由部署步骤准备好”的约束。若路径来自用户输入或包含 ?、# 等 URI 保留字符,不能简单拼接字符串,应先按照 SQLite URI 规则对路径部分进行编码,并把查询参数放在编码后的路径之后。
写入被拒绝,不代表读取失败
只读连接的另一个排查现场是:SELECT 正常,但第一次写入时报 attempt to write a readonly database 一类错误。这通常说明 URI 已经按预期生效,问题不在连接建立,而在代码越过了只读边界。
import sqlite3
conn = sqlite3.connect("file:data/app.db?mode=ro", uri=True)
try:
# 查询属于只读连接允许的操作。
row = conn.execute("SELECT count(*) FROM users").fetchone()
print("当前记录数:", row[0])
# 这个写操作应被 SQLite 拒绝,用来暴露误写路径。
conn.execute("INSERT INTO users(name) VALUES (?)", ("demo",))
except sqlite3.OperationalError as exc:
# 生产代码应记录 SQL 场景,但不要吞掉只读约束错误。
raise RuntimeError(f"只读连接拒绝写入: {exc}") from exc
finally:
conn.close() # 无论查询还是写入失败,都关闭连接。
这里不要把只读 URI 改成普通路径来“修复”错误。先确认调用方是否本来就应该写入;如果需要写入,应把写库职责放到拥有写权限的连接或服务中,而不是让一个只读数据源偷偷承担写操作。
相对路径、绝对路径和工作目录要分开判断
URI 只读模式能约束打开方式,但不能替你决定相对路径从哪里开始解析。file:data/app.db?mode=ro 依赖当前工作目录;同一个服务在本地启动、进程管理器启动和容器启动时,工作目录可能不同。
排查时可把路径解析和 URI 拼装分开记录:
from pathlib import Path
from urllib.parse import quote
import sqlite3
db_path = Path("data/app.db").resolve()
# quote 只处理路径部分,保留斜杠,避免特殊字符被误当成 URI 查询语法。
encoded_path = quote(str(db_path), safe="/:\\")
db_uri = f"file:{encoded_path}?mode=ro"
conn = sqlite3.connect(db_uri, uri=True)
try:
conn.execute("SELECT 1").fetchone()
finally:
conn.close() # 只做探测也要及时关闭连接。
这个版本适合配置系统已经给出绝对路径的场景。Windows 路径、网络挂载和包含特殊字符的路径应在目标运行环境中按 SQLite URI 文档单独确认;不要因为本机相对路径可用,就假设服务部署目录也一致。

上线前按这张清单复查
| 检查点 | 应该看到的结果 | 异常时先看哪里 |
|---|---|---|
| URI 形式 | 以 file: 开头,并带 mode=ro | 配置拼接和路径编码 |
| Python 参数 | sqlite3.connect(..., uri=True) | 是否漏传 uri=True |
| 文件存在性 | 缺失文件在连接阶段失败 | 工作目录、挂载点、部署产物 |
| 查询行为 | SELECT 可以执行 | 文件权限、数据库损坏、SQL |
| 写入行为 | 写操作被明确拒绝 | 调用方是否误用了只读连接 |
| 资源释放 | 所有分支最终调用 close() | 异常处理和连接生命周期 |
归纳起来,Python sqlite3 的 URI 只读连接写法就是 file:数据库路径?mode=ro 配合 uri=True。先用它保证“文件必须存在”,再用查询成功和写入拒绝分别确认读取路径与只读边界,最后把绝对路径解析和连接关闭纳入部署代码。
常见问题
只传 mode=ro,不传 uri=True 可以吗?
不建议这样做。uri=True 是 Python 端启用 URI 解析的开关;缺少它时,查询串不会按本文需要的 SQLite URI 参数处理。
只读连接能执行事务吗?
可以执行不改变数据的查询,但事务语义不能把写入权限变出来。是否显式开启事务应服从应用的查询模式,写操作仍会被只读数据库拒绝。
文件不存在时想自动创建怎么办?
那是初始化或写入职责,不应和只读连接混在一起。先由部署或初始化流程创建并准备数据库,再用 mode=ro 提供读取连接,能更早暴露环境缺失。
-
421 收藏
-
341 收藏
-
333 收藏
-
220 收藏
-
394 收藏
-
225 收藏
-
380 收藏
-
136 收藏
-
335 收藏
-
278 收藏
-
330 收藏
-
417 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习