只读扫描一个 PDF 文献库,逐篇到 Crossref 做匹配,产出 CSV,再转成 Zotero RDF。 脚本不修改 PDF、文件名、目录或 Zotero 数据库。
pip install pypdf pymupdf requests rapidfuzz tqdmpymupdf 用于按字号定位标题(见下文「标题提取」)。缺失时会自动退回 pypdf
的行启发式,准确率明显下降。
python scan_papers_strict.py "/path/to/Papers" --output scan_result \
--mailto you@example.org强烈建议提供 --mailto(或设 $CROSSREF_MAILTO)。它把请求送进 Crossref 的
polite pool,配额远高于匿名池。不提供也能跑通,只是更慢、更多重试。
常用参数:
| 参数 | 默认 | 说明 |
|---|---|---|
--mailto |
无 | Crossref polite pool 联系邮箱 |
--workers |
4 | 并发线程数 |
--rate |
5.0 | 初始请求/秒,会按 Crossref 返回的 X-Rate-Limit-* 头自适应 |
--cache |
<output>/crossref_cache.sqlite |
响应缓存;重跑时几乎不再打 API |
--no-cache |
禁用缓存 | |
--doi-tries |
4 | 每篇最多尝试几个 DOI 候选 |
--head-pages / --tail-pages |
3 / 1 | 读取的首尾页数 |
--top-n |
10 | 保留的候选数 |
--limit |
0 | 只扫前 N 篇,用于试跑 |
--no-title-search |
只走 DOI,不做书目搜索 |
--sleep 已废弃:限流现在按请求而非按文件进行,用 --rate 控制。
| 文件 | 职责 |
|---|---|
scan_papers_strict.py |
编排:遍历、并发、写 CSV |
crossref_client.py |
polite pool、令牌桶限流、重试退避、SQLite 缓存 |
pdf_extract.py |
按版面字号提取标题/作者,NFKC 归一化 |
filename_signal.py |
从文件名解析年份/期刊/作者(独立证据) |
matching.py |
DOI 提取、相似度、打分、置信度、回验 |
csv_to_zotero_rdf.py |
papers.csv → Zotero RDF(保留 collection 树结构) |
打分维度及权重:
| 维度 | 权重 | 来源 |
|---|---|---|
| title | 0.44 | PDF 版面/元数据 |
| filename | 0.30 | 文件名 |
| author | 0.14 | PDF 正文 |
| year | 0.06 | PDF 正文 |
| journal | 0.06 | PDF 正文 |
文件名维度是独立于文本提取的证据,这一点很关键:其余四个维度都来自同一份可能
已经损坏的 PDF 文本,它们会一起失效。本库的命名规范 YY期刊-作者-主题.pdf 实测
年份准确率 98%、期刊 97%、姓氏 89%,比正文提取更可靠。
缺失的维度不参与加权平均(而非记 0 分)。
按优先级:
- 版面 — 第一页字号最大的非样板文字。逐级下探字号,因为 Elsevier 等出版商 把刊名 masthead 排得比标题还大。
- PDF
/Title— 必须通过垃圾模式检查(untitled、Changepdftitle、Microsoft Word - ...、.dvi、期刊页眉…),且与第一页正文能对上才采信。 - 行启发式 — 仅在 PyMuPDF 不可用时使用。
title_source 列记录实际来源。
正文中的 DOI 候选按来源排序后逐个尝试解析,而非取第一个就信:
- 第 1 页 > 第 2-3 页 > 尾页
- 一页上出现 ≥3 个 DOI ⇒ 判为参考文献页,其上所有 DOI 作废
References标题之后的 DOI 作废- 排除数据仓库前缀(Mendeley
10.17632、Zenodo10.5281、figshare10.6084…) - 剥离补充材料后缀(
/-/DCSupplemental*、.s001)与尾部# - 同时扫描「去掉换行」的文本变体,以修复被断行截断的 DOI
Crossref 侧过滤掉 component / dataset / peer-review 类型的记录。
拿到 Crossref 记录后,用文件名年份、标题、作者姓氏做交叉检查,结果写入
verified / verify_note 两列:
confirmed— 有证据支持unverified— 没有可用于核对的独立证据(不代表有问题)conflict— 证据反对这条匹配 ⇒ 强制status=REVIEW
DOI 命中不再等于自动通过。
| 等级 | 含义 |
|---|---|
| HIGH | 证据充分,status=OK,可自动进入下一阶段 |
| MEDIUM | 较可信,人工检查 |
| LOW | 可疑,人工检查 |
| NONE | 证据不足 |
硬性下限:title_score < 0.55 且无强文件名证据时,无论其余字段如何相加都不能到
HIGH。文件名证据强(≥0.85)可升一级,明显矛盾(<0.40)降一级。
doi_crossref 有值不等于匹配成功;只有 match_confidence=HIGH 且
status=OK 才建议自动接受。
papers.csv— 每篇 PDF 一行review.csv— 需要人工检查的条目candidates.csv— 每篇搜索的前 N 个候选,便于判断 top 1 是否正确summary.txt— 统计,含 HTTP 请求数/缓存命中/重试/429 次数crossref_cache.sqlite— 响应缓存
python csv_to_zotero_rdf.py scan_result/papers.csv zotero_import.rdfcollection 列中的 / 会还原为 Zotero 的子集合层级:
beyond-dft/QEDFT/application 导入后是三层嵌套。; 和 | 分隔并列集合。
条目只挂在叶子集合上。
输出会剥离 XML 1.0 非法字符——PDF 元数据里的控制字节曾导致生成的 RDF 无法被 Zotero 解析(报 "The selected file is not in a supported format.")。