lftr
独立的异步 Python LOFTER 抓取与解析库,Python 3.10+。安装名为 lftr,Python 导入名为 lofter。
这是非官方客户端,使用 LOFTER 网页及内部 DWR 标签搜索接口,与 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.1.0-py3-none-any.whl
快速开始
import asyncio
import os
from lofter import LofterClient
async def main():
async with LofterClient(os.environ["LOFTER_COOKIE"]) as client:
posts = await client.fetch_tag_posts("原创", limit=20)
for post in posts:
print(post.post_id, post.title, post.url)
asyncio.run(main())
async with 会复用 HTTP 连接并在结束时关闭。也可手动调用 await client.close()。单个客户端的请求起始时间至少间隔 0.3 秒;连接错误、超时及 HTTP 429/500/502/503/504 最多尝试 3 次,其他 HTTP 错误直接抛出。
结构化抓取
| 方法 | 返回值 | 范围 |
|---|---|---|
fetch_tag_posts(tag, offset=0, limit=20, before=0) |
list[Post] |
一页标签作品 |
fetch_tag_posts_paged(tag, total=...) |
list[Post] |
最多请求 100 条,按 ID 去重 |
fetch_post(url) |
Post |
HTTPS 博主单帖详情 |
fetch_blog_posts(username, enrich=False) |
list[Post] |
博主首页链接,可选逐篇补充详情 |
enrich_posts(posts, continue_on_error=True) |
list[Post] |
按原顺序补充详情,保留原 post_id |
分页示例:
first = await client.fetch_tag_posts("原创")
if first:
before = min(post.publish_time_ms for post in first)
second = await client.fetch_tag_posts("原创", offset=20, before=before)
分页同时使用位置 offset 和上一页最早的毫秒时间戳 before。包不维护订阅游标,不执行“补抓到上次已知帖子”的业务策略,不保证全量枚举,也不保证指定 total 条都为不同作品;可见范围受账号权限、接口分页和平台风控影响。
enrich_posts 默认在单篇失败时记录 warning 并保留原帖,不代表所有详情均成功。需要严格验证时传 continue_on_error=False。fetch_post 对无正文、摘要和图片的页面抛出 RuntimeError,不会把这样的登录页当成成功的帖子。
底层与离线解析 API
为便于迁移已有调用方,保留:
client.get(url, timeout=15):原始 HTML/text。client.search_tag(tag, offset=0, limit=20, before=0):原始 DWR 文本。client.search_tag_paged(tag, total):原始 DWR 分页文本列表,最多请求 100 条。client.update_cookie(cookie):更新后续请求使用的 Cookie。
纯解析函数无需联网:
from lofter import (
Post,
build_tag_search_body,
extract_lofter_username,
parse_blog_posts,
parse_dwr_response,
parse_post_page,
)
Post 保留原插件的 11 个字段:post_id、title、summary、images、author、author_username、url、tags、publish_time、content、publish_time_ms。publish_time 仍使用本地时区格式化;需要机器游标时应使用 publish_time_ms。
HTML 纯解析函数保留原行为:不包含可识别作品的博主页可能返回空列表,单帖解析可能返回空字段;它们不是账号登录状态检测器。HTML 主题差异、登录墙、风控页或站点改版可能导致无法解析。DWR 解析会区分合法空列表、服务端异常、无效结构与非 DWR 响应。
凭据与边界
- 只使用自己有权访问的内容和 Cookie;本包不自动登录或绕过访问限制。
- Cookie 放在进程环境或仅本机可读、被 Git 忽略的
.env.test中,不写入源码、日志、fixtures 或命令历史。 - 低层
get()会向传入的 URL 发送 Cookie;仅向可信目标调用它。优先使用校验目标格式的高层抓取方法。 - DWR 解析会用 dukpy 执行响应脚本,只应处理可信来源的 LOFTER 响应,不应当作通用的不可信 JavaScript 沙箱。
- 图片字段是 URL,不下载图片文件;不包含移动端 API,也不承诺作品总量统计。
测试
默认只运行合成样本与本地 HTTP/TLS 服务,不访问真实 LOFTER:
uv run --locked pytest -q
显式开启真实网络测试:
cp .env.test.example .env.test
# 仅在本机填写 Cookie 和至少有两页作品的标签
uv run --locked pytest -q --live tests/test_live.py
--live 要求 LOFTER_COOKIE 和 LOFTER_TAG;缺少时直接报错,不静默跳过。LOFTER_POST_URL 和 LOFTER_BLOG 是可选的已知样本覆盖,未填写时会从标签搜索结果自动选择单帖 URL 和博主用户名。在线测试会验证真实 DWR 标签分页、HTML 单帖解析、博主主页解析和至少一篇详情 enrich;只请求 LOFTER,不调用 AstrBot 或 QQ。
GitHub Actions 的 Live LOFTER E2E 工作流只支持手动触发,使用仓库 Secret LOFTER_COOKIE 和手动输入的标签,不会在普通 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.1.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.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| lftr-0.1.0.tar.gz | 28.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lftr-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 55.0 kB
Release files / lftr-0.1.0.tar.gz
| Download URL | lftr-0.1.0.tar.gz |
|---|---|
| Size | 28.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
64918d0dfc1c55e01fabfac76170cbd4bd9f07180d746fafd0672a0af3d92094
|
|
BLAKE2b-256 checksum How to use checksums |
fa0e5579d51b0c06057e617cf9bce27d21ab144c9c96d35b03376e5b2b45b864
|
| 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.1.0-py3-none-any.whl
| Download URL | lftr-0.1.0-py3-none-any.whl |
|---|---|
| Size | 27.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bf82792fd8476c09d1ebf7dcd0311924db285d73a712490cb3af0819d0332417
|
|
BLAKE2b-256 checksum How to use checksums |
892cddff23699202db93bfea5943220b9eae1a7c0662e4bc9c12f81e1742254f
|
| 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}
|