用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. 环境准备

首先安装依赖包(建议使用虚拟环境,避免版本冲突):

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 文件)。

解决方案
  1. 切换到项目根目录(通过 cd 命令):

    bash 复制代码
    cd D:\Desktop\js\douban_book_spider
  2. 重新执行部署命令,确保终端显示 Deployed project 成功提示。

坑2:爬虫请求豆瓣返回403 Forbidden

现象

日志显示 Crawled (403) <GET https://book.douban.com/top250?start=0>,爬虫直接终止。

原因

豆瓣反爬机制识别出爬虫:① 遵守robots协议;② 缺少必要请求头;③ 未禁用Cookie;④ IP被限制。

解决方案(逐步递进)
  1. 禁用robots协议:ROBOTSTXT_OBEY = False
  2. 禁用Cookie:COOKIES_ENABLED = False
  3. 添加完整请求头(DEFAULT_REQUEST_HEADERS
  4. 增大访问延迟:DOWNLOAD_DELAY = 5

坑3:Pipeline类未找到(NameError)

现象

日志报错:Module 'douban_book_spider.pipelines' has no attribute 'DoubanBookExcelPipeline'

原因

settings.py 中启用了Pipeline,但 pipelines.py 中未定义对应的类(或类名拼写错误)。

解决方案
  1. 确保 pipelines.py 中存在 DoubanBookExcelPipeline 类(复制完整代码,避免遗漏);

  2. 检查 settings.py 中Pipeline类名与文件中完全一致(大小写、拼写无差异):

    python 复制代码
    ITEM_PIPELINES = {
        'douban_book_spider.pipelines.DoubanBookExcelPipeline': 300,
    }

坑4:openpyxl模块缺失(ModuleNotFoundError)

现象

日志报错:ModuleNotFoundError: No module named 'openpyxl'

原因

openpyxl 安装到了系统Python环境,而非爬虫使用的虚拟环境。

解决方案
  1. 激活虚拟环境(终端前缀显示虚拟环境名称):

    bash 复制代码
    d:\desktop\js\crawler\Scripts\activate
  2. 在虚拟环境中重新安装:

    bash 复制代码
    pip install openpyxl==3.1.2
  3. 验证安装:pip list | findstr openpyxl(显示版本即成功)。

坑5:Windows终端编码错误(UnicodeEncodeError)

现象

日志报错:UnicodeEncodeError: 'gbk' codec can't encode character '\u2705'

原因

Windows终端默认编码为GBK,无法解析特殊符号(如 )和部分Unicode字符。

解决方案
  1. 删除代码中特殊符号(如Pipeline的打印语句中的 );

  2. 简化打印语句为纯中文:

    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 实现了豆瓣图书数据的异步爬取与部署调度,核心难点在于突破豆瓣的反爬机制和解决环境配置问题。通过实战我们总结出以下关键经验:

  1. 环境一致性是基础:始终在虚拟环境中操作,避免依赖包版本冲突和路径问题;
  2. 反爬策略循序渐进:从禁用Cookie、添加请求头,逐步提高爬取成功率;
  3. 日志是排坑关键 :Scrapy和Scrapyd的日志会详细记录错误原因,重点关注 ERRORCRITICAL 级别日志;
  4. 代码规范避坑:类名、配置项拼写一致,避免因细节错误导致项目运行失败。

最终实现的爬虫可稳定爬取豆瓣图书Top250全量数据,生成结构化Excel文件,可直接用于数据分析或二次开发。如果需要扩展爬取范围(如豆瓣全分类图书),只需修改分页URL生成逻辑,即可实现快速扩展。

相关推荐
深蓝电商API4 天前
Scrapy源码剖析:下载器中间件是如何工作的?
爬虫·scrapy
深蓝电商API6 天前
解析器的抉择:parsel vs lxml,在 Scrapy 中如何做出最佳选择?
scrapy·lxml·parsel
小白学大数据11 天前
集成Scrapy与异步库:Scrapy+Playwright自动化爬取动态内容
运维·爬虫·scrapy·自动化
深蓝电商API12 天前
爬虫性能压榨艺术:深入剖析 Scrapy 内核与中间件优化
爬虫·scrapy
B站_计算机毕业设计之家21 天前
python舆情分析可视化系统 情感分析 微博 爬虫 scrapy爬虫技术 朴素贝叶斯分类算法大数据 计算机✅
大数据·爬虫·python·scrapy·数据分析·1024程序员节·舆情分析
深兰科技21 天前
深兰科技法务大模型亮相,推动律所文书处理智能化
人工智能·scrapy·beautifulsoup·scikit-learn·pyqt·fastapi·深兰科技
龙腾AI白云24 天前
大模型-7种大模型微调方法 上
scrapy·scikit-learn·pyqt
万粉变现经纪人25 天前
如何解决 pip install -r requirements.txt 子目录可编辑安装缺少 pyproject.toml 问题
开发语言·python·scrapy·beautifulsoup·scikit-learn·matplotlib·pip
万粉变现经纪人1 个月前
如何解决 pip install -r requirements.txt 私有索引未设为 trusted-host 导致拒绝 问题
开发语言·python·scrapy·flask·beautifulsoup·pandas·pip