用Scrapyd爬取豆瓣图书Top250
在数据采集场景中,异步爬虫是提高效率的核心方案,而 Scrapyd 作为 Scrapy 的部署调度工具,能让爬虫实现分布式运行和定时任务管理。本文将基于「豆瓣图书Top250爬取并写入Excel」的实战案例,详细拆解项目搭建、部署流程,以及过程中遇到的6个典型问题和解决方案,适合爬虫新手参考学习。
一、项目需求与技术选型
1. 核心需求
- 爬取目标:豆瓣图书Top250全量数据(书名、作者、出版社、评分等10个字段)
- 技术要求:异步爬取、支持部署调度、数据导出为Excel
- 反爬要求:绕过豆瓣基础反爬机制,保证爬取稳定性
2. 技术选型
| 工具/框架 | 作用说明 | 核心优势 |
|---|---|---|
| Scrapy | 异步爬虫核心 | 天然支持异步并发、中间件生态完善、数据处理流程清晰 |
| Scrapyd | 爬虫部署与调度 | 支持分布式运行、提供Web管理界面、可通过API调度任务 |
| openpyxl | Excel数据写入 | 支持大文件写入、可设置单元格样式、兼容.xlsx格式 |
| fake-useragent | 反爬辅助 | 随机生成User-Agent,模拟浏览器访问 |
| 代理IP | 突破IP封禁 | 解决豆瓣IP限制问题,提高爬取成功率 |
二、项目搭建完整流程
1. 环境准备
首先安装依赖包(建议使用虚拟环境,避免版本冲突):
# 创建虚拟环境(Windows)
python -m venv crawler-env
# 激活虚拟环境
crawler-env\Scripts\activate
# 安装核心依赖
pip install scrapy scrapyd scrapyd-client openpyxl fake-useragent
2. 项目结构搭建
# 创建Scrapy项目
scrapy startproject douban_book_spider
# 进入项目目录
cd douban_book_spider
# 创建爬虫
scrapy genspider book_spider book.douban.com
最终项目结构:
douban_book_spider/
├── douban_book_spider/
│ ├── items.py # 定义爬取字段
│ ├── middlewares.py # 反爬/代理中间件
│ ├── pipelines.py # Excel写入逻辑
│ ├── settings.py # 项目核心配置
│ └── spiders/
│ └── book_spider.py # 爬虫核心逻辑
└── scrapy.cfg # Scrapyd部署配置
3. 核心模块实现
(1)定义爬取字段(items.py)
import scrapyclass DoubanBookSpiderItem(scrapy.Item):book_name = scrapy.Field() # 书名author = scrapy.Field() # 作者publisher = scrapy.Field() # 出版社publish_date = scrapy.Field() # 出版日期price = scrapy.Field() # 价格rating = scrapy.Field() # 评分comment_count = scrapy.Field() # 评价人数intro = scrapy.Field() # 简介cover_url = scrapy.Field() # 封面URLdetail_url = scrapy.Field() # 详情页链接
(2)爬虫核心逻辑(book_spider.py)
实现分页爬取、详情页提取逻辑,支持异步请求:
import scrapy
from douban_book_spider.items import DoubanBookSpiderItemclass BookSpider(scrapy.Spider):name = 'book_spider'allowed_domains = ['book.douban.com']start_urls = [] # 清空默认URL,通过start方法生成# 异步生成分页URL(Scrapy 2.13+推荐用start()替代start_requests())async def start(self):# 豆瓣Top250共10页,start参数从0开始,每次+25for start in range(0, 250, 25):url = f'https://book.douban.com/top250?start={start}'yield scrapy.Request(url=url,callback=self.parse_book_list,headers={'Referer': 'https://book.douban.com/','User-Agent': self.settings.get('USER_AGENT')},dont_filter=True)# 解析列表页,提取详情页链接def parse_book_list(self, response):book_items = response.xpath('//div[@class="pl2"]')for item in book_items:detail_url = item.xpath('./a/@href').extract_first()if detail_url:yield scrapy.Request(url=detail_url,callback=self.parse_book_detail,headers={'User-Agent': self.settings.get('USER_AGENT')},meta={'detail_url': detail_url})# 解析详情页,提取图书完整信息def parse_book_detail(self, response):item = DoubanBookSpiderItem()detail_url = response.meta['detail_url']# 书名book_name = response.xpath('//span[@property="v:itemreviewed"]/text()').extract_first()item['book_name'] = book_name.strip() if book_name else '未知书名'# 作者、出版社、出版日期、价格(豆瓣详情页格式统一,拆分提取)info_str = ''.join([s.strip() for s in response.xpath('//div[@id="info"]//text()').extract() if s.strip()])item['author'] = info_str.split('作者:')[1].split('出版社:')[0].strip() if '作者:' in info_str else '未知作者'item['publisher'] = info_str.split('出版社:')[1].split('出版年:')[0].strip() if '出版社:' in info_str else '未知出版社'item['publish_date'] = info_str.split('出版年:')[1].split('页数:')[0].strip() if '出版年:' in info_str else '未知日期'item['price'] = info_str.split('定价:')[1].split('装帧:')[0].strip() if '定价:' in info_str else '未知价格'# 评分、评价人数、简介、封面URLitem['rating'] = response.xpath('//strong[@property="v:average"]/text()').extract_first() or '0.0'item['comment_count'] = response.xpath('//span[@property="v:votes"]/text()').extract_first() or '0'item['intro'] = '\n'.join([line.strip() for line in response.xpath('//div[@class="intro"]//p/text()').extract() if line.strip()]) or '无简介'item['cover_url'] = response.xpath('//img[@rel="v:image"]/@src').extract_first() or '无封面'item['detail_url'] = detail_urlyield item
(3)Excel写入Pipeline(pipelines.py)
使用线程锁避免异步写入冲突,设置Excel样式:
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment
import threadingclass DoubanBookExcelPipeline:wb = Nonews = Nonelock = threading.Lock() # 解决异步写入冲突def open_spider(self, spider):# 初始化Excel工作簿self.wb = Workbook()self.ws = self.wb.activeself.ws.title = '豆瓣图书数据'# 表头与样式设置headers = ['书名', '作者', '出版社', '出版日期', '价格', '评分', '评价人数', '简介', '封面URL', '详情页链接']for col, header in enumerate(headers, start=1):cell = self.ws.cell(row=1, column=col, value=header)cell.font = Font(bold=True)cell.alignment = Alignment(horizontal='center')# 列宽适配column_widths = [20, 25, 20, 15, 10, 8, 12, 50, 40, 40]for col, width in enumerate(column_widths, start=1):self.ws.column_dimensions[chr(64+col)].width = widthdef process_item(self, item, spider):# 加锁写入数据with self.lock:row = self.ws.max_row + 1data = [item['book_name'], item['author'], item['publisher'], item['publish_date'], item['price'],item['rating'], item['comment_count'], item['intro'], item['cover_url'], item['detail_url']]for col, value in enumerate(data, start=1):self.ws.cell(row=row, column=col, value=value)return itemdef close_spider(self, spider):# 保存Excel文件self.wb.save('豆瓣图书全量数据.xlsx')print('数据已保存到:豆瓣图书全量数据.xlsx')
(4)项目核心配置(settings.py)
重点配置反爬策略、Pipeline、中间件:
# 反爬配置
ROBOTSTXT_OBEY = False # 禁用robots协议
COOKIES_ENABLED = False # 禁用Cookie(豆瓣反爬关键)
DOWNLOAD_DELAY = 5 # 访问延迟5秒
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'# 默认请求头(模拟浏览器访问)
DEFAULT_REQUEST_HEADERS = {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8','Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8','Referer': 'https://book.douban.com/'
}# 启用Pipeline(Excel写入)
ITEM_PIPELINES = {'douban_book_spider.pipelines.DoubanBookExcelPipeline': 300,
}# 启用代理中间件
DOWNLOADER_MIDDLEWARES = {'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,'douban_book_spider.middlewares.ProxyMiddleware': 544,
}# 日志配置
LOG_LEVEL = 'INFO'
LOG_FILE = 'douban_book_spider.log'# 其他基础配置
BOT_NAME = 'douban_book_spider'
SPIDER_MODULES = ['douban_book_spider.spiders']
NEWSPIDER_MODULE = 'douban_book_spider.spiders'
FEED_EXPORT_ENCODING = 'utf-8-sig' # 解决Excel中文乱码
(5)代理中间件(middlewares.py)
class ProxyMiddleware:def process_request(self, request, spider):request.meta['proxy'] = 'http://127.0.0.1:7890'# 代理失效时重试def process_exception(self, request, exception, spider):return scrapy.Request(url=request.url,callback=request.callback,headers=request.headers,meta=request.meta,dont_filter=True)
4. Scrapyd部署与调度
(1)启动Scrapyd服务
# 在项目根目录执行
scrapyd
启动成功后访问 http://localhost:6800 可看到Scrapyd管理界面。
(2)部署爬虫到Scrapyd
# 项目根目录执行(确保scrapy.cfg配置正确)
scrapyd-deploy localhost -p douban_book_spider
部署成功提示:Deployed project "douban_book_spider" version 1
(3)调度爬虫运行
# 命令行调度(推荐)
curl http://localhost:6800/schedule.json -d project=douban_book_spider -d spider=book_spider
成功响应:{"status": "ok", "jobid": "xxx"}
三、实战踩坑实录(6个核心问题+解决方案)
坑1:Scrapyd界面看不到部署的项目
现象
执行 scrapyd-deploy 后,访问 http://localhost:6800 未找到 douban_book_spider 项目。
原因
部署命令未在 Scrapy项目根目录 执行(根目录需包含 scrapy.cfg 文件)。
解决方案
- 切换到项目根目录(通过
cd命令):cd D:\Desktop\js\douban_book_spider - 重新执行部署命令,确保终端显示
Deployed project成功提示。
坑2:爬虫请求豆瓣返回403 Forbidden
现象
日志显示 Crawled (403) <GET https://book.douban.com/top250?start=0>,爬虫直接终止。
原因
豆瓣反爬机制识别出爬虫:① 遵守robots协议;② 缺少必要请求头;③ 未禁用Cookie;④ IP被限制。
解决方案(逐步递进)
- 禁用robots协议:
ROBOTSTXT_OBEY = False - 禁用Cookie:
COOKIES_ENABLED = False - 添加完整请求头(
DEFAULT_REQUEST_HEADERS) - 增大访问延迟:
DOWNLOAD_DELAY = 5
坑3:Pipeline类未找到(NameError)
现象
日志报错:Module 'douban_book_spider.pipelines' has no attribute 'DoubanBookExcelPipeline'
原因
settings.py 中启用了Pipeline,但 pipelines.py 中未定义对应的类(或类名拼写错误)。
解决方案
- 确保
pipelines.py中存在DoubanBookExcelPipeline类(复制完整代码,避免遗漏); - 检查
settings.py中Pipeline类名与文件中完全一致(大小写、拼写无差异):ITEM_PIPELINES = {'douban_book_spider.pipelines.DoubanBookExcelPipeline': 300, }
坑4:openpyxl模块缺失(ModuleNotFoundError)
现象
日志报错:ModuleNotFoundError: No module named 'openpyxl'
原因
openpyxl 安装到了系统Python环境,而非爬虫使用的虚拟环境。
解决方案
- 激活虚拟环境(终端前缀显示虚拟环境名称):
d:\desktop\js\crawler\Scripts\activate - 在虚拟环境中重新安装:
pip install openpyxl==3.1.2 - 验证安装:
pip list | findstr openpyxl(显示版本即成功)。
坑5:Windows终端编码错误(UnicodeEncodeError)
现象
日志报错:UnicodeEncodeError: 'gbk' codec can't encode character '\u2705'
原因
Windows终端默认编码为GBK,无法解析特殊符号(如 ✅)和部分Unicode字符。
解决方案
- 删除代码中特殊符号(如Pipeline的打印语句中的
✅); - 简化打印语句为纯中文:
# 原代码(报错) print(f'✅ 数据已保存到:豆瓣图书全量数据.xlsx') # 修改后 print('数据已保存到:豆瓣图书全量数据.xlsx')
坑6:start_requests方法废弃警告
现象
日志警告:ScrapyDeprecationWarning: douban_book_spider.spiders.book_spider.BookSpider defines the deprecated start_requests() method
原因
Scrapy 2.13+ 推荐用 start() 方法替代旧的 start_requests() 方法(旧方法仍可使用,但未来会废弃)。
解决方案
将 start_requests() 改为异步协程 start():
# 替换前
def start_requests(self):# 分页URL生成逻辑pass# 替换后
async def start(self):# 分页URL生成逻辑(代码不变)pass
四、项目优化建议
1. 断点续爬
启用Scrapy的 JOBDIR 配置,支持爬虫中断后继续爬取:
# settings.py中添加
JOBDIR = 'job_info' # 断点信息保存目录
2. 数据去重
通过图书名称+作者去重,避免重复数据:
# Pipeline中添加去重逻辑
def __init__(self):self.book_set = set() # 存储已爬取的(书名+作者)组合def process_item(self, item, spider):key = (item['book_name'], item['author'])if key not in self.book_set:self.book_set.add(key)# 写入Excel逻辑return itemreturn DropItem(f'Duplicate item: {key}')
3. 分布式爬取
在多台服务器部署Scrapyd,通过共享Redis队列分发任务,提高爬取效率(需结合 scrapy-redis 扩展)。
五、总结
本项目通过 Scrapyd+Scrapy 实现了豆瓣图书数据的异步爬取与部署调度,核心难点在于突破豆瓣的反爬机制和解决环境配置问题。通过实战我们总结出以下关键经验:
- 环境一致性是基础:始终在虚拟环境中操作,避免依赖包版本冲突和路径问题;
- 反爬策略循序渐进:从禁用Cookie、添加请求头,逐步提高爬取成功率;
- 日志是排坑关键:Scrapy和Scrapyd的日志会详细记录错误原因,重点关注
ERROR和CRITICAL级别日志; - 代码规范避坑:类名、配置项拼写一致,避免因细节错误导致项目运行失败。
最终实现的爬虫可稳定爬取豆瓣图书Top250全量数据,生成结构化Excel文件,可直接用于数据分析或二次开发。如果需要扩展爬取范围(如豆瓣全分类图书),只需修改分页URL生成逻辑,即可实现快速扩展。
