Skip to main content
Pre-release

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。Python connect_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)

Table of built distributions (wheels) for sfs-v2 0.0.1.dev2
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.0.1.dev2 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page