Python sqlite3.blobopen:可以定位写字节,为什么写到末尾却不能自动扩容

前天 3阅读

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预留六个字节。

Python sqlite3.blobopen:可以定位写字节,为什么写到末尾却不能自动扩容

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。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。