
从 CVPR 的 Open Access 页面批量抓论文这件事我断断续续折腾过好几个版本。每次会议一放榜群里就有人喊“有没有 XX 方向全部论文的压缩包”我一开始是手动一篇篇点点到手抽筋后来写了个小脚本从抓列表到并发下载基本上几十秒能把一个特定子领域的 PDF 全部拉下来。这篇文章就把这套思路和可复现的代码完整展开你可以直接照着改关键词换到 CVPR 2025 的任意方向上去用。这个方案解决的核心痛点是CVPR 的论文列表动辄几百篇按 Subfield 或关键词精读的时候你需要的可能只是其中一小撮但这一小撮分散在整张列表里。手动保存 PDF 不仅慢还容易漏用脚本自动筛选、批量下载才是能把时间省下来看论文而不是找论文的正道。适合所有做计算机视觉研究的学生、工程师以及任何想快速建立自己领域 PDF 库的人。1. 整体设计思路与方案选型1.1 为什么“下载论文”也是技术活CVPR 作为计算机视觉方向的顶级会议每年收录论文数量都在两千篇上下。对研究者而言真正相关的子领域可能只有几十篇。麻烦的不是论文少而是论文列表的呈现方式官方 Open Access 页面按标题排序不提供“按领域打包下载”的功能。你想系统读一遍“图像分割”方向的论文就得自己把标题看一遍点进去下载再返回列表下一遍。这种机械劳动极其消耗耐心而且极易出错。后来我想明白一件事这类重复性操作本质上就是一个数据抓取任务。论文列表是公开 HTML每篇论文对应一个 PDF 链接筛选条件是自己定的关键词集合。既然任务是“读 HTML 过滤条件 拉文件”那完全可以用脚本自动化。最开始我试过浏览器插件比如一些“批量下载扩展”但它们的过滤逻辑太弱很难按论文标题做语义匹配也无法定制复杂的包含/排除规则。用 Python 脚本处理这类半结构化文本反而最灵活。我不推荐为此去学完整爬虫框架一个脚本真的就够了。核心依赖只有两个requests 负责网络请求BeautifulSoup 负责解析 HTML。没必要引入 Selenium因为 Open Access 页面是静态渲染的没有动态加载用 requests 拿到完整 HTML 就足够。引入无头浏览器反而增加稳定性风险。1.2 方案对比手点、浏览器插件、脚本抓取手动下载的优点是零学习成本缺点是时间成本爆炸。拿 CVPR 2025 的“Diffusion Models”方向举例如果列表里有 80 篇相关论文手动操作最快也要 40 分钟还不算网络波动。浏览器插件通常只解决“批量下载所有链接”的问题并不解决“如何筛出特定领域的论文”。你想要“所有标题含 diffusion 的论文”插件做不到或者只能靠粗糙的 URL 通配符误匹配率很高。反观脚本方案它的优势是筛选逻辑可完全自定义支持标题关键词正则匹配、多关键词叠加、排除词过滤下载过程可控能设置并发数、超时时间、重试次数避免被服务器封禁结果可复现换一个会议年份或者换一组关键词改两行参数就能重新跑。当然脚本也有门槛你得会一点 Python 基础但我会把代码完整贴出来只需要改配置区就能用。1.3 为什么选择官方 Open Access 数据源CVPR 官方把论文的 PDF 和补充材料免费公开在 Open Access 平台上这是最权威的数据源。从官方站点下载链接结构稳定文件名带完整标题不会出现第三方镜像站链接失效、文件损坏的问题。第三方聚合站虽然提供了更友好的论文列表和搜索但它们的下载链接经常指向自己的 CDN不保证长期有效也不方便用脚本做链接构造。有人会问为什么不直接用 arXiv 版本arXiv 的优点是机器可读性更好接口更规范但缺点是并非所有 CVPR 论文都有 arXiv 版本而且有些论文的版本与最终 Camera Ready 版存在差异。Open Access 上的版本就是正式的会议版本做文献引用、版本对照时更可靠。所以我的建议是以 Open Access 为主arXiv 作为补充来源。2. 核心细节解析与实操要点2.1 找到 CVPR 2025 论文列表的正确入口Open Access 平台的结构是按年份和会议划分目录的。访问 openaccess.thecvf.com 主页后能看到年份列表点进 CVPR 2025 就能进入该会议的全部论文列表页。论文列表支持按日期分页也支持显示全部。一个关键技巧是列表页 URL 后面加参数?dayall可以让所有论文都显示在同一个页面省去翻页的麻烦。比如我常用的入口是这种形式以实际页面为准https://openaccess.thecvf.com/CVPR2025?dayall这里关键是dayall参数它让服务器一次性返回所有论文条目。如果没有这个参数页面会按会议日程分区展示你需要依次抓取多个子页才能覆盖全部论文。为了减少请求次数尽量用这个聚合页。注意openaccess 平台偶尔调整 URL 结构如果链接失效就从官网的“Conferences”标签下点进对应年份然后再拼接参数。另外不要盲信搜索引擎结果有些第三方记录的是旧版链接。2.2 解析论文列表 HTML 的隐藏结构打开论文列表页的开发者工具能看到每篇论文的标准 HTML 结构。我当年第一次解析时走了弯路试图从a标签里区分主链接和 PDF 链接后来发现用dt和dd配合解析才是正解。Open Access 页面里每篇论文的标题放在dt classptitle中dt后面通常跟着若干个dd其中第一个包含摘要另一个包含 PDF 链接和补充材料链接。结构大致如下dl classpaper dt classptitle a hrefhttps://openaccess.thecvf.com/content/CVPR2025/html/Some_Title_CVPR_2025_paper.htmlSome Title/a /dt dd a hrefhttps://openaccess.thecvf.com/content/CVPR2025/papers/Some_Title_CVPR_2025_paper.pdfpdf/a /dd /dl我们真正需要的是两个信息论文标题用于关键词过滤论文 PDF 链接用于下载因此解析思路是先找到所有dt.ptitle里的标题文本再在当前论文条目范围内找 PDF 链接。写代码时可以用find_next()或者遍历dl.paper节点确保标题和 PDF 链接对应关系不串位。2.3 环境准备与依赖安装做这件事只需要一个能跑 Python 的终端。我建议用虚拟环境隔离依赖避免污染全局 Python。命令如下python -m venv cvpr-env source cvpr-env/bin/activate # Windows 下用 cvpr-env\Scripts\activate pip install requests beautifulsoup4requests 用于下载 HTML 和 PDFbeautifulsoup4 用于解析 HTML 结构。如果你处理文件名时想用正则Python 自带 re 模块不用额外安装。如果后续想导出 CSV用自带的 csv 模块即可。这里多说一句并发下载时我会用到concurrent.futures线程池这是 Python 标准库不需要额外依赖。整个工程就两个第三方库非常干净。3. 实操过程与核心环节实现3.1 第一步获取论文列表并筛选领域先定义配置区。你需要准备两份关键词列表include_keywords只要标题包含其中任意一个关键词就纳入下载范围exclude_keywords标题包含任意排除词就跳过这篇。关键是“想让脚本具备模糊匹配的能力”我会用正则而不是简单的字符串in这样能实现一些组合匹配比如“3d and detection”这种逻辑。不过对于大多数场景直接用关键词列表就够了。以下是我写的抓取列表和筛选的核心函数import re import requests from bs4 import BeautifulSoup def fetch_paper_list(page_url: str) - list[dict]: headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(page_url, headersheaders, timeout20) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) papers [] for dt in soup.select(dt.ptitle): # 论文详情页链接和标题 title_link dt.find(a) title title_link.get_text(stripTrue) # 往后的 dd 节点里找 pdf 链接 dd dt.find_next(dd) pdf_link while dd is not None and dd.name dd: pdf_anchor dd.find(a, hrefre.compile(r\.pdf$)) if pdf_anchor: pdf_link pdf_anchor[href] if pdf_link.startswith(http): pass else: # 相对路径补全 pdf_link requests.compat.urljoin(page_url, pdf_link) break dd dd.find_next(dd) papers.append({title: title, pdf_url: pdf_link}) return papers def filter_by_keywords(papers: list[dict], include: list[str], exclude: list[str]) - list[dict]: def match(paper): title_lower paper[title].lower() if not any(k.lower() in title_lower for k in include): return False if any(k.lower() in title_lower for k in exclude): return False return True return [p for p in papers if match(p)]这个函数返回的papers里每一篇都有title和pdf_url。筛选时我统一转成小写再做子串匹配这样大小写不会导致漏筛。3.2 第二步并发下载 PDF 并设置重试下载阶段最怕的是服务器限流、连接超时。我采用线程池并发并发数建议控制在 5 到 8 之间太小下载慢太大容易被服务器拒绝。每个任务里做了三件事加自定义 User-Agent设置streamTrue防止一次性加载大文件到内存下载失败时自动重试 3 次并带 2 秒间隔。代码如下import os import time import requests from concurrent.futures import ThreadPoolExecutor, as_completed def download_pdf(paper: dict, save_dir: str, timeout: int 30, retries: int 3): title paper[title] pdf_url paper[pdf_url] # 处理非法文件名字符 safe_title re.sub(r[\\/:*?|], _, title) file_path os.path.join(save_dir, f{safe_title}.pdf) if os.path.exists(file_path) and os.path.getsize(file_path) 0: return f[跳过] {title} headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } for attempt in range(1, retries 1): try: with requests.get(pdf_url, headersheaders, streamTrue, timeouttimeout) as r: r.raise_for_status() with open(file_path, wb) as f: for chunk in r.iter_content(chunk_size8192): f.write(chunk) return f[完成] {title} except Exception as exc: if attempt retries: time.sleep(2 * attempt) else: return f[失败] {title}: {exc} def batch_download(papers: list[dict], save_dir: str, max_workers: int 5): os.makedirs(save_dir, exist_okTrue) with ThreadPoolExecutor(max_workersmax_workers) as executor: futures [executor.submit(download_pdf, p, save_dir) for p in papers] for future in as_completed(futures): print(future.result())这里的关键点是“断点续传”式跳过逻辑如果目标文件已经存在且非空就直接跳过。这样中断后重新运行脚本不会重复下载此前已完成的文件省时间也减少对服务器的压力。3.3 完整脚本串联把上述函数组合到一起再加一个主函数就构成了可一键运行的脚本。保存成download_cvpr_papers.py只需修改配置区即可应急使用。import os import re import time import requests from bs4 import BeautifulSoup from concurrent.futures import ThreadPoolExecutor, as_completed # 配置区 CVPR_PAGE_URL https://openaccess.thecvf.com/CVPR2025?dayall SAVE_DIR ./cvpr2025_diffusion INCLUDE_KEYWORDS [diffusion, flow matching, score-based] EXCLUDE_KEYWORDS [video, point cloud, medical] MAX_WORKERS 5 # # ... 这里放入上面的 fetch_paper_list、filter_by_keywords、download_pdf、batch_download 函数 ... def main(): print(正在获取论文列表...) all_papers fetch_paper_list(CVPR_PAGE_URL) print(f共获取到 {len(all_papers)} 篇论文) selected filter_by_keywords(all_papers, INCLUDE_KEYWORDS, EXCLUDE_KEYWORDS) print(f筛选出 {len(selected)} 篇相关论文) if selected: batch_download(selected, SAVE_DIR, max_workersMAX_WORKERS) if __name__ __main__: main()运行命令python download_cvpr_papers.py执行后终端会逐条打印每篇论文的下载状态。下载目录里最终就是你需要的“特定领域 PDF 合集”。3.4 “1 分钟”的实现逻辑与限制标题说 1 分钟很多人觉得夸张。实际上如果网络带宽足够并且筛选出的论文数量在几十篇以内5 个并发线程下载普通论文 PDF通常 1-3 MB确实能在 1 分钟内完成。真正耗时的环节是抓取列表和解析 HTML这部分单次请求几百毫秒就能完成。但如果论文数量很多比如 200 篇1 分钟就不现实瓶颈往往在服务器带宽和你的网络连接质量。我的建议是不要为了追求“快”而把并发数调得过高这是很反直觉的教训。有次我把max_workers开到 20结果前几次请求全部超时因为服务器把频繁连接判断为异常流量直接拒绝响应。降到 5 后反而稳定了总耗时变化不大因为瓶颈通常不在本机并发能力。如果你真的需要给一个时间估算可以这样计算假设平均每篇 PDF 大小 2 MB网络下载速度 5 MB/s5 个并发下理论上每秒能下约 5 MB那么 50 篇论文大约需要 20 秒再加上抓列表的时间1 分钟完全可以覆盖。这里的前提是你的出口带宽要够服务器的限速没有刻意压低。4. 常见问题与排查技巧实录4.1 请求被拒绝403 或 429如果你在脚本运行时收到 403 Forbidden 或 429 Too Many Requests这通常说明服务器识别到你的请求来自非常规客户端或请求频率太高。我一般按顺序排查检查是否设置了合理的 User-Agent。requests 默认的python-requests/2.x很容易被过滤器拦截换成浏览器 UA 后 90% 的 403 能解决降低并发数把MAX_WORKERS调到 3 或 4下载之间加很小的随机延时比如 0.1 到 0.3 秒避免请求排队过于整齐如果真的被临时封禁等 5-10 分钟再跑不要立刻高强度重试。添加随机延迟可以这样做import random import time # 在 download_pdf 函数内部请求前加一个随机延时 time.sleep(random.uniform(0.1, 0.3))4.2 论文标题和 PDF 链接对不上我在一次跑 CVPR 2023 脚本时发现某些论文的dt和dd节点顺序并不是完全固定的尤其当论文包含补充材料时dd的数量会多一个。如果只写“找最近的一个dd里的 PDF 链接”可能拿到的是摘要链接而不是 PDF 链接。正确的做法是在当前论文的dt后面持续向后查找dd节点直到找到一个包含.pdf链接的dd为止。我在fetch_paper_list里已经用了while dd is not None and dd.name dd这一段来兜底就是为了应对这种结构变化。如果你在自己的抓取里发现链接对不上优先检查这个循环逻辑。除此之外还可以用 page URL 的requests.compat.urljoin补全相对路径防止某些 PDF 链接写成不完整的相对地址。4.3 下载中断、文件损坏网络下载中断是常态。我在代码里加入了“跳过已有文件”的逻辑这比单纯重试更实用。你只要重新运行脚本已经下载好的论文会显示[跳过]未下载的会继续下载。但有一个隐藏问题如果下载过程因为断网中断可能会残留一个半截的 PDF 文件文件大小比正常小很多。这时候“文件存在就不下载”的逻辑反而害了你。解决办法是在跳过判断里检查最小文件大小if os.path.exists(file_path) and os.path.getsize(file_path) 1024 * 1024: return f[跳过] {title}把最小值配置为 1 MB 是一个折中方案——极少数论文的 PDF 可能不足 1 MB但一般正式论文都是数 MB。如果你下载的方向里有大量短文可以把这个值调低到 500 KB。4.4 文件名中的特殊字符导致保存失败论文标题里包含冒号、问号、斜杠是在 Windows 上最致命的问题。比如标题《A Survey: XXX》中的冒号在 Windows 文件名里是非法字符直接open()会因为文件系统错误而失败。我用正则替换把非法字符统一替换成下划线safe_title re.sub(r[\\/:*?|], _, title)这个处理对 Windows、macOS、Linux 都安全因为/:在任何平台的文件名里都不应该出现。另外如果标题特别长建议截断前 100 个字符避免触碰文件系统路径长度上限。4.5 合规与道德提醒批量下载论文这件事和“大规模抓取公开资源”之间没有明确界限。我的建议是始终遵循以下原则只下载官方开放获取的论文不尝试访问付费或需要授权的资源控制请求频率绝不大量并发压垮服务器下载的论文仅供个人学习和研究使用不进行商业分发如果你的单位或合作方有特定的版权要求以相关规定为准。Open Access 论文是作者主动开放版权的成果我们下载它们是合理使用但“合理”不等于“无限度”。保持礼貌的请求频率既是对服务器的尊重也是保证脚本长期可用的前提。5. 进阶玩法从“下载”到“文献管理流水线”5.1 自动生成关键词索引 CSV脚本下载完 PDF 后散落在文件夹里的文件不便于索引。我会额外生成一个 CSV记录每篇论文的标题、PDF 链接、下载状态和本地文件名用来自建论文数据库。方法很简单在批量下载前把筛选结果写入 CSV下载后回填状态。核心代码import csv def save_index_csv(papers, output_pathindex.csv): with open(output_path, w, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnames[title, pdf_url, status]) writer.writeheader() for p in papers: writer.writerow({ title: p[title], pdf_url: p[pdf_url], status: pending })这个 CSV 配合在线协作工具、Excel 都能直接打开后续按标题搜索、按路径打开文件都方便。5.2 接入 Zotero 或本地 PDF 管理器下载完成不等于文献管理完成。我通常会把所有下载的 PDF 目录拖入 Zotero让 Zotero 自动抓取元数据。因为 PDF 文件名就是论文标题Zotero 能准确完成元数据匹配添加时间、作者、摘要等信息。如果你不使用 Zotero也可以用 Everything 这类本地文件搜索工具直接按文件名索引也能达到“秒搜论文”的效果。5.3 定时跟踪新论文把脚本挂到定时任务里可以对同一会议页面反复抓取判断是否有新增论文。不过 CVPR 的论文列表在会议临近及结束后才稳定这个功能更多适用于“投稿版本更新”或“补充材料上传”的场景不太适合日常追新。想追新论文的更适合盯 arXiv 的 cs.CV 分类。要提醒的是频繁轮询官方页面意义不大论文列表更新不频繁。一个更合理的做法是每个月或每个重要节点手动运行一次脚本更新本地的文献库即可。写在最后的一点体会我从手动下载到写脚本最大的感受是这种“一次性投入、反复收益”的自动化思路比任何花哨的效率工具都值得花时间。你只需要在会议放榜时花 20 分钟改改关键词之后每次写综述、找相关工作时都能从这个本地库里快速调出论文省下的时间远超当初写脚本的成本。最后分享一个小技巧下载完成后别急着删脚本。把关键词和会议年份存成参数下次 CVPR、ICCV、ECCV 放榜直接复用。你甚至可以把脚本里会议页面 URL 改成其他会议只需确保页面结构和 Open Access 一致即可。技术不值钱但把重复劳动变成一次性的脚本值钱。