Zotero 是一款免费开源的文献管理软件,支持 Windows、macOS 和 Linux 平台。它包含桌面端软件和浏览器插件,社区生态丰富,虽然软件本身免费,但部分增强插件或云同步空间可能需要付费。
官网:https://www.zotero.org/ GitHub:https://github.com/zotero/zotero
最新版本为 8.0.1,更新频率加快,预计每 1-3 个月一个大版本。如果你需要云同步功能,注册账号后开启即可,但免费空间仅 300MB。
本文重点演示如何借助 Zotero 实现英文文献的批量下载与管理,流程与中文文献类似。
一、文献检索与导出
在 Web of Science 等数据库检索文献后,选择导出 RIS 格式。注意 Records from 字段一次最多导出 1000 条,超过需分批(如 1-1000, 1001-2000)。
保存的 .ris 文件是纯文本元数据,包含标题、作者、年份、DOI 等信息,这是后续批量处理的基础。
二、文献批量下载原理
Zotero 主要通过以下几种合法渠道获取全文:
1. 开放获取(Open Access) 利用 Unpaywall 等工具。当你导入条目或点击'查找全文'时,Zotero 会拿着 DOI 去合法的开源数据库查询。如果论文是开源的,它会直接下载。Unpaywall 由非营利组织维护,索引了全球数万个 OA 存储库,速度较快且完全合法。
2. 机构权限 如果你的学校购买了数据库,通过浏览器插件,Zotero 可以利用当前 IP 权限抓取官方 PDF。这种方式准确率最高,能下载到解析完美的版本。但需注意,这通常仅限浏览器插件操作,且大量下载可能触发机构或出版方的风控。
3. Sci-Hub 配置
Zotero 默认不启用 Sci-Hub,但可以通过自定义 Resolvers 配置来实现。用户手册允许修改高级设置中的 extensions.zotero.findPDFs.resolvers 项。市面上已有现成插件可直接安装,无需手动编写 JSON。
注意:全平台付费的文献,若无权限无法免费下载。Sci-Hub 涉及版权风险,请自行评估使用场景。
三、实操步骤
1. 导入文献
在 Zotero 左侧新建一个分类文件夹,将之前下载的 .ris 文件拖入其中。双击文件,勾选'导入到新收藏',系统会自动根据文件名创建子文件夹并导入文献。
2. 获取全文
进入 编辑–设置–高级,可设置本地保存路径。数据保存在 storage 目录下,每个文献对应一个子目录。
选中条目右键选择 查找全文。若成功,右侧会出现 PDF 图标。如需批量操作,全选文献后右键执行相同命令。
3. 处理未下载成功的文献
部分文献虽有 DOI 但自动下载失败,此时可使用 Python 脚本进行针对性补录。
首先,在 Zotero 中筛选出无附件的文献,单独放入一个文件夹,然后导出为 RIS 文件(右键文件夹–导出分类)。
接下来运行以下脚本。该脚本支持并行下载多个镜像站点,并具备断点续传和日志记录功能。
# -*- coding = utf-8-*-
import os
import time
import requests
import pandas as pd
import rispy
from bs4 import BeautifulSoup
concurrent.futures ThreadPoolExecutor, as_completed
warnings
re
warnings.filterwarnings()
os.environ[] =
RIS_PATH =
DOWNLOAD_DIR =
LOG_FILE = os.path.join(DOWNLOAD_DIR, )
SCIHUB_MIRRORS_TEMPLATE = [
,
,
,
,
,
,
]
HEADERS = {
: ,
: ,
: ,
: ,
: ,
: ,
: ,
:
}
():
(filename, ):
filename = re.sub(, , filename)
filename = re.sub(, , filename).strip()
filename[:]
():
()
(file_path, , encoding=, errors=) f:
entries = rispy.load(f)
parsed_data = []
entry entries:
doi = entry.get() entry.get() entry.get()
doi:
notes = entry.get(, [])
(notes, ):
n notes:
n n:
doi = n
title = entry.get() entry.get() entry.get() entry.get() entry.get()
title:
title =
year = entry.get() entry.get() entry.get() entry.get()
year:
date = entry.get() entry.get()
year = date[:] date
doi = (doi).strip() doi
title = (title).strip()
year = (year).strip()
clean_title = sanitize_filename(title)
filename =
status = doi
doi:
()
parsed_data.append({
: doi,
: title,
: filename,
: status,
:
})
pd.DataFrame(parsed_data)
():
os.path.exists(DOWNLOAD_DIR):
os.makedirs(DOWNLOAD_DIR)
rebuild =
os.path.exists(LOG_FILE):
df = pd.read_csv(LOG_FILE)
unknown_count = df[]..contains(, =, na=).()
unknown_count > :
()
rebuild =
:
()
df.loc[df[] == , ] =
:
rebuild =
rebuild:
df = parse_ris_robust(RIS_PATH)
df.to_csv(LOG_FILE, index=)
()
df
():
:
resp = session.get(url, headers=HEADERS, timeout=, verify=, allow_redirects=)
resp.status_code != :
soup = BeautifulSoup(resp.content, )
target = soup.find(, attrs={: }) \
soup.find(, attrs={: re.()}) \
soup.find()
target target.get():
raw_url = target.get()
raw_url.startswith():
+ raw_url
raw_url.startswith():
.join(url.split()[:]) + raw_url
raw_url
btn = soup.find(, onclick=)
btn btn[]:
= re.search(, btn[])
:
raw_url = .group()
raw_url.startswith():
+ raw_url
raw_url
Exception:
():
mirror_url = url_template.replace(, doi)
session = requests.Session()
session.mount(, requests.adapters.HTTPAdapter(max_retries=))
:
pdf_url = get_pdf_direct_link(session, mirror_url)
pdf_url:
,
r = session.get(pdf_url, headers=HEADERS, stream=, timeout=, verify=)
ct = r.headers.get(, ).lower()
ct (r.content) < :
,
r.status_code == :
(save_path, ) f:
chunk r.iter_content(chunk_size=):
f.write(chunk)
, mirror_url
Exception e:
, (e)
,
():
doi = row[]
filename = row[]
save_path = os.path.join(DOWNLOAD_DIR, filename)
()
()
df.at[index, ] =
success =
winning_mirror =
ThreadPoolExecutor(max_workers=) executor:
future_to_url = {
executor.submit(attempt_download_single_mirror, url, doi, save_path): url
url SCIHUB_MIRRORS_TEMPLATE
}
future as_completed(future_to_url):
url = future_to_url[future]
:
is_success, msg = future.result()
is_success:
success =
winning_mirror = msg
()
executor.shutdown(wait=, cancel_futures=)
Exception:
success:
df.at[index, ] =
df.at[index, ] =
:
df.at[index, ] =
df.at[index, ] =
()
df.to_csv(LOG_FILE, index=)
():
()
df = init_task_manager()
tasks = df[df[] == ]
total = (tasks)
()
total == :
()
index, row tasks.iterrows():
file_path = os.path.join(DOWNLOAD_DIR, row[])
os.path.exists(file_path) os.path.getsize(file_path) > :
()
df.at[index, ] =
df.at[index, ] =
df.to_csv(LOG_FILE, index=)
parallel_download_handler(index, row, df)
time.sleep()
__name__ == :
main()


