引言
你是否遇到过这样的场景?想爬取豆瓣电影Top250的完整数据(电影名、评分、导演、上映时间),用requests+BeautifulSoup
写了200行代码,却被以下问题困扰:
- 手动管理请求队列,并发效率低;
- 频繁请求被封IP,需手动切换代理;
- 数据提取逻辑分散,清洗和存储代码混杂;
- 遇到JS动态加载的页面,无法直接解析。
这些问题的解决方案,藏在Python爬虫的“瑞士军刀”——Scrapy框架中。它不仅能帮你高效管理请求、解析数据,还内置了反爬应对机制。本文将通过“豆瓣电影Top250”的完整爬取案例,从架构原理到实战代码,带你掌握Scrapy的核心功能,并学会用中间件和管道突破反爬限制。
一、Scrapy架构解析:理解“爬虫引擎”的工作流程
1.1 Scrapy的五大核心组件
Scrapy是一个基于事件驱动的高性能分布式爬虫框架,其核心架构由五大组件组成(附数据流图):
组件 | 职责 |
---|---|
引擎(Engine) | 核心调度器,协调各组件工作(类似“总指挥”) |
调度器(Scheduler) | 管理请求队列(Request Queue),按优先级或调度策略分发请求 |
下载器(Downloader) | 发送HTTP请求,获取响应(Response),支持异步IO(如Twisted框架) |
爬虫(Spider) | 定义爬取逻辑(URL规则、数据提取规则),生成初始请求(Start Requests) |
管道(Pipeline) | 处理提取的数据(清洗、去重、存储到数据库/文件) |
中间件(Middleware) | 扩展组件,包括“下载中间件”(处理请求/响应)和“爬虫中间件”(处理Spider输入输出) |
1.2 典型工作流程(从发起请求到存储数据)
- 引擎从Spider获取初始请求(如豆瓣Top250的首页URL);
- 引擎将请求发送到调度器,加入队列;
- 调度器取出请求,通过引擎转发给下载器;
- 下载器发送HTTP请求,获取响应后返回给引擎;
- 引擎将响应发送给Spider解析;
- Spider从响应中提取数据(Item)和新的请求(Request);
- 数据(Item)被发送到管道处理(清洗、存储);
- 新请求被发送回调度器,重复步骤3-7,直到队列为空。
关键点:Scrapy通过“异步非阻塞”机制(基于Twisted)实现高并发,单台机器可轻松处理每秒数百个请求。
二、实战:用Scrapy爬取豆瓣电影Top250(完整项目)
2.1 环境准备与项目创建
2.1.1 安装Scrapy
pip install scrapy # 安装最新版(本文基于Scrapy 2.10)
2.1.2 创建Scrapy项目
scrapy startproject douban_movie # 创建项目“douban_movie”
cd douban_movie
scrapy genspider top250 movie.douban.com # 生成Spider(名称top250,起始域movie.douban.com)
项目结构如下(关键文件):
douban_movie/
├── douban_movie/
│ ├── __init__.py
│ ├── items.py # 定义数据结构(Item)
│ ├── middlewares.py # 中间件实现
│ ├── pipelines.py # 管道实现
│ ├── settings.py # 全局配置(如并发数、下载延迟)
│ └── spiders/
│ ├── __init__.py
│ └── top250.py # 自定义Spider
2.2 步骤1:定义数据结构(Item)
在items.py
中定义需要提取的字段(如电影名、评分、导演):
import scrapy
class DoubanMovieItem(scrapy.Item):
rank = scrapy.Field() # 排名(1-250)
title = scrapy.Field() # 电影名(如《肖申克的救赎》)
rating = scrapy.Field() # 评分(如9.7)
director = scrapy.Field() # 导演(如弗兰克·德拉邦特)
release_year = scrapy.Field() # 上映年份(如1994)
url = scrapy.Field() # 电影详情页URL
2.3 步骤2:编写Spider(核心爬取逻辑)
在spiders/top250.py
中实现Spider,定义如何发起请求、解析响应:
import scrapy
from douban_movie.items import DoubanMovieItem
class Top250Spider(scrapy.Spider):
name = 'top250' # Spider名称(唯一标识)
allowed_domains = ['movie.douban.com'] # 允许爬取的域名
start_urls = ['https://movie.douban.com/top250?start=0'] # 初始URL(第一页)
def parse(self, response):
"""解析列表页,提取电影数据和下一页链接"""
# 定位每部电影的HTML块(使用XPath)
movie_blocks = response.xpath('//div[@class="item"]')
for block in movie_blocks:
item = DoubanMovieItem()
# 提取排名(如第1名)
item['rank'] = block.xpath('.//div[@class="pic"]/em/text()').get()
# 提取电影名(处理中文和别名)
item['title'] = ''.join(block.xpath('.//span[@class="title"]/text()').getall())
# 提取评分(如9.7)
item['rating'] = block.xpath('.//span[@class="rating_num"]/text()').get()
# 提取导演(格式:导演: 弗兰克·德拉邦特)
info = block.xpath('.//div[@class="bd"]/p[1]/text()').get().strip()
item['director'] = info.split('导演: ')[1].split(' ')[0] # 分割提取导演名
# 提取上映年份(格式:1994 / 美国 / 剧情 犯罪)
item['release_year'] = info.split('\n')[1].strip().split(' / ')[0]
# 提取详情页URL
item['url'] = block.xpath('.//div[@class="pic"]/a/@href').get()
yield item # 将Item发送到管道
# 提取下一页链接(如start=25)
next_page = response.xpath('//span[@class="next"]/a/@href').get()
if next_page:
next_url = response.urljoin(next_page) # 拼接完整URL
yield scrapy.Request(next_url, callback=self.parse) # 递归请求下一页
2.4 步骤3:使用管道(Pipeline)清洗并存储数据
在pipelines.py
中实现数据清洗和存储逻辑(以保存到MySQL为例):
import pymysql
from scrapy.exceptions import DropItem
class DoubanMoviePipeline:
def __init__(self):
# 连接MySQL(需提前创建数据库和表)
self.conn = pymysql.connect(
host='localhost',
user='root',
password='123456',
database='douban_movie',
charset='utf8mb4'
)
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
"""处理每个Item(清洗+存储)"""
# 数据清洗:检查必填字段是否缺失
if not item['title'] or not item['rating']:
raise DropItem(f"缺失关键数据:{item}")
# 存储到MySQL
sql = """
INSERT INTO top250 (rank, title, rating, director, release_year, url)
VALUES (%s, %s, %s, %s, %s, %s)
"""
self.cursor.execute(sql, (
item['rank'],
item['title'],
item['rating'],
item['director'],
item['release_year'],
item['url']
))
self.conn.commit()
return item
def close_spider(self, spider):
"""爬虫结束时关闭连接"""
self.cursor.close()
self.conn.close()
2.5 步骤4:配置中间件(突破反爬限制)
豆瓣对爬虫有严格的反爬机制(如检测User-Agent、限制请求频率),需通过中间件绕过:
2.5.1 随机User-Agent中间件(应对浏览器指纹检测)
在middlewares.py
中添加:
import random
from scrapy.downloadermiddlewares.useragent import UserAgentMiddleware
class RandomUserAgentMiddleware(UserAgentMiddleware):
"""随机切换User-Agent"""
def __init__(self, user_agent=''):
self.user_agent = user_agent
# 常用User-Agent列表(可扩展)
self.user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15',
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1'
]
def process_request(self, request, spider):
request.headers.setdefault('User-Agent', random.choice(self.user_agents))
2.5.2 配置中间件和反爬参数(在settings.py
中)
# 启用自定义User-Agent中间件(禁用默认的)
DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
'douban_movie.middlewares.RandomUserAgentMiddleware': 543,
}
# 其他反爬配置
DOWNLOAD_DELAY = 2 # 下载延迟(2秒/请求,避免被封IP)
CONCURRENT_REQUESTS = 4 # 并发请求数(降低压力)
AUTOTHROTTLE_ENABLED = True # 自动限速(Scrapy内置)
AUTOTHROTTLE_START_DELAY = 1
AUTOTHROTTLE_MAX_DELAY = 5
2.6 步骤5:运行爬虫并验证结果
scrapy crawl top250 # 启动爬虫
运行后,数据会自动存储到MySQL的top250
表中。通过Navicat等工具查看,可看到类似以下记录:
rank | title | rating | director | release_year | url |
---|---|---|---|---|---|
1 | 肖申克的救赎 | 9.7 | 弗兰克·德拉邦特 | 1994 | https://movie.douban.com/subject/1292052/ |
2 | 霸王别姬 | 9.6 | 陈凯歌 | 1993 | https://movie.douban.com/subject/1291546/ |
三、Scrapy进阶技巧:中间件与管道的深度应用
3.1 下载中间件:处理请求与响应的“必经之路”
除了随机User-Agent,下载中间件还可实现:
- 代理IP切换:从代理池(如芝麻代理)获取IP,动态设置
request.meta['proxy']
; - 处理Cookies:模拟登录(如豆瓣需要登录才能查看完整数据);
- 异常重试:对403、503等错误响应,自动重试请求。
示例:代理IP中间件
class ProxyMiddleware:
def process_request(self, request, spider):
# 从代理池API获取随机代理(需替换为实际接口)
proxy = get_proxy_from_pool() # 假设返回"http://123.45.67.89:8080"
request.meta['proxy'] = proxy
3.2 管道:数据清洗与存储的“最后一公里”
管道可实现:
- 去重:使用
Redis
或set
记录已爬取的URL,避免重复存储; - 数据验证:检查评分是否为0-10之间的数值;
- 批量存储:将数据缓存到列表,达到一定数量后批量插入数据库(提升性能)。
示例:Redis去重管道
import redis
class RedisDupePipeline:
def __init__(self):
self.redis = redis.Redis(host='localhost', port=6379, db=0)
def process_item(self, item, spider):
url = item['url']
if self.redis.sismember('douban_movie_urls', url):
raise DropItem(f"重复URL:{url}")
self.redis.sadd('douban_movie_urls', url)
return item
3.3 应对动态加载:Scrapy + Selenium/Playwright
豆瓣部分页面(如短评)通过JS动态加载,需结合浏览器自动化工具:
- 在下载中间件中集成Selenium,获取渲染后的HTML;
- 或使用
scrapy-playwright
(Scrapy官方支持的异步浏览器自动化库)。
示例:Scrapy-Playwright配置
# settings.py
DOWNLOAD_HANDLERS = {
"http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
"https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
}
PLAYWRIGHT_BROWSER_TYPE = "chromium" # 使用Chrome内核
# Spider中使用
async def parse(self, response):
page = response.meta["playwright_page"] # 获取Playwright页面对象
await page.wait_for_selector('.comment-item') # 等待JS加载评论
html = await page.content() # 获取渲染后的HTML
# 解析html...
四、反爬与合规:Scrapy使用的“红线”
- 遵守robots协议:检查目标网站的
/robots.txt
(如豆瓣允许爬虫,但限制频率); - 限制请求频率:通过
DOWNLOAD_DELAY
设置合理间隔(如2-5秒); - 避免敏感数据:不爬取用户隐私(如手机号、身份证号);
- 标识爬虫身份:在请求头中添加
X-Requested-With: Scrapy
(可选,但体现合规性)。
五、总结与学习路径
Scrapy的核心优势在于标准化的爬虫开发流程和强大的扩展能力:
- 架构设计让请求管理、数据提取、存储逻辑分离,代码更易维护;
- 中间件和管道机制可灵活应对反爬、数据清洗等需求;
- 结合Selenium/Playwright可处理动态页面,覆盖90%以上的爬虫场景。
学习建议:
- 从简单网站(如豆瓣、知乎)入手,练习基础爬取;
- 尝试自定义中间件(如代理池、随机Cookies);
- 学习分布式爬取(Scrapy-Redis),应对大规模数据;
- 关注反爬技术演进(如滑动验证码、设备指纹),持续优化爬虫。
掌握Scrapy后,你将能高效完成从“简单数据采集”到“复杂反爬突破”的全流程任务——下一次面对爬虫需求时,Scrapy就是你的“效率倍增器”!