浏览器插件DownThemAll、Link Grabber、Python脚本三行代码、wget递归一条命令、Puppeteer渲染页转PDF,五种从网站上批量搞到PDF资料的方法,从零门槛到能搞定动态加载页面
某个做竞品分析的哥们上周跟我说了一个数:他花了三天时间从一个行业协会网站上手动右键另存为了400多份PDF报告,点到最后右手食指抽筋。问他为什么不批量搞,他说"想过,但那个网站的PDF链接是JS动态加载出来的,右键审查元素也看不到直接链接,以为只能手动"。这不是孤例,从政府公开数据、学术论文库、行业白皮书、技术文档站点上批量获取PDF的需求太多了,但多数人要么不知道有工具,要么碰到动态加载页面就卡住。
五种方案快速定位
| 方案 | 门槛 | 适合页面 | 核心限制 | 成本 |
| DownThemAll插件 | 零门槛 | HTML中直接暴露链接 | 拿不到JS动态链接 | 免费 |
| Link Grabber | 零门槛 | 链接暴露+需IDM配合 | 同样不认JS渲染链接 | 免费+IDM收费 |
| wget递归下载 | 会敲命令 | 静态HTML+目录结构清晰 | JS渲染页面不行 | 免费 |
| Python脚本 | 懂Python | 静态+登录+分页+API | JS渲染页面需换方案 | 免费 |
| Puppeteer/Playwright | 要写代码 | JS动态+SPA+点击翻页 | 速度慢,吃资源 | 免费 |
一、DownThemAll:浏览器里装个插件,点两下就把当前页面所有PDF链接全下完
如果你要下的PDF链接直接写在HTML的<a href="...">标签里,不是JS动态生成的,DownThemAll是效率最高的方案。零代码,点几下鼠标就行。
装好插件后,打开目标网页,点浏览器右上角DownThemAll图标,选"DownThemAll!",弹出一个对话框列出当前页面所有可下载的链接。关键操作在对话框底部的过滤器:
DownThemAll PDF过滤设置三步

1. 在过滤器框里输入 *.pdf,点击"添加过滤器",插件立刻只显示.pdf结尾的链接
2. 如果PDF文件名有规律(比如report_2023.pdf),在"包含关键词"里填 report_ 进一步筛
3. 选好下载位置,勾选"按服务器目录结构保存",多级目录下的PDF不会全堆在一个文件夹
它支持多线程下载(默认4线程,可调),断网了有断点续传,下次打开插件会自动从上次断的地方继续。
但DownThemAll有个硬伤:只能抓当前页面HTML源码里直接写死的链接。如果PDF链接通过Ajax动态加载,或者页面用React/Vue渲染,DownThemAll看到的HTML里根本没有那些链接。
二、Link Grabber + IDM:一键提取页面所有链接,配合IDM批量下载
Link Grabber的思路和DownThemAll类似,但更轻量——它只负责"提取链接",下载交给IDM(Internet Download Manager)。操作流程:打开目标页面 → 点Link Grabber图标 → 右下角"隐藏HTML网页",只留PDF链接 → 全选点下载 → IDM自动批量接管。
优点
· 比DownThemAll更轻,秒出链接列表
· IDM接管后32线程满速下载
· 可设置按文件类型自动分类到不同文件夹
· 链接列表可直接复制给Python脚本用
短板
· 必须装IDM($11.95永久授权)
· 同样无法处理JS动态加载的链接
· 只能抓当前页面,不能递归爬子页面
· Chrome商店偶尔下架,可能需要手动装crx
三、wget:一行命令递归爬整个目录,专治PDF分散在多层子页面的站
有时候PDF不是集中在一个列表页,而是分散在不同目录层级里——https://example.com/reports/2023/xxx.pdf、/reports/2024/xxx.pdf,一年一个文件夹。wget一行命令递归扫完整个目录树。
# 递归下载某目录下所有PDF,间隔1秒,限速200K
wget -r -np -nd -A ".pdf" \
--wait=1 --limit-rate=200k \
-e robots=off \
-P ./downloads \
https://example.com/reports/
关键参数说明
| -r | 递归模式,顺着链接往下爬 |
| -np | 不爬到父目录,防止从/reports/爬到全站 |
| -nd | 不创建目录层级,所有PDF下到同一文件夹 |
| -A ".pdf" | 只接受pdf后缀,HTML、图片全跳过 |
| --wait=1 | 请求间隔1秒,避免被封IP |
| --limit-rate | 限速200K/s,不占满带宽 |
| --level=2 | 限制递归深度,防无限爬(可选但建议加) |
wget最怕的是"跑偏"。不加-np的话链接里的../../会跑到上级目录甚至全站。目标网站如果有"上一篇""下一篇"翻页链接,wget会顺着一直爬。必须加-np限死范围,再加--level限制深度。
补充一个实际用法:如果PDF链接不在同一个目录下,而是分散在多个已知的URL列表里(比如你从Link Grabber导出过一个txt),可以用 wget -i urls.txt 逐行读取URL批量下载。这个模式比递归更可控,因为URL是你手动筛选过的,不会爬偏。
四、Python脚本:三行核心逻辑,加点东西能处理登录、分页、API接口
如果前三种方案搞不定——需要登录、需要翻页、PDF链接藏在JSON接口里——Python是最灵活的方案。
# pip install requests beautifulsoup4
import requests, os, time

from bs4 import BeautifulSoup
from urllib.parse import urljoin
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
soup = BeautifulSoup(requests.get('https://example.com/reports/', headers=headers).text, 'html.parser')
pdf_links = [urljoin('https://example.com/', a['href']) for a in soup.find_all('a', href=True) if a['href'].lower().endswith('.pdf')]
os.makedirs('pdfs', exist_ok=True)
for url in pdf_links:
fname = os.path.join('pdfs', url.split('/')[-1].split('?')[0])
if os.path.exists(fname): continue
r = requests.get(url, headers=headers, stream=True)
with open(fname, 'wb') as f:
for chunk in r.iter_content(8192): f.write(chunk)
print(f'OK: {fname}')
time.sleep(0.5)
五个常用扩展
需要登录:用 requests.Session() 代替直接get,先 session.post(login_url, data=login_data) 登录,后续请求Cookie自动带
多页翻页:分析URL规律(如?page=2),for循环构造所有页面URL,每页跑一遍提取逻辑
断点续传:检查 os.path.exists() 跳过已有文件,再比对Content-Length头验证完整性
多线程加速:ThreadPoolExecutor(max_workers=5) 并行下载,速度提升明显
API接口直接拿:F12→Network→XHR,找到接口后直接用 requests.get(api_url).json(),比解析HTML干净得多
五、Puppeteer / Playwright:前面四种全搞不定的动态页面,这是终极方案
如果目标网站是Vue/React写的SPA应用,PDF链接在点击"展开更多"按钮后才加载,或者页面内容完全由JS动态渲染——前面四种方案全部失效。这时候需要一个真正能运行JavaScript的浏览器环境。
Puppeteer(Google维护)和Playwright(微软维护)启动一个真实的Chromium浏览器,完整渲染页面,执行所有JS,等页面加载完毕后再提取PDF链接。对服务器来说,它和真人用Chrome访问一模一样。
// npm install puppeteer
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({headless: 'new'});
const page = await browser.newPage();

await page.goto('https://example.com/', {waitUntil: 'networkidle0'});
const pdfLinks = await page.evaluate(() =>
[...document.querySelectorAll('a[href$=".pdf"]')].map(a => a.href)
);
console.log(`找到 ${pdfLinks.length} 个PDF`);
await browser.close();
})();
Puppeteer还能做前面四种方案完全做不到的事——把网页直接转成PDF。很多资料不是以PDF文件提供的,而是HTML页面展示的。用 page.pdf() 可以直接把渲染好的页面输出为PDF,排版、图片、表格完整保留。
能做什么
· 完整执行JS,抓到动态加载的链接
· 模拟点击翻页、滚动加载更多
· 网页直接转PDF(page.pdf())
· 截图保存页面快照
· 处理需要点按钮才能触发下载的页面
代价
· 启动浏览器2-5秒,比纯HTTP慢10-50倍
· 每个Chromium实例吃200-500MB内存
· 首次需下载Chromium内核约150MB
· 服务器上需额外装依赖库
· 部分网站检测headless浏览器
Playwright是Puppeteer的进化版,API更现代,原生支持多浏览器,自动等待元素出现再操作。从零开始选型的话Playwright优于Puppeteer,但已有项目不需要特地迁移。
六、方案选型:看你的页面是什么样的
| 页面情况 | 推荐 | 理由 |
|---|---|---|
| PDF链接直接写在a标签里,一个列表页全列出来 | DownThemAll | 最快,30秒搞定 |
| 链接暴露但文件多体积大,需要高速下载 | Link Grabber+IDM | 多线程满速 |
| PDF分散在多个子目录,需要递归爬 | wget递归 | 一行命令搞定多层目录 |
| 需要登录、翻页、或从API接口获取链接 | Python脚本 | 最灵活,能处理登录/翻页/API |
| JS动态渲染、SPA页面、需要点击按钮才加载 | Puppeteer/Playwright | 唯一能渲染JS的方案 |
七、三个容易被忽略的坑
坑一:文件名里带问号参数,保存时报错
很多PDF链接长这样:xxx.com/download?file=report&id=123。直接用斜杠后面的部分当文件名会得到"download?file=report&id=123",Windows不允许文件名含问号。正确做法是先去掉问号后面的参数,或者从Content-Disposition响应头里拿服务器建议的文件名。
坑二:User-Agent没设好,服务器直接拒绝连接
很多网站的CDN或WAF会检查请求头。用Python默认的requests User-Agent(python-requests/2.x)或者wget默认UA去请求,直接返回403。必须伪装成正常浏览器的UA,最好是Chrome最新版的。另外有些网站还会检查Accept-Language和Referer头,缺了就拦。
坑三:批量下载200个文件后发现其中30个是0字节或HTML登录页
没登录的请求被重定向到登录页,服务器返回的是一个HTML页面而不是PDF,但你的脚本照常保存成.pdf文件。打开一看是登录表单。每次下载完应该检查:1)Content-Type头是不是application/pdf;2)文件大小是否合理(小于10KB大概率不是真PDF);3)文件头四个字节是不是%PDF。三条有一条不对就删掉重下或标记出来。
有个细节值得单独说:Python脚本里 stream=True 这个参数很多人漏掉。不加的话requests会把整个PDF文件一次性读到内存里,几百MB的PDF直接撑爆内存。加上stream=True后是分块读取写入磁盘,内存占用恒定为chunk_size大小(8KB),再大的文件也不怕。另一个容易被忽略的是 timeout=30 参数——不加超时的话,遇到一个挂掉的服务器,你的脚本会永远卡在那里不动。
八、别忽略版权和合规
工具能帮你下载任何公开链接的PDF,但不代表所有PDF都可以随便下载和使用。政府公开数据、学术论文的预印本、开源技术文档一般没问题。但如果目标网站有robots.txt明确禁止爬虫、或者有付费墙、或者PDF标注了版权声明,那工具只是帮你省了右键另存为的力气,不代表获取方式合法。
另外,任何自动化下载都要控制请求频率。1秒1次的节奏对大多数网站是安全的,但如果你开了多线程同时打过去几十个请求,轻则被临时封IP,重则可能被认定为恶意攻击。礼貌爬虫的基本修养:限速、设User-Agent、遵守robots.txt(除非你有合理理由不遵守)、不在对方高峰期狂下。
最后说一句:五个方案各有自己的最佳场景,不存在"哪个最好"。关键是想清楚你要下的PDF在什么样的页面上——是静态链接、还是递归分散、还是需要登录、还是JS渲染——然后对号入座。如果拿不准,用浏览器F12看一眼Network面板,看PDF链接是直接写在HTML里还是通过XHR异步加载的,答案就在那里。
