1. 项目背景与核心价值
爬取豆瓣电影Top250这个经典项目,几乎是每个Python爬虫学习者的必经之路。作为国内最具公信力的电影评分平台,豆瓣的榜单数据不仅反映了大众审美取向,更是影视行业研究的重要参考。但官方并未提供完整的结构化数据导出功能,这就给了我们技术人发挥的空间。
我最早在2016年就用Requests+BeautifulSoup实现过这个爬虫,后来随着反爬机制升级,陆续改用过Selenium、Pyppeteer等方案。直到系统学习Scrapy框架后,才发现用专业工具处理这类结构化数据采集,效率能提升3倍以上。这个教程会把我五年来迭代优化的实战经验全盘托出,包括:
- 如何用Scrapy内置选择器精准提取复杂页面数据
- 应对豆瓣反爬体系的7个关键策略
- 将零散数据转化为结构化数据库的完整Pipeline设计
- 生产环境中实际可用的分布式部署方案
2. 环境准备与项目初始化
2.1 基础环境配置
推荐使用Python 3.8+版本,这个区间既有稳定的异步特性支持,又能兼容主流第三方库。我实测过在3.10版本会出现某些依赖包的兼容性问题,建议用conda创建独立环境:
bash复制conda create -n douban_scrapy python=3.8
conda activate douban_scrapy
安装核心依赖时特别注意版本锁:
bash复制pip install scrapy==2.6.3 scrapy-redis==0.7.2 pandas==1.4.3
注意:Scrapy 2.6+版本对中间件机制做了优化,能更好地处理现代网站的JavaScript渲染需求,而scrapy-redis的0.7.x版本在分布式任务调度上有显著性能提升。
2.2 项目工程化创建
使用Scrapy的startproject命令创建项目骨架:
bash复制scrapy startproject douban_top250
cd douban_top250
scrapy genspider movie movie.douban.com
生成的目录结构中需要特别关注:
code复制douban_top250/
├── middlewares.py # 反爬中间件核心配置区
├── pipelines.py # 数据清洗与存储管道
├── settings.py # 全局参数控制中心
└── spiders/
└── movie.py # 爬虫逻辑主文件
建议立即在settings.py中开启以下配置:
python复制USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'
ROBOTSTXT_OBEY = False
DOWNLOAD_DELAY = 3
CONCURRENT_REQUESTS = 4
3. 页面解析与数据提取
3.1 分析页面结构
打开豆瓣Top250页面,按F12进入开发者工具。通过元素检查器可以看到每个电影条目都包裹在<div class="item">中,这个class就是我们最好的锚点。每个item内部包含:
- 电影标题:
<span class="title">(可能有中文名和原名) - 评分:
<span class="rating_num"> - 评价人数:
<div class="star">下的最后一个<span> - 经典台词:
<span class="inq">(可能不存在) - 详情页链接:
<div class="hd">中的<a>标签href属性
3.2 编写Item模型
在items.py中定义结构化字段:
python复制import scrapy
class DoubanItem(scrapy.Item):
rank = scrapy.Field() # 排名
title = scrapy.Field() # 中文标题
orig_title = scrapy.Field() # 原始标题
rating = scrapy.Field() # 评分
votes = scrapy.Field() # 评价人数
quote = scrapy.Field() # 经典台词
detail_url = scrapy.Field() # 详情页链接
cover_url = scrapy.Field() # 封面图URL
directors = scrapy.Field() # 导演列表
actors = scrapy.Field() # 主演列表
year = scrapy.Field() # 上映年份
genres = scrapy.Field() # 类型标签
countries = scrapy.Field() # 制片国家
3.3 实现解析逻辑
在movie.py中编写核心解析方法:
python复制def parse(self, response):
for item in response.css('.item'):
movie = DoubanItem()
movie['rank'] = item.css('.pic em::text').get()
titles = item.css('.title::text').getall()
movie['title'] = titles[0].strip()
movie['orig_title'] = titles[1].strip()[3:-2] if len(titles)>1 else ''
movie['rating'] = item.css('.rating_num::text').get()
movie['votes'] = item.css('.star span::text').re_first(r'(\d+)人评价')
movie['quote'] = item.css('.inq::text').get(default='').strip()
movie['detail_url'] = item.css('.hd a::attr(href)').get()
movie['cover_url'] = item.css('img::attr(src)').get()
# 发起详情页请求
if movie['detail_url']:
yield scrapy.Request(
url=movie['detail_url'],
callback=self.parse_detail,
meta={'movie': movie}
)
4. 反爬策略深度优化
4.1 请求头动态轮换
在middlewares.py中实现动态User-Agent:
python复制from fake_useragent import UserAgent
class RandomUserAgentMiddleware:
def process_request(self, request, spider):
request.headers['User-Agent'] = UserAgent().random
request.headers['Accept'] = 'text/html,application/xhtml+xml'
request.headers['Accept-Language'] = 'zh-CN,zh;q=0.9'
4.2 IP代理池集成
推荐使用付费代理服务(如快代理、站大爷),在settings.py中配置:
python复制DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 400,
'douban_top250.middlewares.RandomProxyMiddleware': 410,
}
配套中间件实现:
python复制class RandomProxyMiddleware:
def process_request(self, request, spider):
proxy = get_random_proxy() # 从代理池API获取
request.meta['proxy'] = f"http://{proxy}"
4.3 请求频率智能控制
在扩展中实现自适应延迟:
python复制from scrapy import signals
class AdaptiveDelayExtension:
@classmethod
def from_crawler(cls, crawler):
ext = cls()
crawler.signals.connect(ext.response_received, signal=signals.response_received)
return ext
def response_received(self, response, request, spider):
if response.status == 403: # 触发反爬
current_delay = request.meta.get('download_delay', 3)
spider.download_delay = min(current_delay * 1.5, 10)
5. 数据存储与管道设计
5.1 MySQL存储管道
python复制import pymysql
from itemadapter import ItemAdapter
class MySQLPipeline:
def __init__(self, host, db, user, password):
self.host = host
self.db = db
self.user = user
self.password = password
@classmethod
def from_crawler(cls, crawler):
return cls(
host=crawler.settings.get('MYSQL_HOST'),
db=crawler.settings.get('MYSQL_DB'),
user=crawler.settings.get('MYSQL_USER'),
password=crawler.settings.get('MYSQL_PASS')
)
def open_spider(self, spider):
self.conn = pymysql.connect(
host=self.host,
user=self.user,
password=self.password,
db=self.db,
charset='utf8mb4'
)
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
sql = """INSERT INTO movies (...) VALUES (...)"""
self.cursor.execute(sql, (
item['rank'],
item['title'],
# 其他字段...
))
self.conn.commit()
return item
5.2 数据去重优化
使用Scrapy内置的RFPDupeFilter配合Redis实现分布式去重:
python复制DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
SCHEDULER_PERSIST = True
REDIS_URL = 'redis://:password@127.0.0.1:6379/0'
6. 部署与监控方案
6.1 Scrapyd分布式部署
安装scrapyd服务端:
bash复制pip install scrapyd scrapyd-client
在scrapyd.conf中配置:
ini复制[scrapyd]
eggs_dir = eggs
logs_dir = logs
items_dir = items
jobs_to_keep = 100
dbs_dir = dbs
max_proc = 16
部署项目:
bash复制scrapyd-deploy --build-egg=output.egg
curl http://localhost:6800/schedule.json -d project=douban_top250 -d spider=movie
6.2 Prometheus监控
集成scrapy-prometheus插件:
python复制EXTENSIONS = {
'scrapy_prometheus.PrometheusExtension': 800,
}
配置grafana面板监控关键指标:
- 请求成功率
- 每分钟抓取条目数
- 各页面类型响应时间分布
- 代理IP可用率
7. 常见问题排查指南
7.1 403 Forbidden错误
现象:连续请求后突然返回403状态码
解决方案:
- 立即切换User-Agent和代理IP
- 临时增加下载延迟至10秒
- 检查请求头是否完整携带Referer、Cookie等字段
7.2 数据提取不全
现象:部分字段获取到空值
排查步骤:
- 使用scrapy shell命令实时测试选择器
bash复制scrapy shell 'https://movie.douban.com/top250' >>> response.css('.title::text').getall() - 检查页面是否动态加载(需开启Downloader Middleware的JS渲染支持)
- 验证XPath/CSS选择器是否随网站改版失效
7.3 数据库连接泄漏
现象:运行一段时间后出现"Too many connections"错误
预防措施:
- 在pipeline中实现连接池管理
- 使用with语句确保连接关闭
- 配置MySQL的wait_timeout参数
8. 项目扩展方向
8.1 增量爬取策略
通过记录最后爬取的时间戳,下次启动时只抓取新增条目:
python复制class MovieSpider(scrapy.Spider):
def start_requests(self):
last_crawl = self.get_last_crawl_time() # 从数据库读取
yield scrapy.Request(
url=self.start_urls[0],
meta={'last_crawl': last_crawl}
)
def parse(self, response):
for item in response.css('.item'):
update_time = self.extract_update_time(item)
if update_time > response.meta['last_crawl']:
# 处理新条目...
8.2 情感分析增强
使用SnowNLP对影评进行情感倾向分析:
python复制from snownlp import SnowNLP
def process_quote(text):
s = SnowNLP(text)
return {
'text': text,
'sentiment': s.sentiments,
'keywords': s.keywords(3)
}
8.3 可视化分析
用Pyecharts生成电影评分分布图:
python复制from pyecharts.charts import Bar
def visualize_rating(data):
bar = Bar()
bar.add_xaxis([str(i) for i in range(10)])
bar.add_yaxis("电影数量", rating_distribution)
bar.render("rating_dist.html")