关于 paper-fetch
paper-fetch 是一个命令行工具,可根据 DOI 或论文标题查找并下载论文 PDF,适合自动化文献获取。它按来源优先级尝试解析和下载,并提供批量处理、JSON 输出及可供 Agent 读取的 CLI schema。
主要功能
- 01通过 DOI 或标题获取 PDF
- 02按顺序尝试多个论文来源
- 03支持 DOI 批量下载
- 04提供 JSON 结果与进度事件
- 05输出机器可读 CLI schema
适用场景
- 01按 DOI 下载单篇论文
- 02通过标题解析并获取论文
- 03批量处理 DOI 列表
- 04让自动化程序读取下载结果
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents365-ai/ 365-skills --skill paper-fetch安装后在 Claude Code 中输入/paper-fetch确认已加载。
附带文件scripts/cloak_pdf.pyscripts/fetch.py
详细介绍
paper-fetch
根据 DOI(或标题)获取论文的 PDF。按优先级依次尝试多个来源,并在找到第一个结果后停止。
解析顺序
- Unpaywall —
https://api.unpaywall.org/v2/{doi}?email=$UNPAYWALL_EMAIL,读取best_oa_location.url_for_pdf(若未设置UNPAYWALL_EMAIL,则跳过) - Semantic Scholar —
https://api.semanticscholar.org/graph/v1/paper/DOI:{doi}?fields=openAccessPdf,externalIds - arXiv — 若存在
externalIds.ArXiv,则使用https://arxiv.org/pdf/{arxiv_id}.pdf - PubMed Central OA — 若存在 PMCID,则使用
https://www.ncbi.nlm.nih.gov/pmc/articles/{pmcid}/pdf/ - bioRxiv / medRxiv — 若 DOI 前缀为
10.1101,则查询https://api.biorxiv.org/details/{server}/{doi}获取最新版本的 PDF URL - 出版商直连 (仅限机构模式 —
PAPER_FETCH_INSTITUTIONAL=1) — 将 DOI 前缀映射到出版商 PDF 模板(Nature / Science / Wiley / Springer / ACS / PNAS / NEJM / Sage / T&F / Elsevier)。授权下载的是调用方自己的订阅 IP / Cookie / EZproxy;未授权的响应无法通过%PDF检查,因此会继续尝试第 7 步。 - Sci-Hub 镜像 (默认启用;使用
PAPER_FETCH_NO_SCIHUB=1禁用) — 最后的备用方案。按顺序尝试PAPER_FETCH_SCIHUB_MIRRORS中的镜像列表(或内置默认列表sci-hub.ru、sci-hub.st、sci-hub.su、sci-hub.box、sci-hub.red、sci-hub.al、sci-hub.mk、sci-hub.ee);若全部失败,则在每个进程中抓取一次https://www.sci-hub.pub/,获取最新镜像。验证码 / 缺少论文的页面没有 PDF iframe,因此会静默跳过。 - 否则 → 报告失败,并附上标题 / 作者信息,以便用户通过馆际互借(ILL)申请
CloakBrowser 备用方案(下载层,可选启用 — PAPER_FETCH_CLOAK=1)。 这不是单独的数据源:它位于下载瓶颈处,因此适用于上述任意来源。当 Cloudflare 阻止已解析出的 PDF URL 时——HTTP 403/429,或返回“Just a moment…” HTML 中间页而非文件——且操作员已选择启用时,会通过 cloak_pdf.py 辅助程序使用 CloakBrowser(能够通过 JS 验证的隐身 Chromium)重试该 URL。返回的字节会通过相同的 %PDF 魔数 + 50 MB 检查进行重新验证;成功后,结果会包含 via: "cloak"。默认关闭,且失败时会安全关闭(缺少 CloakBrowser 时静默跳过);Agent 无法选择启用——参见下文的 CloakBrowser access。
如果只提供标题,请通过 --title "<title>" 直接传入。解析链如下:
- Crossref
query.title— 首选;涵盖所有主要期刊 / 会议 DOI - Semantic Scholar
/paper/search/match— 当 Crossref 的首选匹配置信度较低(match_score < 40)或与次选结果的分差小于< 3时,作为备用方案。关键在于,S2 能覆盖仅有 arXiv 的预印本(没有 Crossref DOI)。当 S2 找到的论文只有 arXiv id 时,会合成规范的10.48550/arXiv.<id>,以保持下载链统一。 - Crossref 的最佳猜测(低置信度) — 仅当两个解析器都难以确定时使用。结果封套会设置
meta.title_resolution.low_confidence: true,并包含low_confidence_reason(score_below_threshold/ambiguous_runner_up),以便 Agent 决定放弃或通过--dry-run确认。
无论哪种情况,解析出的 DOI、胜出的解析器、完整的 resolvers_tried 列表以及排名靠前的候选匹配都会显示在 meta.title_resolution 下。
如果已注册 semanticscholar-skill,它可以作为更丰富的标题 → DOI 解析前置步骤——适用于还需要相关性排序、摘要搜索或引文上下文,而不仅仅是 DOI 的情况。Agent 编写 Python 脚本,使用该技能的 match_title() 读取 externalIds.DOI,然后运行 paper-fetch <doi>。如果结果只有 ArXiv id(没有 DOI),则合成 10.48550/arXiv.<ArXiv> 并传给 paper-fetch。
如果只需要 DOI,--title 是单命令路径——paper-fetch 内置的 Crossref → S2 链可以处理大多数情况。
用法
python scripts/fetch.py <DOI> [options]
python scripts/fetch.py --title "<paper title>" [options]
python scripts/fetch.py --batch <FILE|-> [options]
python scripts/fetch.py schema # machine-readable self-description
标志
以下标志是 Agent 在日常使用中会组合使用的标志。完整规范——包括 --dry-run、--pretty、--stream、--overwrite、--timeout、--version,以及参数类型和退出码映射——请运行 python scripts/fetch.py schema(机器可读,并通过 schema_version 检查漂移)。
| 标志 | 默认值 | 说明 |
|---|---|---|
doi | — | 要获取的 DOI(位置参数)。使用 - 从标准输入读取单个 DOI |
--title TITLE | — | 论文标题;下载前会通过 Crossref 将其解析为 DOI。不能与位置参数 DOI 或 --batch 同时使用 |
--batch FILE | — | 每行一个 DOI 的批量下载文件。使用 - 从标准输入读取 |
--out DIR | pdfs | 输出目录 |
--format | auto | 面向 Agent 的 json,面向用户的 text。自动检测:stdout 不是 TTY 时使用 json,是 TTY 时使用 text |
--idempotency-key KEY | — | 安全重试键。使用相同的键重新运行时,会从 <out>/.paper-fetch-idem/ 重放原始信封,不进行网络 I/O |
Agent 发现:schema 子命令
python scripts/fetch.py schema
在 stdout 上输出 CLI 的完整机器可读描述(不进行网络访问)。其中包括 cli_version、schema_version、参数类型、退出码、错误码、信封结构和环境变量。Agent 应读取一次,根据 schema_version 缓存,并在缓存版本发生变化时重新读取。
输出规范
stdout 输出单个 JSON 信封。每个信封都包含一个 meta 槽位。
成功(所有 DOI 均已解析):
{
"ok": true,
"data": {
"results": [
{
"doi": "10.1038/s41586-021-03819-2",
"success": true,
"source": "unpaywall",
"pdf_url": "https://www.nature.com/articles/s41586-021-03819-2.pdf",
"file": "pdfs/Jumper_2021_Highly_accurate_protein_structure_predic.pdf",
"meta": {"title": "Highly accurate protein structure prediction with AlphaFold", "year": 2021, "author": "Jumper"},
"sources_tried": ["unpaywall"]
}
],
"summary": {"total": 1, "succeeded": 1, "failed": 0},
"next": []
},
"meta": {
"request_id": "req_a908f5156fc1",
"latency_ms": 2036,
"schema_version": "1.9.0",
"cli_version": "0.13.1",
"sources_tried": ["unpaywall"]
}
}
部分成功(批量模式——部分 DOI 获取失败,退出码反映失败类别):
{
"ok": "partial",
"data": {
"results": [
{ "doi": "10.1038/s41586-021-03819-2", "success": true, "source": "unpaywall", ... },
{
"doi": "10.1234/nonexistent",
"success": false,
"source": null,
"pdf_url": null,
"file": null,
"meta": {},
"sources_tried": ["unpaywall", "semantic_scholar"],
"error": {
"code": "not_found",
"message": "No open-access PDF found",
"retryable": true,
"retry_after_hours": 168,
"reason": "OA availability changes over time; retry after embargo lifts or preprint appears"
}
}
],
"summary": {"total": 2, "succeeded": 1, "failed": 1},
"next": ["paper-fetch 10.1234/nonexistent --out pdfs"]
},
"meta": { ... }
}
next 槽位是建议后续执行的命令数组:重新调用这些命令只会重试失败的部分。与 --idempotency-key 配合使用,可安全重试整个批次,而不会重新下载已成功的项目。
失败(参数错误,退出码为 3):
{
"ok": false,
"error": {
"code": "validation_error",
"message": "Provide a DOI or --batch file",
"retryable": false
},
"meta": { ... }
}
单项跳过(目标文件已存在,且未指定 --overwrite):
{
"doi": "10.1038/s41586-021-03819-2",
"success": true,
"source": "unpaywall",
"pdf_url": "https://...",
"file": "pdfs/Jumper_2021_...pdf",
"skipped": true,
"skip_reason": "file_exists",
"sources_tried": ["unpaywall"]
}
幂等性重放(使用相同的 --idempotency-key 重新运行):
缓存的信封会原样返回,但 meta.request_id 和 meta.latency_ms 会更新为当前调用的值,并会设置 meta.replayed_from_idempotency_key。不会进行网络 I/O。
Stderr 进度(NDJSON)
使用 --format json 时,stderr 会为保持运行状态每行输出一个 JSON 对象:
{"event": "session", "request_id": "req_...", "elapsed_ms": 0, "cli_version": "0.13.1", "schema_version": "1.9.0"}
{"event": "start", "request_id": "req_...", "elapsed_ms": 2, "doi": "10.1038/..."}
{"event": "source_try", "request_id": "req_...", "elapsed_ms": 2, "doi": "...", "source": "unpaywall"}
{"event": "source_hit", "request_id": "req_...", "elapsed_ms": 2036, "doi": "...", "source": "unpaywall", "pdf_url": "..."}
{"event": "download_ok", "request_id": "req_...", "elapsed_ms": 4120, "doi": "...", "file": "..."}
事件类型:session、start、source_try、source_hit、source_miss、source_skip、source_enrich、source_enrich_failed、download_ok、download_error、download_skip、dry_run、not_found、resolve_error。所有事件都包含 request_id 和 elapsed_ms,使编排器能够关联 stderr 中的进度与最终的 stdout 信封。每次调用只会触发一次 session 事件,且会在任何 DOI 处理或网络 I/O 开始前触发;该事件包含 cli_version / schema_version,让 Agent 无需等待最终信封即可检测缓存副本的模式漂移。
当 Semantic Scholar 仅用于在其他来源已提供 PDF URL 后补充缺失的 author / title 时,会触发 source_enrich;其 fields 数组会准确列出已补充的字段。补充调用失败时会触发 source_enrich_failed——仍会使用 Unpaywall 的 PDF URL,文件名则回退为 unknown_<year>_…。
使用 --format text 时,stderr 会输出人类可读的文本。
退出代码
| Code | 含义 | 可重试类别 |
|---|---|---|
0 | 所有 DOI 均已解析/预览 | — |
1 | 未解析 — 一个或多个 DOI 没有开放获取副本;没有传输故障 | 暂不重试(在 retry_after_hours 后重试) |
2 | 保留用于身份验证错误(目前未使用) | — |
3 | 验证错误(参数错误、缺少输入) | 否 |
4 | 传输错误(网络/下载/IO 故障) | 是 |
此分类法让编排器能够以确定的方式处理故障:值得立即重试退出代码 4,退出代码 1 不值得,而退出代码 3 则是调用方的错误。
JSON 中的错误代码
每个可重试错误都会在错误对象中包含 retry_after_hours 提示,让编排器无需猜测即可安排重试。
| Code | 含义 | 可重试 | retry_after_hours |
|---|---|---|---|
validation_error | 参数错误或输入为空 | 否 | — |
title_resolve_failed | Crossref 对给定的 --title 查询未返回任何条目(请尝试更长/更准确的标题,或直接传入 DOI) | 否 | — |
not_found | 未找到开放获取 PDF | 是 | 168(一周 — 开放获取内容通常会在禁运期/预印本的时间尺度内上线) |
resolve_network_error | 元数据解析器发生传输错误(超时/5xx/403);开放获取情况未知 — 应重试,而不是将其视为 not_found。对应退出代码 4。 | 是 | 1 |
download_network_error | 下载期间发生网络故障 | 是 | 1 |
download_not_a_pdf | 响应不是 PDF(HTML 落地页) | 否 | — |
download_host_not_allowed | PDF URL 未通过 SSRF 安全检查(私有 IP/非 http(s)/非 80,443/被屏蔽的元数据主机/解析到私有网段的主机名/不安全的重定向跳转) | 否 | — |
download_size_exceeded | 响应超过 50 MB 限制 | 是 | 24 |
download_io_error | 本地文件系统写入失败 | 是 | 1 |
internal_error | 意外错误 | 否 | — |
规范映射定义在 scripts/fetch.py 中的 RETRY_AFTER_HOURS,并显示在 schema.error_codes 中。
示例
# Single DOI (JSON output when piped; text when in a terminal)
python scripts/fetch.py 10.1038/s41586-020-2649-2
# Single title (resolved to DOI via Crossref, then downloaded)
python scripts/fetch.py --title "Highly accurate protein structure prediction with AlphaFold"
# Dry-run preview (resolve without downloading)
python scripts/fetch.py 10.1038/s41586-020-2649-2 --dry-run
# Title + dry-run — preview the resolved DOI and candidate matches
python scripts/fetch.py --title "Attention Is All You Need" --dry-run
# Force JSON (for agents even inside a terminal)
python scripts/fetch.py 10.1038/s41586-020-2649-2 --format json
# Human-readable with pretty colors in a pipeline
python scripts/fetch.py 10.1038/s41586-020-2649-2 --format text
# Batch download, safely retriable
python scripts/fetch.py --batch dois.txt --out ./papers \
--idempotency-key monday-review-batch
# Pipe DOIs from another tool
zot -F ids.json query ... | jq -r '.[].doi' | python scripts/fetch.py --batch -
# Agent discovery
python scripts/fetch.py schema --pretty
# Streaming mode — one result per line as each DOI resolves
python scripts/fetch.py --batch dois.txt --stream
# Works without UNPAYWALL_EMAIL (skips Unpaywall, uses remaining 4 sources)
python scripts/fetch.py 10.1038/s41586-020-2649-2
环境
| 变量 | 默认值 | 用途 |
|---|---|---|
UNPAYWALL_EMAIL | unset | 用于 Unpaywall API 的联系邮箱。可选,但建议设置。不设置时将跳过 Unpaywall(其余来源仍可使用)。 |
PAPER_FETCH_INSTITUTIONAL | unset | 设置为任意值(例如 1)以启用机构模式 — 激活每秒 1 个请求的速率限制器和出版商直连回退。见下文。 |
PAPER_FETCH_NO_SCIHUB | unset | 设置为任意值以禁用 Sci-Hub 回退(步骤 7)。 |
PAPER_FETCH_SCIHUB_MIRRORS | unset | 按优先级顺序尝试的逗号分隔镜像主机名(例如 sci-hub.ru,sci-hub.st,sci-hub.su)。覆盖内置默认值。 |
PAPER_FETCH_CLOAK | unset | 设置为任意值以启用 CloakBrowser 回退 — 通过隐身 Chromium 重试被 Cloudflare 拦截的 PDF(HTTP 403/429 或非 PDF 中间页)。见下文的 CloakBrowser 访问。 |
CLOAKBROWSER_PYTHON | auto | 可用于 import cloakbrowser 的 Python 路径,供 cloak 回退使用。自动检测顺序:此变量 → ~/github/CloakBrowser/.venv/bin/python → 当前解释器。 |
PAPER_FETCH_CLOAK_HEADED | unset | 设置为任意值以启动有头(可见)浏览器,而非无头浏览器。较难应对的 Cloudflare 验证(例如 science.org)会让无头模式失效,且只能在真实窗口中通过 — 如果 cloak 回退持续返回 HTTP 403 / "Just a moment…",请设置此项。需要显示器。 |
CloakBrowser 访问(选择启用)
部分出版商(例如 science.org)使用 Cloudflare 防护,纯 HTTP 客户端会收到 403/429,或看到“Just a moment…” JS 挑战页面,而不是 PDF。因此,即使浏览器可以正常访问该 URL,默认的 urllib 下载也无法通过。CloakBrowser 是一款能够通过此类挑战的隐蔽版 Chromium。此技能借鉴了 cloakFetch 的实现方式。
选择启用: export PAPER_FETCH_CLOAK=1(还需要可导入 cloakbrowser 的 Python,参见 CLOAKBROWSER_PYTHON)。
工作原理: 此回退机制位于下载层,而不是作为新的来源,因此适用于任何解析出的 URL(Unpaywall、出版商直链、Sci-Hub,等等)。遇到 Cloudflare 拦截时,fetch.py 会通过指定的 Python 调用配套脚本 cloak_pdf.py;CloakBrowser 加载 PDF 主机的源站以通过 JS 挑战,然后通过页面内的 fetch() 获取 PDF(这样请求会携带浏览器的真实指纹和 cf_clearance Cookie),并将字节内容写入标准输出。fetch.py 本身仅使用标准库,不会导入 cloakbrowser。
无头与有头模式。 配套脚本默认以无头模式运行。某些挑战(例如 science.org)会让无头 Chromium 无法通过,并一直停留在“Just a moment…”页面——设置 PAPER_FETCH_CLOAK_HEADED=1 可使用可见窗口,从而通过这些挑战。已验证:纯客户端请求返回 403 的 www.science.org/doi/pdf/… PDF,可通过有头 CloakBrowser 回退机制成功下载。
仅限同源。 页面内的 fetch 为同源请求,因此当解析出的 URL 是被拦截主机上的 PDF 直链时(例如 www.science.org/doi/pdf/…),此回退机制才有效。若 URL 会跨源重定向(例如不带路径的 doi.org/… 链接),或旧版主机上的挑战始终无法通过,则会安全地失败,并继续尝试下一个来源。
以下行为保持不变:
- 返回的字节内容仍会经过相同的
%PDF魔数检查和 50 MB 大小上限验证。SSRF 防护仍会在启动浏览器前检查 URL。 - 不解决 CAPTCHA。 CloakBrowser 可通过自动化 JS 挑战,但不能通过交互式挑战;遇到交互式 Turnstile 时仍会安全地失败,并继续尝试下一个来源。
- 安全失败。 没有可导入
cloakbrowser的 Python、缺少辅助脚本或发生任何错误时,都会静默地继续尝试下一个来源。Agent 绝不会被告知被拦截的下载已成功。 - Agent 无法自行选择启用——必须由人工操作员设置
PAPER_FETCH_CLOAK。这与机构模式具有相同的信任边界。 - 成功时,每个结果对象都会包含
via: "cloak",以便编排器识别是否使用了回退机制。
成本: 触发回退机制时会启动真实浏览器(耗时约 20–40 秒;首次下载 Chromium 约占用 200 MB)。它仅在常规下载被拦截后才会触发,因此不影响正常下载。
机构访问(主动选择启用)
许多研究人员通过所在机构的 IP 范围(在校园内或通过 VPN)拥有合法的订阅访问权限。Paper-fetch 可以利用这种访问权限,由出版商自身的身份验证机制(你的 IP、会话 Cookie)决定是否提供 PDF。
不同模式下的主机可达性没有区别——公共模式已信任开放获取 API(Unpaywall、Semantic Scholar、bioRxiv、PMC)返回的 URL,并会获取任何通过 SSRF 防护的 HTTPS 主机。机构模式增加了两项功能:(1)直连出版商回退(上文第 6 步):当所有开放获取来源均未命中时,根据 DOI 前缀构造出版商侧的 PDF URL,以便你的机构 IP/Cookie 授权获取;(2)1 req/s 限速器,防止批量任务导致你的 IP 因“系统性下载”而被限流或封禁。
主动选择启用: export PAPER_FETCH_INSTITUTIONAL=1
机构模式下的变化:
| 方面 | 公共模式(默认) | 机构模式 |
|---|---|---|
| 主机可达性 | 通过 SSRF 防护的任何公共 HTTPS 主机 | 相同 |
| SSRF 防护 | 已启用(屏蔽私有 IP / 非 http(s) / 非 80、443 端口 / 云元数据) | 已启用——规则相同 |
| 直连出版商回退 | 关闭 | 开启——根据 DOI 前缀 → 出版商 PDF URL,在所有开放获取来源均未命中后作为最后手段 |
| 限速 | 无 | 1 req/s 令牌桶(所有出站请求) |
meta.auth_mode | "public" | "institutional" |
保持不变的内容:
%PDF魔数检查和 50 MB 大小上限(防止 HTML 落地页和超大响应蒙混过关)- 绝不破解 CAPTCHA。如果出版商显示验证挑战,响应内容将不会以
%PDF开头,paper-fetch 会转而尝试下一个来源。 - 机构模式本身不使用浏览器自动化、Playwright 或隐匿技术——它通过直连出版商 URL 发起普通 HTTP 获取。(浏览器自动化是单独选择启用的功能:上文的 CloakBrowser 回退,由
PAPER_FETCH_CLOAK控制。) - Agent 不能自行选择启用——必须由人工操作员在 Shell 环境中设置
PAPER_FETCH_INSTITUTIONAL。这就是信任边界。
当 paper-fetch 找不到开放获取副本,且你处于公共模式时,错误封装中会包含 suggest_institutional: true,并提示用户设置环境变量。Agent 可以原样呈现该提示,而不是悄无声息地失败。
服务条款提示: 几乎所有出版商的订阅都禁止“系统性下载”。1 req/s 限速以及现有的单文件幂等机制,旨在让个人研究用途保持在可接受范围内。运行多个并行的 paper-fetch 进程或取消限速,可能触发出版商封禁整个机构的 IP。请勿这样做。
说明
- 身份验证由外部负责。 Agent 绝不会运行登录子命令。人工操作员或编排器会在环境中设置
UNPAYWALL_EMAIL;Agent 会继承该变量。未设置邮箱时,会妥善降级为使用其余 4 个来源。 - 信任具有方向性。 CLI 参数只在入口点验证一次。SSRF 防护、
%PDF魔数检查和 50 MB 大小上限由环境层强制执行,而非由 Agent 请求决定。Agent 无法通过传递标志放宽安全限制——选择启用机构模式(及其限速风险配置)是操作员通过环境变量执行的操作。 - 下载天然具备幂等性。 对同一
--out重复运行时,会跳过已存在的文件(确定性文件名:{first_author}_{year}_{journal_abbrev}_{short_title}.pdf;如果元数据缺少期刊/发表平台,则省略期刊部分)。同时使用--idempotency-key,还可以在不进行任何网络 I/O 的情况下重放完全相同的封装。 - 默认输出目录:
./pdfs/。