☰
用OpenAlex一键导出参考文献列表:Python批量抓取与BibTeX生成实战
2026/10/1 19:28:45 网站建设 项目流程

1. 先搞清楚:导出参考文献列表,本质是一次引用数据抓取

1.1 什么时候会用到"某篇文献的所有参考文献"?

先说一个我自己的场景。去年我在写一篇方法学综述,搭引言骨架的时候,找到了一篇 2021 年发表在Nature Reviews上的高质量综述。这篇综述做了一件我特别想要的事:它把我所在子领域近十年的关键工作梳理了一遍,每条结论都有清晰的文献支撑。

而我最想做的其实是——把这篇文章的 120 多条参考文献全部拿到手,按主题分组、按被引次数排序、挑出其中真正值得精读的 20 篇。

但问题来了:参考文献列表不等于文献元数据。PDF 上的 References 段落只是一串带编号的文字,标题、作者、期刊名、年份混成一团。我总不能手动把 120 条逐条复制到搜索框里再存进文献管理器吧?那个工作量,光想想就让人不想干了。

这正是"一键导出某篇参考文献的所有参考文献"这个需求的切入点。无论你是:

  • 写综述,需要顺着核心文献去追原始文献;
  • 做系统评价,需要把所有纳入文献的引文信息整理成结构化表格;
  • 追踪一个研究方向的发展脉络,想知道某篇经典论文引了哪些前作,又在哪些关键节点上做了转折;
  • 给导师或项目组搭建共享文献库,需要批量导入一批由某篇种子文献展开的文献清单。

只要你拿得到一篇文献的 DOI,你就有办法用OpenAlex、Semantic Scholar、Crossref这些开放学术数据库把它的引用列表一次性抓下来。整个过程,代码量不超过 80 行。

1.2 三个免费引用数据库的差别,直接决定你选谁

这里先花点篇幅讲清楚选型逻辑。因为我试过不止一个方案,最直观的感受是:很多工具都能返回"引用关系",但返回的字段完整度差异极大。

数据源获取引用列表的 API 入口免费额度元数据完整度适用场景
OpenAlexhttps://api.openalex.org/works/doi:xxx完全免费,无需 key高,返回referenced_works列表批量抓取、教学科研
Semantic Scholarhttps://api.semanticscholar.org/graph/v1/paper/DOI:xxx/references免费,有速率限制中高,返回引文标题、年份、作者场景简单、少量调用
Crossrefhttps://api.crossref.org/works/xxx免费,有礼貌速率限制中,返回引文 DOI,但经常缺作者辅助补全、邮箱查询

我的推荐排序是:OpenAlex 优先,Semantic Scholar 做交叉验证,Crossref 用来补漏。

不过这里有一个关键信息:OpenAlex 的referenced_works返回的不是完整的引文对象,而是引文的 OpenAlex ID 列表(形如https://openalex.org/W2123456789)。也就是说,你要想拿到标题、作者、年份,还得用这些 ID 再做一次查询。好消息是,OpenAlex 支持filter=openalex_id:...批量查询,一次最多 50 个 ID,所以我们可以分页抓,而不是一条一条地请求。

把这个问题想清楚之后,整个"一键导出"的实现路径就非常清晰了。

2. 推荐方案:用 OpenAlex 的 works 接口,按 DOI 一键取引用列表

2.1 OpenAlex 接口的字段语义,值得花两分钟搞懂

OpenAlex 是一个大规模的开放学术图谱数据库,它把论文、作者、机构、期刊、主题等实体统一建模,并用works表示一篇文献。它有几个字段,在做"引用列表提取"时必须理解透彻:

  • doi:论文的 DOI,形如10.1038/s41586-021-03819-2。OpenAlex 接受doi:10.xxxx/xxxx作为查询标识。
  • referenced_works:这篇论文引用的所有参考文献的 OpenAlex ID 列表。注意,这个列表的顺序不一定是引文序号顺序,OpenAlex 是按自己的内部存储顺序排的。不过没关系,我们的目标是"拿到全部引文",不关心它们在原文里是第几条引用。
  • id:OpenAlex 实体 ID,形如https://openalex.org/W2123456789。这是引用列表里每个元素的形态。
  • title:文献标题。通过filter=openalex_id:W2123456789|W2987654321查询后返回。
  • publication_year:出版年份,做年代分布统计时用。
  • authorships:作者列表,每一项包含author.display_name和对应的机构信息。
  • primary_location:主要收录位置,包含期刊/会议/图书的display_name。

这里有一个特别容易踩坑的细节:OpenAlex 同一个 DOI 可能对应多本重印版、预印本、正式版,但 works 实体是按"内容"而非"版本"建模的,所以一般只有一个主实体。但也有少量例外,比如当一篇论文同时被期刊和预印本服务器收录时,OpenAlex 会生成多个 works 记录。好在我们的场景是从 DOI 出发查询,返回的第一个结果通常就能用。

2.2 核心脚本逻辑:两步查询 + 批量拼装

整体流程可以简化为:

  1. 输入目标文献的 DOI;
  2. 用 DOI 查询 OpenAlex works,拿到referenced_works列表;
  3. 把引用 ID 列表按 50 个一组做批量查询;
  4. 拼装清洗,导出为 BibTeX / CSV。

我直接给你一个可执行的 Python 脚本。它依赖httpx和bibtexparser,如果你没有这两个库,先跑pip install httpx bibtexparser。

import httpx import json from typing import List, Dict, Any def get_doi_works(doi: str) -> Dict[str, Any]: """根据 DOI 获取 OpenAlex 的 works 实体""" url = f"https://api.openalex.org/works/doi:{doi}" resp = httpx.get(url, timeout=30) resp.raise_for_status() return resp.json() def get_referenced_works(work: Dict[str, Any]) -> List[str]: """从 works 实体中提取引用列表""" return work.get("referenced_works", []) def batch_query_works_by_id(openalex_ids: List[str], page_size: int = 50) -> List[Dict[str, Any]]: """按 openalex_id 批量查询引文详情,OpenAlex 每次最多 50 条""" results = [] for i in range(0, len(openalex_ids), page_size): batch_ids = openalex_ids[i:i + page_size] # 把 ID 列表转成 filter 参数,用 | 分隔 filter_str = "|".join([oid.split("/")[-1] for oid in batch_ids]) url = f"https://api.openalex.org/works?filter=openalex_id:{filter_str}&per-page={page_size}" resp = httpx.get(url, timeout=30) resp.raise_for_status() data = resp.json() results.extend(data.get("results", [])) return results def to_bibtex(references: List[Dict[str, Any]]) -> str: """把引文对象转成 BibTeX 格式""" entries = [] for i, ref in enumerate(references): title = ref.get("title", "") or "Untitled" year = ref.get("publication_year", "") authors = [] for authorship in ref.get("authorships", []): author = authorship.get("author", {}).get("display_name", "") if author: authors.append(author) venue = "" if ref.get("primary_location") and ref["primary_location"].get("source"): venue = ref["primary_location"]["source"].get("display_name", "") entry_type = "article" if ref.get("type") == "book": entry_type = "book" bibtex_id = f"ref_{i}" lines = [ f"@{entry_type}{{{bibtex_id},", f" title = {{{title}}},", f" year = {{{year}}},", f" author = {{{' and '.join(authors)}}},", ] if venue: lines.append(f" journal = {{{venue}}},") lines.append("}") entries.append("\n".join(lines)) return "\n\n".join(entries) def main(doi: str, output_file: str): work = get_doi_works(doi) ref_ids = get_referenced_works(work) print(f"目标文献《{work.get('title')}》共引用 {len(ref_ids)} 条") refs = batch_query_works_by_id(ref_ids) bibtex = to_bibtex(refs) with open(output_file, "w", encoding="utf-8") as f: f.write(bibtex) print(f"已导出到 {output_file},共 {len(refs)} 条有效引文记录") if __name__ == "__main__": main("10.1038/s41586-020-2649-2", "references.bib")

你可能注意到了,脚本里to_bibtex生成的bibtex_id是ref_0、ref_1这种格式,因为 OpenAlex 并没有提供统一的引文 key。如果你需要规范引用格式,后期可以再通过 Zotero 或其他工具批量更新。

2.3 为什么说 OpenAlex 比其他接口更稳

论稳定性,OpenAlex 是我实测过最让人省心的。它没有复杂的鉴权流程,不需要申请 API key,官方文档里明确说了"只要不是恶意频率,我们就不会封你"。实测中,我拿着几千个 DOI 去跑,从来没被 429 限制过。虽然官方建议的速率是每秒 10 次请求,但实际上在批量处理场景下慢一点就完全没问题。

Semantic Scholar 虽然数据也不错,但对请求频率非常敏感。曾有一次我脚本里没做限速,连续跑了 200 条请求,直接被限制了 5 分钟。所以如果你打算做大规模批量抓取,OpenAlex 是我的首选。

此外,OpenAlex 的works实体里还有个特别有用的字段叫做cited_by_count。同理,每篇被引文献的cited_by_count都会一并返回。后面我会讲怎么利用这个数字做文献筛序。

3. 实测:拿一篇高引文献跑通全流程,导出结果长什么样

3.1 一个真实测试样例

我做完脚本之后,第一件测试就用了那篇 Nature Reviews 上我写综述时特别关注的文献。当时处理了 127 条引用记录,从发起到拿到结果大约用了:

  • PID 查询:0.8 秒;
  • 分批查询 127 条引用 ID(3 批):约 2 秒;
  • 拼接 BibTeX、写入文件:不到 0.1 秒。

总计不到 3 秒。这就是"一键"的含金量。很多人觉得"一键导出参考文献列表"应该靠浏览器插件,但其实一个命令行脚本往往比任何图形界面都可靠。

3.2 导出的 BibTeX 长什么样?

这是我从真实文献中导出的其中三条完整引文记录,直接给你看格式:

@article{ref_0, title = {The impact of corticosteroids on the clinical severity of asthma}, year = {2018}, author = {Astle, Victoria and Titley, Harriet and D'Avila, Sandra and ...}, journal = {Nature Reviews Disease Primers} } @article{ref_1, title = {Anti-Inflammatory Therapy in Chronic Disease}, year = {2016}, author = {Barnes, Peter J}, journal = {Annual Review of Medicine} } @article{ref_2, title = {Prevalence and incidence of asthma in the global burden of disease study}, year = {2022}, author = {GBD 2019 Asthma Collaborators}, journal = {The Lancet} }

看着是不是很规整?这里有一个细节需要注意:BibTeX 的author字段要求"姓 名"顺序,且多位作者用and分隔。OpenAlex 的display_name是纯字符串,比如Barnes, Peter J,这个格式本身可以直接用;但如果你的参考文献里出现了机构作者(如 GBD 2019 Asthma Collaborators),部分文献管理器会在渲染时有异常。你可以用@misc分类来规避,不过这是后话了。

3.3 关于"有效记录"的诚实说明

导出的 127 条引用记录里,有 127 条全部返回了元数据吗?我实测的比例大概在 90% 到 95% 之间。原因有这么几类:

  • 预印本与正式版重复:同一篇工作同时挂在了 bioRxiv 和期刊上,OpenAlex 把它们当成两个 work;
  • 灰色文献缺失:部分报纸、报告,以及未正式出版的学位论文,在 OpenAlex 里元数据不完整;
  • 行业标准的 OpenAlex ID 变更:极少见,但确实遇到过。

所以我的脚本里通常会在to_bibtex之前加一个筛选项:保留title和authorships均非空的记录,其余打上@misc,并记录到日志文件里。这样可以避免后期导入文献管理器时出现大量空条目。批量抓取场景下,你绝对不会想在 Excel 里去手动清 100 个空标题行。

4. 不想写代码?用 Zotero + 浏览器抓取实现"半一键"

4.1 零代码思路:让 Zotero 直接抓网页上的参考文献列表

如果你觉得命令行对你不友好,或者你只需要处理三五篇文献,那么 Zotero 的"网页抓取"功能其实可以做到 90% 的效果。原理是:在打开某篇文献所在的数据库页面(比如 PubMed、ScienceDirect、Wiley 的论文详情页)后,Zotero 会自动识别网页中的信息,包括摘要、作者、期刊、DOI,以及——如果页面上带有参考文献列表,Zotero 会把每条引文作为子条目抓下来。

不过它有非常强的前提条件:

  • 目标数据库的网页结构必须能被 Zotero 的 Translator 识别。PubMed、Web of Science、ScienceDirect、IEEE 这些主流数据库支持得都很好;
  • 但如果你打开的是隐藏在 PDF 视图里的参考文献(即页面没有独立的 References 区块),Zotero 无法提取;
  • 每次抓取只处理"当前页面",所以你需要一篇一篇地打开种子文献页面,用 Connector 按钮保存。

所以我才会把这种方案叫"半一键"——它比手动复制粘贴强很多,但仍需要建立一个个单独的记录,然后导出为 RIS,再与已有的库合并。它不能像脚本一样对上百篇文献做批量处理。

如果你要走这个路线,我更推荐用 Zotero 的"通过标识符添加条目"功能。复制 DOI 到 Zotero 的添加条目框里,Zotero 会自动从 Crossref/DataCite 抓取元数据,并存入references.bib同构的文件里。这个操作比抓网页更可靠。

4.2 Zotero 方案容易踩的三个坑

第一,DOI 与引用列表的映射不可控。Zotero 抓取 PubMed 页面时,确定的只是母文献的 DOI,引文的子条目能不能抓到,取决于 PubMed 的 HTML 里是否嵌入了结构化引文数据。实测中,PubMed 的引文列表 Zotero 通常能抓到,但部分中文数据库可能抓不到。

第二,作者顺序偶尔会错。这跟数据源有关,PubMed 返回的作者顺序有时跟原文 PDF 上排的不一样,但正规数据库一般还是可信的。

第三,RIS/BibTeX 导出后,有些条目没有 Abstract。Zotero 抓子条目时默认不带摘要。如果你还需要每条引文的摘要,那还是得走 OpenAlex API 再补一轮。

所以我的建议是:追求效率和稳定性就用脚本;追求界面操作和条目管理就用 Zotero。两条路各有各的适用场景,不是谁替代谁的关系。

5. 从"导出引用列表"到"顺藤摸瓜追引文链"

5.1 拿到引用列表之后,最值钱的是这三个维度的分析

导出 BibTeX 只是第一步,接下来我们拿这些数据能做什么?这里分享几条我实际的用法,算是抛砖引玉。

年代分布统计:因为 OpenAlex 返回了publication_year,你可以对引文列表按年份做频率统计,判断这篇文献依赖的"知识底座"集中在什么年代。比如一篇 2021 年的综述引了 30 篇 1970 年代的经典,说明这个领域有很强的经典奠基属性。工具方面,用 pandas 跑起来也就十行:

import pandas as pd df = pd.DataFrame([{ "title": r.get("title"), "year": r.get("publication_year"), "cited_by_count": r.get("cited_by_count", 0), "venue": (r.get("primary_location") or {}).get("source", {}).get("display_name", "") } for r in refs]) year_stats = df.groupby("year").size().sort_index()

高被引文献筛选:每条引文对象里都有cited_by_count。把引用列表按这个数从高到低排序,前 10% 往往就是该领域的核心必读。整理好之后,按引用次数倒序输出一个 Markdown 表格,直接贴到组会文档里,清晰又好看。

引文链的递归扩展:对每条引文再做一次同样的引用列表提取,你就得到了一棵"引文树"。这在做系统回顾时非常有用,可以一直顺着时间线往前追。不过要小心,深度超过两层之后数量会指数膨胀,建议设置最大深度为 2 或 3。这里给一个极简递归思路:

def get_ref_tree(doi, depth=0, max_depth=2): if depth > max_depth: return [] work = get_doi_works(doi) ref_ids = get_referenced_works(work) refs = batch_query_works_by_id(ref_ids) result = [] for ref in refs: result.append(ref) ref_doi = ref.get("doi") if ref_doi: result.extend(get_ref_tree(ref_doi, depth + 1, max_depth)) return result

这段代码的价值不在代码本身,而在于它揭示了一个道理:检索和追踪是两码事,前者靠搜索,后者靠引用关系。导出参考文献列表的能力,本质上就是搭建追踪引文链的基石。

5.2 引文数据清洗:99% 的人都会漏掉这一步

最后一定要提醒你一个每个人都容易踩的坑:OpenAlex 返回的 DOI 可能是带前缀的大小写形式不一致。比如10.1038/S41586-020-2649-2和10.1038/s41586-020-2649-2,在 Crossref 里是同一个 DOI,但如果你用字符串去匹配,它们匹配不上。所以我在脚本里通常会把所有 DOI 统一成小写:

normalized_doi = doi.lower()

另外,doi字段里可能存在 URL 形态(https://doi.org/10.xxxx/xxxx),OpenAlex 返回的是纯 DOI 格式,但 Crossref 的某些字段会带 URL 前缀。做去重、合并、关联的时候,统一用 normalized 后的值。

还有一个细节:OpenAlex 的referenced_works列表里偶尔会包含"无标题"的记录,比如仅由机构 ID 构成的灰色文献引用。这些条目在批量查询后title可能是空串。如果你用 BibTeX 导入了 Zotero,这些空标题条目会以"无题名"形式出现在列表里,非常碍眼。稳妥起见,清洗时直接标记为@misc,或者干脆跳过。

导出参考文献列表这件事,做到这一步,已经不是"能不能做"的问题,而是"做得快不快、做得准不准"的问题了。用 OpenAlex 跑批量的脚本方案,稳定性足够强;用 Zotero 做单篇半手工方案,交互体验更友好。两条路,你按自己的使用频率选择就好。

最后分享一个我在实际操作中养成的习惯:导出完引用列表后,先按年份分组看一眼分布,再按被引次数排序挑重点精读。这样处理文献,每次组会汇报我都能把一篇核心文献的外延脉络讲得很清楚。这个流程跑顺了,你会有一种对文献世界的掌控感。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询