This release is a pre-release and may not be stable for production use.
SFS v2:面向 Python 爬虫的 SQL 文件系统
sfs-v2 提供虚拟目录与文件、批量读写、分页查询、内容扫描和压缩存储。Python SDK 通过随 wheel 附带的 Rust 原生库工作:本地可直接使用 SQLite;多个爬虫协作时,可以连接多个 SFS 服务实例,由服务实例共享一个 PostgreSQL 数据库。虚拟路径(如 /pages/a.html)不是操作系统文件路径。
当前为预发布版本。 Python 是唯一活跃维护的 SDK;Kotlin/Node.js 原型未纳入本次发布。SQLite 是当前经过 Python 集成验收的入口;PostgreSQL、MySQL 直连和 gRPC 远程 Python 入口均位于
sfs_v2.experimental,其接口与行为仍可能变化。
安装与支持范围
python -m pip install "sfs-v2==0.0.1.dev2"
python -c "import sfs_v2; print(sfs_v2.__version__)"
| 平台 | 本次 PyPI wheel | 说明 |
|---|---|---|
| Windows x86_64 | win_amd64 |
原生库为 sfs_ffi.dll |
| Linux x86_64 | manylinux_2_28_x86_64 |
glibc ≥ 2.28,原生库为 libsfs_ffi.so |
| macOS、ARM64、Alpine/musl | 未提供 | pip 无匹配 wheel;不要安装其他平台的 wheel |
要求 Python ≥ 3.8。分发名是 sfs-v2,Python 导入名是 sfs_v2;直接 import sfs-v2 不符合 Python 语法。PyPI 上的 sfs 是另一个项目,本版不提供 sfs 导入别名,因此可避免与其模块名冲突。此前 0.0.1.dev1 使用过 import sfs,升级后须改用 import sfs_v2,建议在虚拟环境中升级并删除不需要的旧安装。wheel 内只包含对应平台的原生库,不需要另行安装 Rust;本包不包含 sfs 服务端/命令行程序。
三分钟上手:本地 SQLite
以下代码可直接运行,数据库目录会由 SFS 使用;文件地址以 / 为根,文件内容可以是任意字节。
from tempfile import TemporaryDirectory
from sfs_v2 import OpenOptions, Sfs, WriteItem
with TemporaryDirectory() as database_dir:
with Sfs.open_sqlite(database_dir) as store:
store.makedirs("/pages/2026")
store.write_text("/pages/2026/a.html", "<h1>Hello</h1>")
store.write_many([
WriteItem("/pages/2026/b.html", b"<p>B</p>"),
WriteItem("/pages/2026/c.bin", b"\x00\xff"),
])
assert store.read_text("/pages/2026/a.html") == "<h1>Hello</h1>"
for result in store.read_many(["/pages/2026/b.html", "/pages/2026/c.bin"]):
print(result.path, result.data) # ReadResult(path, data),不是 bytes 列表
print([item.full_path for item in store.list_files("/pages/2026")])
# 写入句柄关闭后,现有字典若被使用,只读句柄仍能正确解码。
with Sfs.open_sqlite(database_dir, options=OpenOptions(readonly=True)) as reader:
assert reader.read("/pages/2026/c.bin") == b"\x00\xff"
Sfs.open_sqlite(directory, options=OpenOptions(...)) 接受数据库目录,不是 .db 文件。OpenOptions 目前公开 readonly=False 和 compression_level=19(可设置 1–22);例如写入密集、希望降低 CPU 消耗时可尝试 OpenOptions(compression_level=3),再用真实数据评估。只读打开不会训练字典或写入数据库。请使用 with / close() 及时关闭句柄,close() 可重复调用;关闭后继续操作会抛出 SfsClosedError。
常用文件操作
同步 Sfs 方法 |
用途 |
|---|---|
mkdir(path) / makedirs(path) |
建立目录 / 递归建立目录 |
write(path, bytes) / write_text(path, str) |
写入二进制 / UTF-8 文本 |
write_many([WriteItem(path, data), ...]) |
一批写入;正常返回后才是该批写入成功 ACK |
read(path) / read_text(path) |
读取二进制 / UTF-8 文本;整段内容会进入内存 |
read_many([path, ...]) |
返回 ReadResult(path, data) 列表 |
stat(path) / exists_file(path) / exists_dir(path) |
元数据及存在性检查 |
list_files(path) / list_dirs(path) |
目录查询,返回 FileInfo 列表 |
iter_files(path) / iter_dirs(path) |
自动分页,适合大目录 |
dir_info(path) / fs_info() |
目录或全库文件与目录数量 |
rename(old, new) / remove(path, recursive=False) |
重命名 / 删除;递归删除须显式指定 |
scan(options) |
按条件遍历文件元数据和内容,须关闭扫描句柄 |
maintenance.shrink() |
回收无引用的存储空间,属于维护操作 |
FileInfo 包含 full_path、size、is_file、created_at、updated_at 等字段;目录的 size 为 None。单次列表默认最多 1000 条:
from sfs_v2 import CursorPage, OffsetPage
page = store.list_files("/pages", page=OffsetPage(limit=100, offset=0))
next_page = store.list_files("/pages", page=CursorPage(limit=100, start_after=page[-1].full_path)) if page else []
for info in store.iter_files("/pages", page_size=1000):
print(info.full_path, info.size)
OffsetPage / CursorPage 的 limit 范围为 1–10000;列表/迭代器只返回该目录的直接子项,大量结果优先使用 iter_files,不要一次取完。以上 store 指已打开的 Sfs 句柄。
带过滤条件的扫描
扫描返回 ScannedFile(info, data);与只列元数据不同,每条结果还带有文件内容。ScanFilter 可组合路径前缀、无前导点的扩展名、名称和文件大小/时间条件。时间过滤要求带时区的 datetime。
from sfs_v2 import ScanErrorMode, ScanFilter, ScanOptions, ScanOrder
options = ScanOptions(
filter=ScanFilter(path_prefix="/pages", extensions=("html",)),
order=ScanOrder.PATH,
batch_size=256,
error_mode=ScanErrorMode.COLLECT,
)
with store.scan(options) as scan:
for item in scan:
print(item.info.full_path, len(item.data))
print(scan.stats) # ScanStats(scanned, errors, skipped)
print(scan.take_errors()) # COLLECT 模式收集的 ScanError 列表
默认 error_mode=ABORT,遇错立即报错,避免悄悄漏掉数据;SKIP 会跳过,COLLECT 会继续并保留有界错误信息(最多约 10000 条或 8 MiB 错误文本)。扫描单文件内容上限为 4 MiB;超过上限时按所选错误模式处理。若确需读取较大的单个文件,可在普通 read(path) 所受的内存和后端限制内单独读取;当前 Python SDK 没有公开的分块流式读写 API,不要把底层 Rust/C 流的上限当成 Python 能力。SfsScan.stats / take_errors() 须在其同步 context manager 退出前调用。
asyncio 爬虫写入
AsyncSfs 为阻塞的原生调用使用专用单线程 executor,并为并发写入配置有界队列和单 writer 批量提交;不会让耗时 ctypes 操作直接阻塞事件循环。
import asyncio
from tempfile import TemporaryDirectory
from sfs_v2 import AsyncOptions, AsyncSfs
async def main():
with TemporaryDirectory() as database_dir:
store = await AsyncSfs.open_sqlite(
database_dir,
async_options=AsyncOptions(
queue_capacity=256,
max_queued_bytes=256 * 1024 * 1024,
write_batch_size=100,
flush_interval=0.05,
),
)
async with store:
await store.makedirs("/crawl")
await asyncio.gather(*(
store.write_text("/crawl/page-%d.html" % i, "<p>%d</p>" % i)
for i in range(10)
))
print(await store.read_text("/crawl/page-0.html"))
asyncio.run(main())
AsyncOptions 上述数值即默认值;queue_capacity 限制待写任务数,max_queued_bytes 限制待写字节数。await write(...) 或 await write_many(...) 正常返回,才表示底层写操作成功提交;排进队列不算 ACK,同批失败会通知等待该批的调用方。async with / await aclose() 会停止接纳新写入、排空已有请求、等待在途原生调用后再关闭。取消任务、超时、连接断开或 ACK 丢失,不保证数据没有写入;需要业务侧用稳定路径、幂等写入或额外校验处理结果不明的请求,不能无条件重放。
异步读、目录操作和维护方法与同步 API 对应,需 await;大目录可用 async for info in store.iter_files("/pages")。异步扫描是 store.scan(options) 返回的 AsyncSfsScan,scan() 本身不需要 await。以下代码放在上例的 async def main()、async with store: 内:
from sfs_v2 import ScanFilter, ScanOptions
scan = store.scan(ScanOptions(filter=ScanFilter(extensions=("html",))))
async with scan:
async for item in scan:
print(item.info.full_path)
print(await scan.stats())
print(await scan.take_errors())
上例在 async def 内运行;AsyncSfsScan 关闭后仍可读取最终统计和收集的错误,与同步扫描的 stats 属性用法不同。不提供 scan.to_list();扫描大量内容时应逐条处理。
多爬虫部署:SFS 服务与 PostgreSQL(实验性 Python 远程入口)
推荐拓扑是 Python 爬虫 →(HTTPS/gRPC)→ 一个或多个 SFS 服务实例 →(PostgreSQL TLS)→ 同一个 PostgreSQL 数据库。Python 爬虫只需 SFS 地址与 bearer token,不要把数据库 DSN/凭据下发给每个爬虫,也不要让爬虫直连 PostgreSQL。每个 SFS 实例使用自己的 HTTP/gRPC 监听端口,数据库名保持相同;跨机器还须规划连接数、备份及故障恢复。
pip install sfs-v2 不会安装 sfs serve。下面命令必须在已单独部署 SFS CLI 的服务机器上执行;CLI 的 --port 是 Web/REST,--rpc-port 才是 Python connect_remote 使用的 gRPC 端口。
# 本机开发示例(仅回环地址允许明文;CLI 单独安装):
sfs serve --backend sqlite --db ./sfs-data --host 127.0.0.1 --port 51237 --rpc-port 51337
from sfs_v2.experimental import connect_remote
with connect_remote("http://127.0.0.1:51337") as remote:
remote.makedirs("/crawl")
remote.write("/crawl/example.html", b"<html>stored by SFS</html>")
assert remote.read("/crawl/example.html") == b"<html>stored by SFS</html>"
生产跨主机部署示意(不包含真实凭据):
# 在每台 SFS 机器上由密钥管理器注入:
# SFS_CONN_STR='host=pg.internal.example port=5432 dbname=sfs user=... password=... sslmode=require'
# SFS_AUTH_TOKEN='<随机且足够长的持久 token>'
: "${SFS_CONN_STR:?必须注入 PG 连接串,包含 sslmode=require}"
: "${SFS_AUTH_TOKEN:?必须注入 SFS bearer token}"
export SFS_PG_TLS_CA_FILE=/etc/sfs/pg-ca.pem # 私有 CA;公有 CA 可使用系统信任根
sfs serve --backend postgres --host 0.0.0.0 --port 51237 --rpc-port 51337 \
--tls-cert /etc/sfs/server-chain.pem --tls-key /etc/sfs/server-key.pem
# 第二实例使用同一数据库和认证配置;如在同一主机,另选两个未占用的监听端口。
import os
from sfs_v2.experimental import connect_remote
# 证书必须由爬虫机器信任,且其主机名与 sfs.example.internal 匹配。
with connect_remote(
"https://sfs.example.internal:51337",
token=os.environ["SFS_AUTH_TOKEN"],
) as remote:
remote.write("/crawl/example.html", b"<p>saved</p>")
爬虫需异步 gRPC 时,可在 async def 中使用 async with await sfs_v2.experimental.connect_remote_async(endpoint, token=token) as remote:;其连接及读写都通过专用 executor,不在事件循环执行阻塞 FFI。普通 gRPC 请求默认有 120 秒客户端截止时间,爬虫进程可用 SFS_REMOTE_REQUEST_TIMEOUT_SECS=1..600 配置。超时后的写入状态可能未知,不能据此盲目重试。
两条 TLS 链路需分别配置:
- 爬虫 → SFS:非 loopback 监听必须同时提供有效 bearer token 与 SFS 的 TLS 证书/密钥。Web/REST 与 gRPC 可共用证书,但端口不同;即使
--rpc-port 0仅开放 HTTP,非 loopback 仍需 HTTPS + token。Pythonconnect_remote("https://...", token=...)验证系统信任的 CA 和主机名;当前公开 Python 接口没有私有/自签 CA 参数,不要关闭证书校验。token传不带Bearer前缀的原始值;SDK 会构造认证头,不要打印它。 - SFS → PostgreSQL:在 SFS 机器的 PG DSN 中明确写
sslmode=require,以强制 TLS 并验证 CA 与服务器主机名;私有 CA 可通过该 SFS 进程的SFS_PG_TLS_CA_FILE指向绝对 PEM 路径。此模式比 libpq 的普通require更严格。未指定sslmode或prefer不会自动升级成 TLS,远端地址会被拒;除可信隧道中的明确例外,不使用sslmode=disable。轮换 CA 后需重启 SFS 实例,使业务连接与字典连接的信任配置一致。不要把 PG CA/密码误当成 Python 爬虫的连接参数。
远程 Python 入口依然是实验性:目前没有公开的按调用设置 deadline、私有 CA 或文件分块流接口;服务端旧版本不支持 ScanV2 时,扫描会明确报 SfsUnsupportedError,不会退回到丢失错误/统计的旧扫描协议。普通远程 read / write 是整段消息,受 300 MiB gRPC unary 消息上限、解压保护以及 Python 内存/异步队列预算等更严格限制;这不是承诺单个 Python 文件恰可达到 300 MiB。
其他实验性 SQL 入口
from sfs_v2.experimental import connect_postgres, connect_mysql 可以在 Python 中直接连接 SQL 后端,但尚未完成稳定 Python 集成矩阵;多爬虫推荐上述 SFS 服务拓扑,不建议把 PG/MySQL 密钥分发给所有客户端。PostgreSQL 的服务端同时支持 REST 与 gRPC;MySQL 当前服务模式仅支持 HTTP,不要将 MySQL 服务实例配置为 Python gRPC 后端。SQL 连接协议/安全设置不同,不能把 PostgreSQL 的 TLS 参数照搬到 MySQL URL。
字典压缩、持久化与备份
可写的 Python SQLite 打开默认启用 zstd 字典训练;可写的 PostgreSQL SFS 服务也会挂载数据库字典管理器。训练默认开启不等于从第一个文件起就使用字典:通常累计约 5000 次成功写入后才会后台评估;只有压缩收益超过 5% 才启用候选字典,否则仍采用常规压缩。OpenOptions 当前不公开设置训练间隔/开关的 Python 参数。
- SQLite:字典元数据位于数据库目录中的
dict.meta.db,字典文件位于dicts/。只读连接会挂载已有字典用于解码,但不会训练。备份/迁移时应保证数据库文件、卷文件与字典元数据取自同一一致性快照;不能仅复制一个主库文件。 - PostgreSQL:字典版本和备份保存在同一个 PG 数据库的
DictVersions/DictVersionsBackup等表中,多个 SFS 实例共享,不依赖每台服务机器的dict.meta.db。只读实例不训练,但必须能加载所需版本;缺失或损坏时会报错,不会把压缩字节当原文返回。
写入 API 的成功返回意味着底层操作已提交;SQLite 采用 WAL、synchronous=FULL,成功 ACK 才可视作持久化写入完成。PG 事务会设置 synchronous_commit=on,但实际落盘仍要求 PG 服务端正确启用 fsync;异步复制或故障切换不自动保证零数据丢失。write_many 用于批量写;多个独立 write 不是一个跨请求的大事务。发生超时或连接中断时,ACK 未到不等于事务未成功。生产环境仍需演练备份恢复与故障切换,不要只依赖单份数据库或字典文件。
错误与维护
所有 SFS 运行时错误派生自 SfsError,保留 code、operation、path、database_code 和 retryable;常见子类有 SfsNotFoundError、SfsReadonlyError、SfsCorruptDataError、SfsDictionaryError、SfsDatabaseLockedError 与 SfsResourceExhaustedError。
from sfs_v2 import SfsError, SfsNotFoundError
try:
data = store.read("/missing.html")
except SfsNotFoundError:
data = None
except SfsError as exc:
print(exc.code, exc.operation, exc.path, exc.database_code, exc.retryable)
raise
retryable 只标记部分明确可安全重试的情况(如已被 PG 中止的事务);不要把所有网络异常/提交失败都当成未写入。需要回收无引用数据时,可在合适的维护窗口执行 report = store.maintenance.shrink(),查看 report.succeeded 和 report.vacuum_failed_volumes;异步版本为 await store.maintenance.shrink()。这不是日常每次写入后需要调用的方法。
| 现象 | 检查 |
|---|---|
No matching distribution |
Python/CPU 架构及 glibc 是否符合上方 wheel 平台表;目前没有 macOS/ARM64 wheel |
| 无法加载原生库或 ABI 不匹配 | 确认安装的是 sfs-v2,清除开发时残留的 SFS_NATIVE_PATH,避免混用不同版本原生库 |
| 只读时字典缺失或损坏 | 检查完整数据库、卷与字典是否来自一致性备份,不要忽略报错 |
| 远程 TLS/认证失败 | 检查 SFS 证书主机名和 CA、服务端 token、是否连到 gRPC 端口;PG TLS 是另一条链路 |
| 远程写超时 | 结果不确定;先根据业务键检查已写入状态,再决定是否幂等补偿 |
源码开发与验收
仅从源代码开发 Python 绑定时,需要先在目标平台编译 sfs-ffi;SFS_NATIVE_PATH 用于指向开发产物,会覆盖 wheel 自带的原生库,因此不要在普通 PyPI 用户环境中设置它。
cargo build --release -p sfs-ffi
# 在 Python SDK 的源码目录运行本地测试(需安装 pytest,并让原生库可加载):
python -m pytest test_open_options.py test_sfs.py test_async_sfs.py -v
- Windows 开发产物:
target/release/sfs_ffi.dll;Linux:target/release/libsfs_ffi.so。 - 公开 Python 契约通过原生库 ABI v2 握手;发现 Python 与原生库不匹配时会明确拒绝加载。
- Python 包版本
0.0.1.dev2与底层 Rust crate 的内部版本号不必相同;请以已安装的 Python 包版本和 ABI 握手为准。
项目发布元数据标注许可证为 MIT。当前 sfs-v2 是预发布版,部署前请在自己的爬虫负载和目标数据库上完成备份、恢复、并发与故障测试。
Release files for sfs-v2 0.0.1.dev2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sfs_v2-0.0.1.dev2-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| sfs_v2-0.0.1.dev2-py3-none-manylinux_2_28_x86_64.whl | Python 3 | none | Linux glibc 2.28+ x86-64 | Details |
Total release size: 10.1 MB
Release files / sfs_v2-0.0.1.dev2-py3-none-win_amd64.whl
| Download URL | sfs_v2-0.0.1.dev2-py3-none-win_amd64.whl |
|---|---|
| Size | 4.6 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
49191b8325d62163fbef185a3b235bc13ff088528fedd9414cec161e222e0b19
|
|
BLAKE2b-256 checksum How to use checksums |
de5470d333385839f6ecb84be409fce050b642e8f9d1a6239bc1b025ca98983e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Release files / sfs_v2-0.0.1.dev2-py3-none-manylinux_2_28_x86_64.whl
| Download URL | sfs_v2-0.0.1.dev2-py3-none-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 5.5 MB |
| Tags | Linux glibc 2.28+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
1c3659620c49094f57a7b63a7c836eaa02e38d40054bff46fcc51444f6f80472
|
|
BLAKE2b-256 checksum How to use checksums |
4d63b7e2e57bb1884f5a3b40b0e9919e5df0822b72b15e8645ad9cea54baa9ef
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|