跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
PythonAI

Zotero 8.0.1 英文文献批量下载与自动化脚本实战

Zotero 8.0.1 支持多平台同步,演示基于 Web of Science 导出的 RIS 文件进行文献批量导入。通过配置 Unpaywall、机构权限及 Sci-Hub 解析器实现全文获取,针对未成功下载的条目提供 Python 并行下载脚本。此外介绍 AI 插件配置方法,辅助文献深度分析与阅读,构建高效科研工作流。

字节跳动发布于 2026/4/9更新于 2026/5/2218 浏览
Zotero 8.0.1 英文文献批量下载与自动化脚本实战

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()
from
import
import
import
'ignore'
'CURL_CA_BUNDLE'
''
# ================= 配置区域 =================
r"xxx.ris"
r"xxx\未下载"
"download_status_log.csv"
# 7 个镜像站点模板
"https://www.pismin.com/{doi}"
"https://sci-hub.st/{doi}"
"https://sci-hub.ru/{doi}"
"https://sci-hub.box/{doi}"
"https://sci-hub.red/{doi}"
"https://sci-hub.ren/{doi}"
"https://sci-hub.ee/{doi}"
'User-Agent'
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36'
'Accept'
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'
'Connection'
'keep-alive'
'Upgrade-Insecure-Requests'
'1'
'Sec-Fetch-Dest'
'document'
'Sec-Fetch-Mode'
'navigate'
'Sec-Fetch-Site'
'none'
'Sec-Fetch-User'
'?1'
# ================= 核心功能函数 =================
def
sanitize_filename
filename
if
not
isinstance
str
return
"Unknown_Filename"
r'[\\/*?:"<>|]'
'_'
r'\s+'
' '
return
200
def
parse_ris_robust
file_path
print
f"[解析] 正在读取 RIS 文件:{file_path}"
with
open
'r'
'utf-8-sig'
'ignore'
as
for
in
'doi'
or
'DO'
or
'number'
if
not
'notes'
if
isinstance
list
for
in
if
'10.'
in
and
'/'
in
break
'title'
or
'primary_title'
or
'TI'
or
'T1'
or
'T2'
if
not
"Unknown_Title"
'year'
or
'publication_year'
or
'PY'
or
'Y1'
if
not
'date'
or
'DA'
4
if
else
"NoYear"
str
if
else
""
str
str
f"[{year}] {clean_title}.pdf"
'Pending'
if
else
'No_DOI'
if
not
print
f" [警告] 发现无 DOI 文献:{title[:30]}..."
'DOI'
'Title'
'Filename'
'Status'
'Message'
''
return
def
init_task_manager
if
not
False
if
'Title'
str
'Unknown Title'
case
False
False
sum
if
5
print
f"[检测] 旧记录文件包含 {unknown_count} 个未知标题,判定为解析失败。正在强制重新解析 RIS..."
True
else
print
f"[读取] 加载现有进度,共 {len(df)} 条记录。"
'Status'
'Downloading'
'Status'
'Pending'
else
True
if
False
print
f"[构建] 新的统计文件已创建,共 {len(df)} 条。"
return
def
get_pdf_direct_link
session, url
try
10
False
True
if
200
return
None
'html.parser'
'embed'
'type'
'application/pdf'
or
'iframe'
'src'
compile
r'\.pdf'
or
'div'
if
and
'src'
'src'
if
'//'
return
'https:'
if
'/'
return
'/'
'/'
3
return
'button'
True
if
and
'location.href'
in
'onclick'
match
r"href='(.*?)'"
'onclick'
if
match
match
1
if
'//'
return
'https:'
return
except
pass
return
None
def
attempt_download_single_mirror
url_template, doi, save_path
"{doi}"
'https'
1
try
if
not
return
False
"Page parsed but no PDF found"
True
20
False
'Content-Type'
''
if
'html'
in
or
len
1000
return
False
"Not a PDF file"
if
200
with
open
'wb'
as
for
in
8192
return
True
except
as
return
False
str
return
False
"Unknown Error"
def
parallel_download_handler
index, row, df
'DOI'
'Filename'
print
f"\n--> [{index + 1}/{len(df)}] 开始并行下载:{doi}"
print
f" 目标文件:{filename}"
'Status'
'Downloading'
False
""
with
8
as
for
in
for
in
try
if
True
print
f" [√] 成功!来源镜像:{winning_mirror.split('/')[2]}"
False
True
break
except
continue
if
'Status'
'Downloaded'
'Message'
f"From {winning_mirror}"
else
'Status'
'Failed'
'Message'
"All mirrors failed or timed out"
print
f" [X] 所有镜像均失败:{doi}"
False
def
main
print
"=== 2026 WoS 极速下载器 (并行版) ==="
'Status'
'Pending'
len
print
f"\n=== 待处理队列:{total} 个文献 ==="
if
0
print
"没有由于下载的任务。检查是否需要重置 'Failed' 状态。"
return
for
in
'Filename'
if
and
2000
print
f"[{index + 1}] 文件已存在,跳过:{row['Filename']}"
'Status'
'Downloaded'
'Message'
'File exists'
False
continue
1
if
"__main__"

运行前请修改代码中的 RIS_PATH 和 DOWNLOAD_DIR 路径。脚本会自动记录下载状态,避免重复下载。

对于仍无法下载的文献(如无 DOI 或全网付费),建议直接在浏览器中手动保存。

4. 整理文献

如果需要逐篇分析文献内容,可将 Zotero 本地的 PDF 文件统一复制到一个目录。以下脚本可递归扫描并去重复制。

# -*- coding = utf-8-*-
import os
import shutil
from pathlib import Path

def collect_zotero_pdfs(src_dir, dest_dir):
    src_path = Path(src_dir)
    dest_path = Path(dest_dir)
    if not dest_path.exists():
        dest_path.mkdir(parents=True, exist_ok=True)
    print(f"已创建输出目录:{dest_path}")
    print("正在扫描并复制文件,请稍候...")
    count = 0
    for pdf_file in src_path.rglob('*.pdf'):
        try:
            target_file = dest_path / pdf_file.name
            if target_file.exists():
                stem = pdf_file.stem
                suffix = pdf_file.suffix
                counter = 1
                while target_file.exists():
                    target_file = dest_path / f"{stem}_{counter}{suffix}"
                    counter += 1
            shutil.copy2(pdf_file, target_file)
            count += 1
            if count % 10 == 0:
                print(f"已处理 {count} 个文件...")
        except Exception as e:
            print(f"处理文件 {pdf_file.name} 时出错:{e}")
    print("-" * 30)
    print(f"任务完成!共成功复制 {count} 个 PDF 文件到:")
    print(dest_path)

if __name__ == "__main__":
    input_directory = r"你的 zotero 的数据保存路径\storage"
    output_directory = r"输出目录"
    collect_zotero_pdfs(input_directory, output_directory)

四、AI 插件集成

Zotero 支持多种 AI 插件辅助阅读,以对话插件为例。安装后需在设置中配置模型和 API Key。

推荐使用 Gemini 或 DeepSeek。选择 Full API 模式,填入对应的 API ID 和 Key。例如 Gemini 需手动填写 gemini-3-flash-preview。

配置完成后,选中一篇文献,点击插件图标提问即可。AI 能帮你总结摘要、解释术语或生成参考文献列表,大幅提升阅读效率。

提示:API Key 涉及费用,请按需充值。

目录

  1. 一、文献检索与导出
  2. 二、文献批量下载原理
  3. 三、实操步骤
  4. 1. 导入文献
  5. 2. 获取全文
  6. 3. 处理未下载成功的文献
  7. -- coding = utf-8--
  8. ================= 配置区域 =================
  9. 7 个镜像站点模板
  10. ================= 核心功能函数 =================
  11. 4. 整理文献
  12. -- coding = utf-8--
  13. 四、AI 插件集成
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 豆包 AI 实战指南:代码提效、API 集成与避坑指南
  • 字节跳动交易与广告前端一面面经深度解析
  • 机器人 SLAM 技术核心原理解析
  • VSCode Copilot 无法连接网络的解决方案
  • Windows 部署 OpenAkita 并接入飞书,打造本地 AI 助手
  • ClawdBot Web 控制台模型切换实操:Config→Models→Providers
  • PX4 飞控系统入门指南:从零搭建无人机开发环境
  • 基于 Java、GeoTools 与 PostGIS 的对跖点求解实战
  • OpenClaw、EasyClaw 与 WorkBuddy 三款 AI 智能体工具安装与选型指南
  • 位运算实战:判断字符唯一性与查找缺失数字
  • Python 住宅代理自动化采集音乐数据实战
  • Spring Cloud 微服务:Sentinel vs Resilience4j 深度对比与选型指南
  • 圣光艺苑:基于 SDXL 的一键鎏金画框生成与提示词指南
  • 数据结构:C 语言单链表实现与原理
  • 大模型高效推理与部署技术实战
  • 二叉树深度优先搜索:核心原理与实战案例
  • 免费开源漫画阅读器 Komikku 使用指南
  • Java 全栈工程师面试实录:从基础到项目实战
  • Go 语言实现回文字符串检测算法详解
  • 医疗大模型 LoRA 微调实战指南

相关免费在线工具

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

  • 随机西班牙地址生成器

    随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online

  • curl 转代码

    解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online