在数据采集场景中,异步爬虫是提高效率的核心方案,而 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. 环境准备
首先安装依赖包(建议使用虚拟环境,避免版本冲突):
bash
# 创建虚拟环境(Windows)
python -m venv crawler-env
# 激活虚拟环境
crawler-env\Scripts\activate
# 安装核心依赖
pip install scrapy scrapyd scrapyd-client openpyxl fake-useragent
2. 项目结构搭建
bash
# 创建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)
python
import scrapy
class 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() # 封面URL
detail_url = scrapy.Field() # 详情页链接
(2)爬虫核心逻辑(book_spider.py)
实现分页爬取、详情页提取逻辑,支持异步请求:
python
import scrapy
from douban_book_spider.items import DoubanBookSpiderItem
class 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开始,每次+25
for 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 '未知价格'
# 评分、评价人数、简介、封面URL
item['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_url
yield item
(3)Excel写入Pipeline(pipelines.py)
使用线程锁避免异步写入冲突,设置Excel样式:
python
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment
import threading
class DoubanBookExcelPipeline:
wb = None
ws = None
lock = threading.Lock() # 解决异步写入冲突
def open_spider(self, spider):
# 初始化Excel工作簿
self.wb = Workbook()
self.ws = self.wb.active
self.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 = width
def process_item(self, item, spider):
# 加锁写入数据
with self.lock:
row = self.ws.max_row + 1
data = [
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 item
def close_spider(self, spider):
# 保存Excel文件
self.wb.save('豆瓣图书全量数据.xlsx')
print('数据已保存到:豆瓣图书全量数据.xlsx')
(4)项目核心配置(settings.py)
重点配置反爬策略、Pipeline、中间件:
python
# 反爬配置
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)
python
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服务
bash
# 在项目根目录执行
scrapyd
启动成功后访问 http://localhost:6800 可看到Scrapyd管理界面。
(2)部署爬虫到Scrapyd
bash
# 项目根目录执行(确保scrapy.cfg配置正确)
scrapyd-deploy localhost -p douban_book_spider
部署成功提示:Deployed project "douban_book_spider" version 1
(3)调度爬虫运行
bash
# 命令行调度(推荐)
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命令):bashcd 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类名与文件中完全一致(大小写、拼写无差异):pythonITEM_PIPELINES = { 'douban_book_spider.pipelines.DoubanBookExcelPipeline': 300, }
坑4:openpyxl模块缺失(ModuleNotFoundError)
现象
日志报错:ModuleNotFoundError: No module named 'openpyxl'
原因
openpyxl 安装到了系统Python环境,而非爬虫使用的虚拟环境。
解决方案
-
激活虚拟环境(终端前缀显示虚拟环境名称):
bashd:\desktop\js\crawler\Scripts\activate -
在虚拟环境中重新安装:
bashpip install openpyxl==3.1.2 -
验证安装:
pip list | findstr openpyxl(显示版本即成功)。
坑5:Windows终端编码错误(UnicodeEncodeError)
现象
日志报错:UnicodeEncodeError: 'gbk' codec can't encode character '\u2705'
原因
Windows终端默认编码为GBK,无法解析特殊符号(如 ✅)和部分Unicode字符。
解决方案
-
删除代码中特殊符号(如Pipeline的打印语句中的
✅); -
简化打印语句为纯中文:
python# 原代码(报错) 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():
python
# 替换前
def start_requests(self):
# 分页URL生成逻辑
pass
# 替换后
async def start(self):
# 分页URL生成逻辑(代码不变)
pass
四、项目优化建议
1. 断点续爬
启用Scrapy的 JOBDIR 配置,支持爬虫中断后继续爬取:
python
# settings.py中添加
JOBDIR = 'job_info' # 断点信息保存目录
2. 数据去重
通过图书名称+作者去重,避免重复数据:
python
# 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 item
return DropItem(f'Duplicate item: {key}')
3. 分布式爬取
在多台服务器部署Scrapyd,通过共享Redis队列分发任务,提高爬取效率(需结合 scrapy-redis 扩展)。
五、总结
本项目通过 Scrapyd+Scrapy 实现了豆瓣图书数据的异步爬取与部署调度,核心难点在于突破豆瓣的反爬机制和解决环境配置问题。通过实战我们总结出以下关键经验:
- 环境一致性是基础:始终在虚拟环境中操作,避免依赖包版本冲突和路径问题;
- 反爬策略循序渐进:从禁用Cookie、添加请求头,逐步提高爬取成功率;
- 日志是排坑关键 :Scrapy和Scrapyd的日志会详细记录错误原因,重点关注
ERROR和CRITICAL级别日志; - 代码规范避坑:类名、配置项拼写一致,避免因细节错误导致项目运行失败。
最终实现的爬虫可稳定爬取豆瓣图书Top250全量数据,生成结构化Excel文件,可直接用于数据分析或二次开发。如果需要扩展爬取范围(如豆瓣全分类图书),只需修改分页URL生成逻辑,即可实现快速扩展。