Python sqlite3.blobopen:可以定位写字节,为什么写到末尾却不能自动扩容
sqlite3.blobopen返回的对象支持read、write和seek,很容易被当成会自动长大的普通文件。实际使用时,在末尾追加一个字节却得到ValueError,原因是这个句柄只能访问已有BLOB的固定长度。定位能力和扩容能力需要分开看。
下面在Linux、CPython 3.12.14、SQLite 3.53.1实跑,要求Python 3.12或更新版本以使用这里的autocommit参数;blobopen接口本身从Python 3.11加入。保存为demo.py,运行python3 demo.py。数据库只在内存中,先用zeroblob预留六个字节。
AI模型生成概念插图:固定尺寸托盘可以替换内部位置,外侧部件不能把边框自动撑大;只表现固定容量,不是数据库截图。
完整程序与本地结果
完整可运行程序
import sqlite3
db = sqlite3.connect(":memory:", autocommit=False)
try:
db.execute("CREATE TABLE packet(id INTEGER PRIMARY KEY, data BLOB)")
db.execute("INSERT INTO packet VALUES(1, zeroblob(6))")
with db.blobopen("packet", "data", 1) as blob:
blob.write(b"AB")
blob[-1] = ord("!")
assert len(blob) == 6
print("initial capacity:", len(blob))
blob.seek(5)
try:
blob.write(b"XY")
except ValueError:
print("overrun:", "ValueError")
else:
raise AssertionError("blob write must not extend the value")
assert len(blob) == 6
blob.seek(0)
saved = blob.read()
assert saved == b"AB\x00\x00\x00!"
print("bytes after rejected write:", saved.hex())
db.commit()
assert db.execute("SELECT length(data) FROM packet").fetchone() == (6,)
with db.blobopen("packet", "data", 1, readonly=True) as reader:
assert reader.read() == saved
print("reopened size:", len(reader))
db.execute("UPDATE packet SET data=zeroblob(8) WHERE id=1")
with db.blobopen("packet", "data", 1) as replacement:
assert replacement.read() == bytes(8)
print("replacement size:", len(replacement))
print("old payload preserved:", False)
db.commit()
finally:
db.close()本次实际输出(以下为结果,不是程序)
initial capacity: 6 overrun: ValueError bytes after rejected write: 414200000021 reopened size: 6 replacement size: 8 old payload preserved: False
写入改变内容,不改变既有长度
前两字节写入AB,最后一字节改成感叹号,长度仍然是6。下标-1修改末字节,而不是向后追加。接着seek到位置5,只剩一个字节空间,却尝试写两个字节XY,因此得到ValueError。
重新读回的十六进制是414200000021,对应AB、三个零字节和感叹号。断言同时检查内容与长度,证明这次越界写入没有把半个XY留下,也没有扩张字段。只记录“有异常”不够,失败后已有内容是否符合约定同样值得验证。
关闭句柄后再提交与重新打开
程序退出Blob的with后提交,再以readonly=True打开同一行验证内容,容量仍为6。Blob句柄有自己的关闭时机,数据库连接也有自己的事务与关闭责任。这里明确先结束写入句柄,再完成提交,不让这两层生命周期混在一起。
blobopen第三个位置参数选的是行的rowid,本例用INTEGER PRIMARY KEY建立清楚对应。不要把业务文本编号直接当这个参数,也不能假定所有表结构都支持同一路径;WITHOUT ROWID表不适用该接口。示例没有在旧句柄仍开着时修改它指向的整行。
改变容量,需要更换存储值并重新取得句柄
最后用UPDATE把整列替换为zeroblob(8),再打开新句柄,长度才变成8。旧内容并没有自动迁移,读回的是八个零字节,所以输出old payload preserved为False。这一步是替换数据,不是原有六字节缓冲的无损扩容。
真实流程若需要保留旧内容,应先计算新长度、安排数据迁移与事务边界,再验证最终字节。预留容量也不等于有效负载长度,零字节可能本来就是合法内容,不能靠扫描尾部零值猜测实际长度。为大对象设计分段写入时,应另外存储明确长度和格式信息,并对每段的偏移与剩余空间做检查。
参考资料
资料核验日期:2026年10月2日。以上输出对应文中固定输入和明确运行版本,本地执行退出码为0。


