Skip to main content

lftr

独立的异步 Python LOFTER 抓取与解析库,Python 3.10+。安装名为 lftr,Python 导入名为 lofter。

这是非官方客户端,使用 LOFTER app 端 JSON 接口,与 LOFTER 官方无隶属关系。不依赖 AstrBot,也不包含订阅数据库、消息发送或统计条件管理。

安装

PyPI 安装名为 lftr。首次发行成功后可使用:

python -m pip install lftr

安装名与导入名不同:代码仍使用 from lofter import LofterClient,不要执行 pip install lofter。

从有访问权限的 GitHub 仓库安装开发版本:

python -m pip install "git+ssh://git@github.com/wdcyxxycdw/lofter.git@main"

正式接入其他项目时,建议将 main 换为已经验证的完整提交号。

从源码开发或构建 wheel:

uv sync --locked
uv build
python -m pip install dist/lftr-0.2.0-py3-none-any.whl

快速开始

import asyncio

from lofter import LofterClient


async def main():
    async with LofterClient() as client:
        posts = await client.fetch_tag_posts("原创")
        for post in posts:
            print(post.post_id, post.title, post.url)


asyncio.run(main())

抓取公开内容不需要 Cookie。async with 会复用 HTTP 连接并在结束时关闭,也可手动调用 await client.close()。单个客户端的请求起始时间至少间隔 0.3 秒;连接错误、超时及 HTTP 429/500/502/503/504 最多尝试 3 次,其他 HTTP 错误直接抛出。

错误语义

app 接口的 HTTP 状态码恒为 200,真实状态在响应体的 meta.status:

meta.status 行为
200 正常
401 抛出 LofterAuthError(继承 RuntimeError)
其他(常见 4200) 抛出 RuntimeError。4200 是通用错误码,参数不合法、method 名无效与内容不可见都会返回它,其 msg 文案不可直接展示给用户

合法的空结果会返回空列表,不会与错误混淆。

结构化抓取

返回 Post(11 个字段,契约冻结):

方法 返回值 范围
fetch_tag_posts(tag, offset=0, sort="new") list[Post] 一页标签作品
fetch_tag_posts_paged(tag, total=...) list[Post] 按服务端游标翻页,按 ID 去重
fetch_post(url) Post HTTPS 博主单帖详情
fetch_blog_posts(username, limit=50, offset=0) list[Post] 博主作品,已含完整正文与全部图片
fetch_collection_posts(collection_id, offset=0, limit=20) list[Post] 合集内作品
enrich_posts(posts) list[Post] 按原顺序补充详情,保留原 post_id

sort 可取 new / total / date / week / month,不同取值的结果集互不相同。

标签搜索的每页条数由服务端决定(实测 12 条),limit 参数被服务端忽略;fetch_tag_posts 仍保留 limit 与 before 形参以兼容旧签名,但二者不再影响结果。分页请使用 offset:

first = await client.fetch_tag_posts("原创")
second = await client.fetch_tag_posts("原创", offset=len(first))

fetch_blog_posts 的响应已带完整正文与全部图片,因此 enrich=True 不再触发额外请求。enrich_posts 默认在单篇失败时记录 warning 并保留原帖;需要严格验证时传 continue_on_error=False。fetch_post 对没有正文、摘要和图片的帖子抛出 RuntimeError。

本包不维护订阅游标,不执行“补抓到上次已知帖子”的业务策略,不保证全量枚举。可见范围受账号权限、接口分页和平台风控影响。

扩展字段

Post 的 11 个字段是冻结契约,新增数据一律通过 PostDetail 提供,不会改变 Post 的形状:

方法 返回值
fetch_tag_posts_detailed(tag, offset=0, sort="new") list[PostDetail]
fetch_blog_posts_detailed(username, limit=50, offset=0) list[PostDetail]
fetch_post_detailed(url) PostDetail
detail = (await client.fetch_tag_posts_detailed("原创"))[0]
detail.post           # 仍是 11 字段的 Post
detail.post_type      # 1=文字 2=图片 4=视频(3/5/6 未在样本中出现)
detail.word_count     # 字数
detail.is_top         # 是否置顶
detail.create_time_ms # 创建 / 修改 / 编辑时间
detail.modify_time_ms
detail.edit_time_ms
detail.stats.views    # 互动数据:responses/favorites/reblogs/shares/views/subscribes/hot

fetch_post_detailed 的 stats.views 恒为 0,这是单帖接口自身的行为,其余计数正常;需要浏览量请改用 fetch_blog_posts_detailed 或 fetch_tag_posts_detailed。

视频与合集

video = await client.fetch_post_video(url)        # 非视频帖返回 None
video.url, video.hls_url, video.h265_url          # mp4 直链 / HLS / H.265
video.cover_url, video.duration, video.size
video.width, video.height

collections = await client.fetch_collections("someuser")
posts = await client.fetch_collection_posts(collections[0].collection_id)

Collection 字段:collection_id、name、description、cover_url、tags、 post_count、view_count、hot、last_publish_time_ms。

底层与离线解析 API

  • client.get(url, timeout=15):原始 HTML/text。
  • client.search_tag(tag, ...) / client.search_tag_paged(tag, total):保留的原始 DWR 文本出口,仍走 TagBean.search,需要有效 Cookie。
  • client.update_cookie(cookie):更新后续请求使用的 Cookie。
  • decode_permalink("1d038690_34f54df9c"):纯本地把 permalink 解成 (blog_id, post_id)。

纯解析函数无需联网:parse_dwr_response、parse_post_page、parse_blog_posts、extract_lofter_username、build_tag_search_body。HTML 与 DWR 解析器保留原行为,但高层抓取已不再经过它们。

凭据与边界

  • 只使用自己有权访问的内容;本包不自动登录或绕过访问限制。抓取公开内容不需要 Cookie。
  • Cookie 仅用于保留的原始 DWR 出口。放在进程环境或仅本机可读、被 Git 忽略的 .env.test 中,不写入源码、日志、fixtures 或命令历史。
  • 低层 get() 会向传入的 URL 发送 Cookie;仅向可信目标调用它。优先使用校验目标格式的高层抓取方法。
  • DWR 解析会用 dukpy 执行响应脚本,只应处理可信来源的 LOFTER 响应,不应当作通用的不可信 JavaScript 沙箱。
  • 图片与视频字段是 URL,不下载媒体文件。
  • **读取会计入浏览量。**通过 app 接口读取作品会使该作品的 viewCount 增加。大规模抓取会虚增作者的浏览数据,请据此控制频率与范围。
  • 使用的是 LOFTER app 端非公开接口,URL 中的 product= 版本号会随 app 升级漂移,接口可能在任何时候变更或失效。

兼容性

Post 的 11 个字段是冻结契约。新增数据一律走 PostDetail、Video、Collection 等独立类型, 以免 asdict(post) / astuple(post) / Post(**stored_dict) 这类往返用法在调用方侧被破坏。

高层抓取已全量切换到 app JSON 接口,没有回退到 DWR 的路径:数据要么来自 app 接口,要么抛错, 不会出现字段因链路不同而静默为空的情况。原始 DWR 出口(search_tag、search_tag_paged、 parse_dwr_response、build_tag_search_body)保持原行为不变。

测试

本地运行与 CI 相同的 Ruff 静态检查:

uv run --locked ruff check .

每次 push/PR 都会运行独立的 lint job,检查 E4、E7、E9 和 F 规则,覆盖源码与测试中的导入位置、语法和未使用变量等问题;不自动修改或格式化代码。

默认只运行合成样本与本地 HTTP/TLS 服务,不访问真实 LOFTER:

uv run --locked pytest -q

显式开启真实网络测试:

LOFTER_TAG=摄影 uv run --locked pytest -q --live tests/test_live.py

--live 只要求 LOFTER_TAG;缺少时直接报错,不静默跳过。LOFTER_POST_URL 和 LOFTER_BLOG 是可选的已知样本覆盖,未填写时会从标签搜索结果自动选择单帖 URL 和博主用户名。在线测试会验证标签分页与去重、单帖详情、扩展字段、博主作品与合集抓取;只请求 LOFTER,不调用 AstrBot 或 QQ。

另外提供 LOFTER_COOKIE 时会额外验证保留的原始 DWR 出口仍然可达;未提供时该用例跳过。

GitHub Actions 的 Live LOFTER E2E 工作流只支持手动触发,不会在普通 push/PR CI 中运行。 CI 在 Python 3.10 和 3.13 上运行离线测试、构建 sdist/wheel,再将 wheel 安装至仓库外的干净虚拟环境重新运行测试,验证没有依赖原插件或源码路径。

发布到 PyPI

发布工作流为 .github/workflows/publish.yml,仅在 GitHub Release 正式发布时触发,不在提交或 PR 时上传。

在 PyPI 的账户 Publishing 页面添加 pending trusted publisher:

字段 值
PyPI project name lftr
Repository owner wdcyxxycdw
Repository name lofter
Workflow filename publish.yml
Environment 留空

工作流合并到 main 后,为对应提交创建 v0.2.0 Release。工作流会检查标签与 pyproject.toml 的版本一致,运行离线测试、构建 lftr 的 wheel/sdist,再通过 OIDC Trusted Publishing 上传。构建任务没有发布权限,发布任务不需要长期 API Token 或 LOFTER Cookie。

发布会公开安装包及其中的源码;GitHub 仓库本身无需改为公开。PyPI 的同名版本文件不能覆盖,后续发行必须更新版本及锁文件,再创建对应的新 Release。

来源与许可证

提取自 wdcyxxycdw/astrbot_plugin_lofter,基准提交 3606b03e4c1689fd5bbff87b3b857a5ba2674a3f。原插件 metadata 标注作者为 yamanashi,保留原作者及贡献者归属,详见 NOTICE。

沿用原项目 README 中的 AGPLv3 声明,本包为 AGPL-3.0-only,完整文本见 LICENSE。本次独立包提取不修改原插件的部署或数据库;插件侧依赖迁移另行进行。

Metadata

Release files for lftr 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for lftr 0.2.0
File Size Uploaded
lftr-0.2.0.tar.gz 34.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for lftr 0.2.0
File Interpreter ABI Platform
lftr-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 67.7 kB

Release files / lftr-0.2.0.tar.gz

Download URL lftr-0.2.0.tar.gz
Size 34.4 kB
Tags Source
SHA-256 checksum
How to use checksums
d36a88cf996798e044af1570a7fde8f747208d58e606a263138c9319fae60692
BLAKE2b-256 checksum
How to use checksums
df6d17cd8c9f5927adcadc1a4e81a18396b10e99bc180fbe5bbafbc6f1df4637
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / lftr-0.2.0-py3-none-any.whl

Download URL lftr-0.2.0-py3-none-any.whl
Size 33.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
af337d14edbfc47fe3ad6103932ad42baf6adc86e7d90e53037f17f8feb0c17d
BLAKE2b-256 checksum
How to use checksums
181acc06a4e10fa1e620b5730d92519bc1f80b18a08b1221ad459405b5e1cce3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

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