Generated · Geek Style
用 Scrapy 批量下载网站 PDF:从 14 个索引页到 3176 份文件
914 字
5 分钟
用 Scrapy 批量下载网站 PDF:从 14 个索引页到 3176 份文件
需求背景
手头有一个 txt 文件,里面是 14 个目标网站的索引页面 URL。这些页面本身不是 PDF,而是含大量 PDF 链接的索引页面。目标是把它们背后的所有 PDF 文件全部下载到本地。
为什么选 Scrapy
| 需求 | Scrapy 内置方案 | 自己写 requests 循环 |
|---|---|---|
| 异步并发 | Twisted 异步引擎 | 需手动 asyncio + aiohttp |
| URL 去重 | RFPDupeFilter 自动过滤 | 需自维护 Set/Bloom |
| 递归爬取 | start_urls → yield Request | 需自写 BFS/DFS |
| 重试机制 | RETRY_TIMES=3 自动重试 | 需自己写装饰器 |
| 速率控制 | DOWNLOAD_DELAY + AUTOTHROTTLE | 需手动 sleep |
| 中断恢复 | JOBDIR 持久化队列 | 需自建断点续传 |
| 域名过滤 | OffsiteMiddleware 自动拦截 | 需每个请求检查 |
结论:Scrapy 不是一个简单的 HTTP 库,而是一个爬虫框架。框架帮你处理了 90% 的工程细节,你只写业务逻辑。
爬虫设计
整体思路
14 个起始 URL │ ▼parse() — 解析 HTML,提取所有 <a href="..."> │ ├── .pdf 结尾 ──→ save_pdf() 直接下载 │ ├── 非 HTML 扩展名 (.jpg/.css/.js等) ──→ 跳过 │ └── 其他 HTML ──→ parse() 递归(最大深度 5 层)核心代码
class PdfSpider(scrapy.Spider): name = "pdf_spider" allowed_domains = [...] # 目标域名白名单
# 跳过这些非 HTML 扩展名,避免无效请求和解析异常 NON_HTML_EXTENSIONS = { ".pdf", ".jpg", ".jpeg", ".png", ".gif", ".svg", ".css", ".js", ".zip", ".doc", ".docx", ".mp3", ".mp4", }
def parse(self, response): # ① Content-Type 检查:非 HTML 直接跳过 content_type = response.headers.get("Content-Type", b"").decode("utf-8") if "text/html" not in content_type: if "application/pdf" in content_type: return self.save_pdf(response) # 意外 PDF 也保存 return
# ② 提取所有链接 links = response.css("a::attr(href)").getall()
for link in links: absolute_url = urljoin(response.url, link)
if absolute_url.lower().endswith(".pdf"): yield Request(absolute_url, callback=self.save_pdf) else: ext = os.path.splitext(urlparse(absolute_url).path)[1] if ext not in self.NON_HTML_EXTENSIONS: yield Request(absolute_url, callback=self.parse)
def save_pdf(self, response): filename = self._get_filename(response.url) with open(f"output_dir/{filename}", "wb") as f: f.write(response.body)关键配置
CONCURRENT_REQUESTS = 4 # 全局并发CONCURRENT_REQUESTS_PER_DOMAIN = 2 # 每个域名最多 2 并发DOWNLOAD_DELAY = 1 # 请求间隔 1 秒(尊重服务器)RANDOMIZE_DOWNLOAD_DELAY = True # 随机化延迟RETRY_TIMES = 3 # 失败自动重试 3 次DEPTH_LIMIT = 5 # 最大递归深度DOWNLOAD_TIMEOUT = 60 # 60 秒超时踩坑记录
坑 1:NotSupported: Response content isn't text
现象:爬虫运行一段时间后大量报错。
原因:某些 URL 返回的是二进制(PDF、图片),但被 parse() 当作 HTML 调用了 response.css()。
解决:在 parse() 开头检查 Content-Type header,非 HTML 直接跳过。
坑 2:无意义请求过多
现象:爬虫在下载 .jpg、.css 等无用资源。
原因:没有在链接层面过滤非 HTML 资源的扩展名。
解决:添加 NON_HTML_EXTENSIONS 集合,在生成新 Request 前过滤掉。
坑 3:同名文件冲突
现象:不同目录下有同名 PDF(如多个站点都有 01a.pdf)。
解决:写入前检查文件是否存在,存在则追加 _1、_2 后缀。
运行结果
| 指标 | 数值 |
|---|---|
| 下载 PDF 数量 | 3,176 |
| 总文件大小 | 1.8 GB |
| 爬取页面数 | 7,847 |
| 成功响应 | 7,802 |
| 运行时长 | ~10 小时 |
| 平均速度 | ~50 页/分钟 |
| 403 错误 | 332(被拒页面,不影响下载) |
| 最大递归深度 | 5 层 |
项目结构
project/├── urls.txt ← 起始 URL 列表├── pdf_downloader/ ← Scrapy 项目│ ├── scrapy.cfg│ └── pdf_downloader/│ ├── settings.py ← 全局配置│ └── spiders/│ └── pdf_spider.py ← 爬虫核心逻辑(唯一手写文件)├── crawl_jobdir/ ← JOBDIR 持久化队列├── scrapy_log.txt ← 运行日志└── *.pdf ← 下载的所有 PDF总结
这个任务的本质是从一个种子集合出发,沿着链接图做受限遍历。Scrapy 用 5 个内置中间件(Offsite、Depth、Retry、Redirect、UserAgent)和 20 行配置覆盖了所有工程细节,你只需要在 parse() 和 save_pdf() 两个回调里写业务逻辑。这就是”框架”和”库”的区别。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!
用 Scrapy 批量下载网站 PDF:从 14 个索引页到 3176 份文件
https://fzy.it.com/posts/scrapy-pdf-downloader/ 相关文章 智能推荐
1
从零构建 Google Trends 爬虫 API:单文件架构的工程实践
技术实践 详解一个基于 Flask + Playwright 的 Google Trends 热搜数据采集服务,涵盖双解析策略降级、指数退避重试、Pending 离线缓存、假成功防御等生产级工程实践。
2
Scrapy 架构深度解析:每个文件在框架中扮演什么角色
技术原理 以 PDF 批量下载项目为实例,逐文件拆解 Scrapy 项目结构(settings.py、spider、middlewares、pipelines、items),解释引擎-调度器-下载器-爬虫-管道五层架构的协作原理。
3
全异步 Yahoo 热门新闻爬虫:模块化架构与三路数据输出实践
技术实践 详解一个基于 Python asyncio + Playwright + Kafka 的 Yahoo Trending 新闻爬虫,涵盖双引擎降级策略、7种选择器链、asyncio 与同步 requests 混用方案,以及本地 JSON、Kafka、ODS 三路并行数据输出设计。
4
列表和元组,到底用哪一个?
Python 从可变性、性能到语义层面,一次讲清 Python 里 list 和 tuple 到底该怎么选。
5
从单机到集群:分布式爬虫后端架构设计指南
系统设计 设计一套完整的分布式爬虫系统——任务队列派发、VPS 集群执行、Kafka 数据总线分发、多数据库(MySQL/MongoDB/ES/Doris/Redis)各司其职,包含故障处理和容错设计。
随机文章 随机推荐