1. 为什么选择Scrapy爬取豆瓣电影数据?
作为一名爬虫开发者,我最初接触豆瓣电影Top 250数据时尝试过requests+BeautifulSoup的方案,但很快遇到了三个痛点:一是反爬机制导致频繁被封IP,二是数据清洗和存储的代码量暴增,三是异步请求难以管理。直到改用Scrapy框架,这些问题才迎刃而解。
Scrapy的核心优势在于其完整的爬虫开发生命周期管理。它的架构设计包含Engine、Scheduler、Downloader、Spiders和Item Pipeline五个核心组件,这种模块化设计让爬虫开发变得异常清晰。以豆瓣电影为例,当我们需要处理分页、反爬、数据存储等复杂场景时,Scrapy的中间件系统和管道机制可以轻松实现以下功能:
- 自动限速与随机延迟(避免触发反爬)
- 请求优先级队列管理(优化爬取顺序)
- 失败请求自动重试(提高稳定性)
- 数据清洗与去重(保证数据质量)
- 多格式存储支持(JSON/CSV/MySQL等)
提示:豆瓣的反爬策略近年来持续升级,包括但不限于:请求头验证、Cookie时效控制、IP频次限制。Scrapy的Downloader Middleware可以优雅地处理这些限制。
2. 环境准备与项目初始化
2.1 基础环境配置
推荐使用Python 3.8+环境,这是目前最稳定的Scrapy支持版本。通过conda创建隔离环境是明智之选:
bash复制conda create -n douban_scrapy python=3.8
conda activate douban_scrapy
pip install scrapy scrapy-user-agents fake-useragent
关键依赖说明:
scrapy-user-agents:提供动态User-Agent轮换fake-useragent:生成真实浏览器UA字符串- 可选但建议安装的库:
pymysql(数据库存储)、scrapy-redis(分布式扩展)
2.2 项目创建与结构解析
执行标准初始化命令后,我们需要特别关注几个关键文件:
bash复制scrapy startproject douban_top250
cd douban_top250
scrapy genspider movie_spider movie.douban.com
生成的项目结构包含以下核心文件:
code复制douban_top250/
├── scrapy.cfg # 部署配置文件
└── douban_top250/ # 项目主目录
├── items.py # 数据模型定义
├── middlewares.py # 中间件配置
├── pipelines.py # 数据处理管道
├── settings.py # 爬虫配置
└── spiders/ # 爬虫代码目录
└── movie_spider.py # 核心爬虫逻辑
3. 反爬策略深度破解实战
3.1 请求头伪装技术
豆瓣会严格检测请求头完整性,以下是必须包含的headers:
python复制custom_headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Referer': 'https://movie.douban.com/top250',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-User': '?1',
}
在middlewares.py中实现动态UA:
python复制from fake_useragent import UserAgent
class RandomUserAgentMiddleware:
def process_request(self, request, spider):
request.headers['User-Agent'] = UserAgent().random
request.headers.update(spider.custom_headers)
3.2 IP代理与请求频率控制
在settings.py中配置智能限速:
python复制DOWNLOAD_DELAY = 3 # 基础延迟
AUTOTHROTTLE_ENABLED = True # 自动限速
AUTOTHROTTLE_START_DELAY = 5
AUTOTHROTTLE_MAX_DELAY = 60
CONCURRENT_REQUESTS_PER_DOMAIN = 2
# 启用代理中间件
DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 400,
'douban_top250.middlewares.RandomProxyMiddleware': 410,
}
代理池实现示例:
python复制class RandomProxyMiddleware:
def process_request(self, request, spider):
proxy = random.choice(PROXY_LIST) # 你的代理IP池
request.meta['proxy'] = f"http://{proxy}"
4. 页面解析与数据提取艺术
4.1 XPath选择器高级用法
豆瓣电影页面的HTML结构有几个关键特征:
- 每部电影包裹在
<div class="item">中 - 评分位于
<span class="rating_num"> - 评价人数用
<div class="star">下的第二个<span>
对应的XPath表达式:
python复制def parse(self, response):
for movie in response.xpath('//div[@class="item"]'):
yield {
'title': movie.xpath('.//span[@class="title"][1]/text()').get(),
'rating': movie.xpath('.//span[@class="rating_num"]/text()').get(),
'votes': movie.xpath('.//div[@class="star"]/span[last()]/text()').re_first(r'\d+'),
'quote': movie.xpath('.//span[@class="inq"]/text()').get(),
'cover_url': movie.xpath('.//img/@src').get()
}
4.2 数据清洗管道实现
在pipelines.py中添加数据标准化处理:
python复制import re
class DoubanDataCleanPipeline:
def process_item(self, item, spider):
# 处理评分数据
item['rating'] = float(item['rating']) if item['rating'] else 0.0
# 提取评价人数中的数字
item['votes'] = int(re.sub(r'\D', '', item['votes'] or '0'))
# 处理中文标题和外国标题
if isinstance(item['title'], list):
item['title_cn'] = item['title'][0]
item['title_original'] = item['title'][1] if len(item['title']) > 1 else None
return item
5. 分页爬取与异常处理
5.1 递归分页实现技巧
豆瓣Top250的分页规则是每页25条,URL格式为:
https://movie.douban.com/top250?start={offset}
在parse方法中添加分页逻辑:
python复制def parse(self, response):
# 当前页数据解析...
# 分页处理
next_offset = response.meta.get('offset', 0) + 25
if next_offset < 250: # Top250最多250条
yield scrapy.Request(
url=f'https://movie.douban.com/top250?start={next_offset}',
callback=self.parse,
meta={'offset': next_offset},
headers=self.custom_headers
)
5.2 异常处理机制
常见的异常场景及处理方案:
| 异常类型 | 触发场景 | 解决方案 |
|---|---|---|
| 403 Forbidden | 触发反爬 | 更换UA/IP,增加延迟 |
| 404 Not Found | 页面不存在 | 记录日志并跳过 |
| 503 Service Unavailable | 服务器过载 | 指数退避重试 |
| TimeoutError | 网络超时 | 自动重试3次 |
在middlewares.py中实现异常处理:
python复制class ExceptionMiddleware:
def process_exception(self, request, exception, spider):
if isinstance(exception, (TimeoutError, ConnectionError)):
new_request = request.copy()
new_request.dont_filter = True # 避免被去重
return new_request
6. 数据存储方案对比
6.1 JSON与CSV存储配置
在settings.py中启用内置导出器:
python复制FEEDS = {
'top250.json': {
'format': 'json',
'encoding': 'utf8',
'store_empty': False,
'indent': 4,
},
'top250.csv': {
'format': 'csv',
'fields': ['title', 'rating', 'votes', 'quote'],
'encoding': 'utf8',
}
}
6.2 MySQL数据库集成
完整的MySQL管道实现:
python复制import pymysql
class MySQLPipeline:
def __init__(self):
self.conn = pymysql.connect(
host='localhost',
user='root',
password='yourpassword',
db='douban',
charset='utf8mb4'
)
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
sql = """INSERT INTO movies
(title_cn, title_original, rating, votes, quote, cover_url)
VALUES (%s, %s, %s, %s, %s, %s)"""
self.cursor.execute(sql, (
item['title_cn'],
item['title_original'],
item['rating'],
item['votes'],
item['quote'],
item['cover_url']
))
self.conn.commit()
return item
def close_spider(self, spider):
self.conn.close()
7. 部署与监控进阶技巧
7.1 Scrapyd远程部署
安装和启动Scrapyd服务:
bash复制pip install scrapyd
scrapyd &
部署项目到远程服务器:
bash复制scrapy deploy default -p douban_top250
7.2 日志监控与分析
在settings.py中配置详细日志:
python复制LOG_LEVEL = 'INFO'
LOG_FILE = 'douban.log'
LOG_FORMAT = '%(asctime)s [%(name)s] %(levelname)s: %(message)s'
LOG_DATEFORMAT = '%Y-%m-%d %H:%M:%S'
使用Logstash进行日志分析:
conf复制input {
file {
path => "/path/to/douban.log"
start_position => "beginning"
}
}
filter {
grok {
match => { "message" => "%{TIMESTAMP_ISO8601:timestamp} \[%{DATA:component}\] %{LOGLEVEL:level}: %{GREEDYDATA:message}" }
}
}
8. 法律与道德边界探讨
虽然技术本身中立,但爬虫使用必须遵守robots.txt协议。豆瓣的robots.txt部分内容如下:
code复制User-agent: *
Disallow: /subject_search
Disallow: /amazon_search
Disallow: /search
Allow: /top250
关键合规要点:
- 严格遵守爬取间隔(建议≥5秒/请求)
- 不绕过任何反爬措施(如验证码破解)
- 数据仅用于个人学习与研究
- 不进行商业用途或大规模分发
在实际项目中,我会在settings.py中添加合规延迟:
python复制DOWNLOAD_DELAY = 5
RANDOMIZE_DOWNLOAD_DELAY = True
CONCURRENT_REQUESTS_PER_IP = 1
