为解决 AI agent 在自主科研探索过程中获取文献难、给 PDF 却难以正确识别文档格式和公式等痛点,本推出本项目,便于 AI 高效获取和处理论文。
本项目通过无头或有头浏览器直接访问论文网页,从网页中提取格式正确的论文全文,并转为 AI 友好的 Markdown 文件进行保存,同时下载论文的 PDF、全部高清原图和补充材料以供研究。
- ✅ 全文 — 不依赖摘要或片段,提取完整文章正文
- ✅ 排版正确 — 保留章节层级、段落结构
- ✅ 公式正确 — LaTeX 公式完整保留,MathJax/MathML 无缝转换
- ✅ 补充材料完整 — 自动发现并下载数据集、视频等附件
- ✅ 高清原图 — 优先获取期刊提供的高分辨率版本
- ✅ Markdown 化 — 最终输出 AI 原生友好的
.md文件 - ✅ 引用可解析 — 参考文献格式化为 BibTeX 代码块
本项目要求在有相关期刊访问权限的网络内使用(如校园网或机构 VPN)。
本文档说明 complete_paper_extraction.py 的主工作流、各个 publisher 的接口契约,以及新增 publisher 时需要遵守的边界。
入口函数是:
complete_extraction_workflow(doi, output_file=None, force_headed=False)主流程只负责统一调度,不直接处理具体出版商的网页结构。它的职责是:
-
准备输出目录。 默认输出到项目下的
captured_data。每篇论文会先建立 DOI 缓存目录:output_dir / doi.replace("/", "_")。 -
构造 DOI 跳转 URL。
https://doi.org/{doi} -
获取元数据并进行两阶段 publisher 判断。
阶段一(Crossref 元数据决策):先通过 Crossref API 获取 DOI 的元数据,检查
publisher字段是否包含以下出版商名称:HEADLESS_ACCESSIBLE_PUBLISHERS = ["nature", "aip", "cambridge", "springer", "oup"]
- 如果匹配且
force_headed=False:进入阶段二(无头预检)。 - 如果不匹配或
force_headed=True:跳过阶段二,直接使用有头 Chrome。
阶段二(Phase 0 无头预检):启动无头 Chromium 访问 DOI,根据最终跳转 URL 进行备选 publisher 判断。 这是对阶段一的补充,确保在 Crossref 信息不完整或延迟高的情况下仍可做出正确决策。
- 如果匹配且
-
根据 URL/DOI 判断 publisher。 规则位于
publisher/orchestrator.py的detect_publisher_from_url()。 -
创建对应的
PublisherHandler。 -
调用 handler 的统一接口:
await handler.extract_all(captured=captured_data) handler.convert_to_markdown(...)
-
下载 PDF、图片、补充材料。
-
保存 Markdown 和 metadata JSON。
当前 detect_publisher_from_url() 的主要规则:
10.1038、nature.com、springer.com、s41...->nature10.1103、journals.aps.org、prl/pre/pra->aps10.1063、pubs.aip.org、aip.scitation.org->aip10.1088、iopscience.iop.org->iop10.1017、cambridge.org->cambridge10.1093、academic.oup.com->oupsciencedirect.com、10.1016->nature(Elsevier 回退)epj-conferences.org、10.1051->nature(EDP Sciences 回退)arxiv.org->arxiv- 其他 ->
unknown
handler 创建由 get_publisher_handler() 负责:
nature->NatureHandleraps->APSHandleraip->AIPHandleriop->IOPHandlercambridge->CambridgeHandleroup->OupHandlerarxiv-> 带journal_prefix="arxiv"的APSHandlerunknown-> 默认APSHandler
当前主流程有三条路径。
如果 force_headed=False,主流程会先启动无头 Chromium 访问 DOI,并保存:
captured_data/{doi}/headless_initial.html
captured_data/{doi}/page.html
Phase 0 会先访问 DOI resolver URL。如果 DOI 可以直接识别为 Nature,且 DOI resolver 访问失败,会继续尝试 Nature 文章直连 URL:
https://www.nature.com/articles/{doi_suffix}
如果最终 publisher 在:
HEADLESS_ACCESSIBLE_PUBLISHERS = ["nature", "aip", "cambridge", "springer"]中,主流程直接把这个无头 page 传给对应 handler,然后进入统一处理阶段。
无头预检可以使用持久化登录态文件:
.auth/headless_storage_state.json
正常远程运行时,Phase 0 只读取这个文件,不会自动连接 127.0.0.1:9222。如果需要从真实 Chrome 刷新该文件,可以在方便使用本机 Chrome/CDP 时显式运行:
python complete_paper_extraction.py --doi <doi> --refresh-headless-auth.auth/ 不应提交到 git。
如果无头预检没有完整跑完,但 DOI 或最终 URL 可以识别为无头可访问 publisher,主流程不会连接有头 Chrome,而是创建一个没有 page 的 handler:
handler = get_publisher_handler(
publisher,
captured_data_dir=captured_data_dir,
doi=doi,
)这时 publisher 需要在自己的 extract_all() 里处理 page is None 的情况。Nature 当前支持这种模式:当没有收到 page 时,它会自己创建无头浏览器访问 DOI。
如果 publisher 不在 HEADLESS_ACCESSIBLE_PUBLISHERS,或者用户传入 --force-headed,主流程会使用有头 Chrome。
流程是:
-
检查
127.0.0.1:9222是否已有 Chrome。 -
如果没有,通过
chrome_launcher.py启动。 -
使用 Playwright CDP 连接:
http://localhost:9222 -
创建页面。
-
根据 DOI 初步判断 publisher。
-
创建 handler,并在
page.goto()前启动网络监听。 -
跳转 DOI。
-
根据最终 URL 再判断一次 publisher,必要时重建 handler。
-
进入统一处理阶段。
APS 当前只能通过这条有头路径访问;IOP 也通过此路径。
| 条件 | 提取阶段 | 下载阶段 | 说明 |
|---|---|---|---|
force_headed=False, publisher 在 HEADLESS 列表内 |
headless(共用 precheck page) | headless(新建) | Nature、AIP、Cambridge、OUP |
force_headed=False, publisher 不在 HEADLESS 列表内 |
headed CDP | headed(复用 context) | APS、IOP |
force_headed=True, publisher 不在 HEADLESS 列表内 |
headed CDP | headed(复用 context) | 用户显式要求有头,与上一条行为一致 |
force_headed=True, publisher 在 HEADLESS 列表内 |
headless(Handler 自主管理) | headless(新建) | force_headed 被忽略 —— 无头可访问 publisher 仍走无头 |
核心原则:提取和下载阶段使用同一个浏览器模式,force_headed 标识贯穿全流程。
所有 publisher 都继承 publisher/base.py 中的 PublisherHandler。
初始化参数统一为:
PublisherHandler(page=None, captured_data_dir=None, doi=None)含义:
page:可选 Playwright 页面。有头模式或无头直连模式会传入;无头自主管理模式可以是None。captured_data_dir:当前 DOI 的响应缓存目录。doi:当前论文 DOI。
handler 可以通过 configure() 更新上下文:
handler.configure(page=page, captured_data_dir=captured_data_dir, doi=doi)主流程真正依赖的核心接口是:
async def extract_all(self, page=None, doi=None, captured=None) -> dict
def convert_to_markdown(self, metadata, article_text, **kwargs) -> str其他抽象方法用于 publisher 内部组织,例如:
extract_metadata()get_fulltext_url()get_pdf_url()get_supplemental_url()extract_references()get_figures()
所有 publisher 的 extract_all() 必须返回统一结构:
{
"metadata": {
"title": str,
"authors": [str],
"author_with_affiliations": [
{
"author": str,
"affiliations": [str],
}
],
"abstract": str,
"journal": str,
"year": str,
"volume": str,
"issue": str,
"pages": str,
"doi": str,
"publication_date": str,
"corresponding_author_emails": [str],
"references": [str],
},
"links": {
"pdf_url": str,
"figure_urls": {
"fig_1": {
"url": str,
"caption": str,
}
},
"supplemental_urls": [str],
"supplemental_descriptions": {
"filename": "description",
},
},
"fulltext_data": str | dict,
"journal_prefix" or "journal_name": str,
}主流程不关心 fulltext_data 是 HTML 还是 JSON。APS 当前返回 JSON,Nature 当前返回 HTML。具体转换逻辑由各自的 convert_to_markdown() 实现。
无论 publisher 是 APS 还是 Nature,只要进入 process_with_handler(),后续流程一致:
-
调用
handler.extract_all(captured=captured_data)。 -
取出
metadata、links、fulltext_data。 -
使用 Semantic Scholar 数据补全缺失的
year/title。 -
创建最终论文目录:
{year}--{title}/ -
调用
_download_all_resources()下载资源。 -
调用
handler.convert_to_markdown()生成 Markdown。 -
保存
.md。 -
调用
save_metadata_json()保存元数据 JSON。 -
打印统计信息。
PDF、图片和补充材料由主流程统一下载。publisher 只负责在 links 中提供 URL。
_download_all_resources() 会根据 force_headed 决定下载方式:
force_headed=False:新建一个无头 Chromium 专门下载资源。force_headed=True:复用有头 Chrome context,并在需要时新建页面下载,避免破坏当前文章页面。
因此,publisher handler 不应该自己下载 PDF、图片或补充材料。它只负责发现链接和描述。
部分下载链接(如 figshare 的 ndownloader.figstatic.com/files/{id})不含文件扩展名。download_supplemental_materials() 在保存文件后,会调用 _detect_and_rename() 通过 python-magic 读取文件头字节检测 MIME 类型,自动补齐正确的扩展名(.pdf、.zip、.docx 等)。MIME 到扩展名的映射定义在 MIME_TO_EXT 字典中。
APS 由 publisher/aps.py 的 APSHandler 处理。
关键点:
- APS 不在
HEADLESS_ACCESSIBLE_PUBLISHERS中,默认需要有头 Chrome。 setup_network_capture()会监听 APS 的 abstract、fulltext、supplemental 响应。extract_all()依赖有头页面和捕获到的 JSON/HTML。- 正文来自 APS fulltext JSON。
- references 从 abstract HTML 的
ol.references提取。 - APS JSON → Markdown 正文转换由
publisher/aps.py中的convert_json_data_to_markdown()完成。
Nature 由 publisher/nature.py 的 NatureHandler 处理。
关键点:
- Nature 在
HEADLESS_ACCESSIBLE_PUBLISHERS中,可以无头访问。 - 如果主流程没有传入
page,NatureHandler.extract_all()会自己创建无头浏览器访问 DOI。 - 正文来自页面 HTML。
- metadata、authors、images、references、supplementary 等由 Nature handler 从 HTML、JSON-LD、meta 标签中提取。
- Markdown 转换由 Nature handler 按页面 HTML 结构处理。
AIP 由 publisher/aip.py 的 AIPHandler 处理。
10.1063、pubs.aip.org、aip.scitation.org会被识别为aip。- AIP 在
HEADLESS_ACCESSIBLE_PUBLISHERS中,可以直接使用 Phase 0 的无头页面。
从 HTML <head> 中的 citation_* meta 标签提取:
citation_author+citation_author_institution→ 作者及机构citation_title、citation_doi、citation_journal_titlecitation_volume、citation_issue、citation_publication_datecitation_pdf_url→ PDF 下载链接,传给主流程下载
extract_article_text_from_html() 一次遍历完成摘要和正文提取,摘要与正文走同一套公式转换管道(MathML → LaTeX)。方法返回 (abstract_md, body_md) 元组,不再用独立的 extract_main_abstract_from_html() 产生重复解析。
注意:部分新版 AIP 文章所有 section 的 data-section-parent-id 都是 "0"(旧文章只有摘要 section 是 0)。当前代码通过检测 wrapper 内是否包含 <section class="abstract" aria-label="Main abstract"> 来识别摘要,不依赖 parent-id。
extract_figures_from_html() 从 .fig-section 容器中提取图片 URL 和标题,通过主流程统一下载并插入 Markdown。
extract_references_from_html() 从 .mixed-citation 容器提取参考文献,移除 Google Scholar/Crossref/ADS 等外部链接,保留 DOI 链接,转为 Markdown 格式。
_extract_supplemental_links_from_html() 从内联渲染的 figshare widget(#articlefulltext_figshare)中提取 ndownloader.figstatic.com 下载链接,传回主流程下载。
convert_to_markdown() 生成完整 Markdown,结构为:
# 标题
**Authors:**
作者名
机构
**DOI:** 10.1063/...
## Publication
## Abstract
## Article Text
## ReferencesIOP 由 publisher/iop.py 的 IOPHandler 处理。
10.1088、iopscience.iop.org会被识别为iop。- IOP 不在
HEADLESS_ACCESSIBLE_PUBLISHERS中,默认需要有头 Chrome,经过标准有头路径访问。
从 HTML <head> 中的 citation_* meta 标签提取:
citation_author→ 作者姓名,机构从 DOM 元素提取citation_title、citation_doi、citation_journal_titlecitation_volume、citation_issuecitation_publication_date、citation_online_date→ 年份(优先 publication_date)citation_pdf_url→ PDF 下载链接,传给主流程下载citation_abstract→ 摘要文本citation_keywords→ 关键词列表
extract_article_text_from_html() 复用 wildcard.find_generic_article_body() 查找 div.wd-jnl-art-full-text 主内容容器,遍历其中的 heading/paragraph section,通过统一的公式转换管道(MathML → LaTeX)处理数学公式。摘要通过 wildcard.extract_abstract_with_fallbacks() 从 <section data-title="Abstract"> 提取。
extract_figures_from_html() 从 div.wd-jnl-fig 容器提取图片 URL 和标题,fallback 到 body 内的 <img> 标签。
extract_references_from_html() 从 meta[name="citation_reference"] 提取参考文献,通过 wildcard.parse_citation_reference_string() + wildcard.format_as_bibtex() 转为 BibTeX 格式。
convert_to_markdown() 生成完整 Markdown,结构为:
# 标题
**Authors:**
作者列表
**DOI:** 10.1088/...
## Publication
## Abstract
## Article Text
## Figures
## References参考文献以 ```bibtex 代码块输出。
Cambridge 由 publisher/cambridge.py 的 CambridgeHandler 处理。
10.1017、cambridge.org会被识别为cambridge。- Cambridge 在
HEADLESS_ACCESSIBLE_PUBLISHERS中,可以直接使用 Phase 0 的无头页面。
从 HTML <head> 中的 citation_* meta 标签提取:
citation_author→ 作者姓名(无作者机构 meta 标签,机构从 DOM 提取)citation_title、citation_doi、citation_journal_titlecitation_volume、citation_firstpage、citation_publication_datecitation_pdf_url→ PDF 下载链接,传给主流程下载citation_abstract→ 摘要文本citation_keywords→ 关键词列表citation_author_orcid→ 作者 ORCID
作者机构通过 <div data-test-author="Name" class="row author"> DOM 元素提取,其中 <dt class="title"> 为作者名(带 * 表示通讯作者),<dd> 内包含机构名称。通讯作者邮箱从 <div class="corresp"> 和 mailto: 链接提取。
extract_article_text_from_html() 一次遍历 <div class="body"> 内的所有 section(<div class="sec intro">, <div class="sec methods"> 等),返回 (abstract_md, body_md) 元组。正文通过统一的公式转换管道(MathML → LaTeX)处理 <mjx-container> 数学公式。摘要单独从 <div class="article-abstract"> 提取。
extract_figures_from_html() 从 <section> 内的 <div class="fig-ada"> + <div class="figure-thumb"> 组合中提取图片 URL 和标题。标题来自 <div class="caption"> 内的 <span class="label"> 和 <p class="p">,图片 URL 从 <img data-src="..."> 获取。通过主流程统一下载并插入 Markdown。
extract_references_from_html() 从 <div id="references-list"> 容器中提取参考文献,保留 DOI 链接,通过统一公式管道转为 Markdown 格式。
_extract_supplemental_links_from_html() 从 <div class="notes supplementary-material"> 中提取补充材料链接。
convert_to_markdown() 生成完整 Markdown,结构为:
# 标题
**Authors:**
作者名
机构
**Email:** xxx@xxx
**DOI:** 10.1017/...
## Publication
## Abstract
## Article Text
## Supplemental Material
## ReferencesOUP (Oxford University Press) 由 publisher/oup.py 的 OupHandler 处理。
10.1093、academic.oup.com会被识别为oup。- OUP 在
HEADLESS_ACCESSIBLE_PUBLISHERS中,可以直接使用 Phase 0 的无头页面。
从 HTML <head> 中的 citation_* meta 标签提取,包括 citation_author / citation_author_institution 配对(按出现顺序匹配作者与机构)、citation_title、citation_doi、citation_journal_title、citation_volume/citation_issue、citation_firstpage/citation_lastpage、citation_publication_date、citation_pdf_url。
extract_article_text_from_html() 遍历 <div data-widgetname="ArticleFulltext"> 容器的直接子节点:
<h2 class="abstract-title">+<section class="abstract">→ 摘要<h2 class="section-title">/<h3>/<h4>→ 各级标题<p class="chapter-para">→ 段落(含内联公式、xref-bibr/xref-fig 链接)<ul class="roman-lower">/<ol>→ 列表,递归处理嵌套<p><div class="formula-wrap">→ 显示公式,<span class="label title-label">(A1)</span>转为\tag{A1}<div class="block-child-p">→ 内联文字与多个 formula-wrap 混排块,通过占位符再注入<div class="table-full-width-wrap">→ 表格(含标题 + caption + 渲染<table>的.table-overflow版本)<div data-content-id="figN">→ 图注(图片由extract_figures_from_html()处理)
公式通过 <mjx-assistive-mml> 内的 MathML 走 mathml_to_latex_pandoc 转 LaTeX。
extract_figures_from_html() 从 <div data-content-id="figN"> 内的 <a class="download-slide"> 提取高清 URL。OUP 这个 href 是个 /DownloadFile/DownloadImage.aspx?image=... 重定向器,里面嵌入了 Silverchair CDN 的真实地址。_clean_download_slide_url() 剥掉重定向器前缀,并删除会话相关的 sec/ar/xsltPath/imagename/siteId 查询参数(保留 Expires/Signature/Key-Pair-Id,CDN 验签需要),得到可以直接下载的链接。
extract_references_from_html() 遍历 <div id="ref-auto-bib{N}" class="ref-content">:
- 把 citation 内容转成网页显示的样子(
<div class="surname">/<div class="given-names">/<div class="year">/<div class="source">/... 用空格连起来),剥掉Crossref/Search ADS等 citation-links 装饰。 - 从
<div class="pub-id">抓 DOI。 - 与 Crossref
references数据按 DOI 匹配,给每条引用追加一个只含year+doi的@misc{bibN, ...}BibTeX 块。
extract_supplemental_from_html() 找两个地方:
<div class="dataSuppLink">里的<a href="...stz656_supplemental_file.zip">—— 真实下载链接。<h2>SUPPORTING INFORMATION</h2>/<h2>Supplementary data</h2>后面的<p>—— 拿来当文件描述(通常是<strong>filename.ext</strong>形式)。
convert_to_markdown() 输出结构:
# 标题
## Authors # 含机构
## Publication # 期刊 / 卷期 / 页 / DOI
## Abstract
## Article Text # 标题 + 段落 + 公式 + 表格 + 嵌入图
## Acknowledgements # 单独抽出
## Supporting Information # SUPPORTING INFO 文字 + 下载链接
## References # 每条 + 对应 BibTeX 块publisher/wildcard.py 提供跨 publisher 共享的提取函数,被 NatureHandler、IOPHandler、AIPHandler、CambridgeHandler、OupHandler 共同引用:
| 函数 | 用途 |
|---|---|
find_generic_article_body(soup) |
CSS 选择器级联查找文章正文容器 |
extract_abstract_with_fallbacks(soup) |
多策略摘要提取 |
generate_bibtex_key(authors, year, title) |
生成 BibTeX 引用 key |
format_as_bibtex(parts) |
解析后的引用 dict → @article{key, ...} 字符串 |
parse_citation_reference_string(ref_str) |
解析分号分隔的 citation_reference meta 标签 |
prepare_mathjax_html_fragment(html) |
MathJax CHTML → placeholder 折叠 |
convert_html_fragment_to_markdown(html) |
HTML → Markdown + 公式还原 |
convert_mathml(mathml_str) |
MathML → LaTeX(通过 pandoc) |
新增 publisher 时应优先复用 wildcard 中的函数,减少重复代码。
主入口,可通过 asyncio.run() 作为 Python API 调用:
import asyncio
from complete_paper_extraction import complete_extraction_workflow
md_path = asyncio.run(complete_extraction_workflow("10.1103/PhysRevLett.125.015001"))也支持 CLI:python complete_paper_extraction.py "10.1103/PhysRevLett.125.015001"
批量 DOI 处理器,支持从文件或命令行读取多个 DOI:
python batch_process.py --file dois.txt
python batch_process.py --dois "10.1103/..." "10.1063/..."内置随机睡眠防拉黑机制(通过 config.py 的 BATCH_SLEEP_* 配置),可用 --no-sleep 临时禁用。
is_bot_challenge_page(url, html) 在 Phase 0 无头预检阶段检测页面是否为反爬虫拦截页面(Radware Bot Manager、Cloudflare Turnstile、Distil Networks 等)。检测到拦截后自动回退到有头 Chrome 路径,并设置 headless_blocked 标志防止无头 handler 路径被错误触发。
core/utilities.py 中的 format_references_as_bibtex(references) 将参考文献列表转为 BibTeX 代码块:
- 对每条参考文献尝试提取 DOI
- 通过
fetch_semanticscholar()查询 Semantic Scholar 获取结构化元数据 - 构建
@article{key, author={...}, title={...}, ...}条目 - Fallback 到
wildcard.parse_citation_reference_string()解析 - 所有条目包裹在
```bibtex代码块中
SAVE_WITHOUT_REFERENCES 配置项(config.py)控制参考文献为空时是否仍然保存 Markdown。
新增 publisher 时按这个顺序做:
- 新建
publisher/{name}.py,继承PublisherHandler。 - 实现
extract_all()和convert_to_markdown()。 - 复用
publisher/wildcard.py中的共享函数(正文查找、公式转换、BibTeX 格式化等)。 - 如果需要缓存网页或 API 响应,实现
setup_network_capture()。 - 在
publisher/orchestrator.py的detect_publisher_from_url()中增加 DOI/URL 识别规则。 - 在
get_publisher_handler()中返回新的 handler。 - 在
publisher/__init__.py中导出新的 handler。 - 如果该 publisher 可以无头完整访问,把名字加入
HEADLESS_ACCESSIBLE_PUBLISHERS。 - 保持
extract_all()返回统一结构,避免修改主流程。
核心原则:complete_paper_extraction.py 保持 publisher 不敏感;具体网页结构、API 响应、HTML 转 Markdown 的逻辑都放在各自的 PublisherHandler 中。