1. 项目概述:Scrapy爬取豆瓣电影Top250实战指南
在数据驱动的互联网时代,掌握网络爬虫技术已成为数据分析师、产品经理乃至运营人员的必备技能。作为Python生态中最专业的爬虫框架,Scrapy凭借其异步处理、中间件扩展和管道机制,能够高效稳定地获取结构化数据。本次我们选择豆瓣电影Top250作为目标,不仅因为其丰富的电影元数据(评分、导演、主演、短评等),更因其适中的反爬机制非常适合练手——既有基本的Cookie验证和请求频率限制,又不会像电商平台那样设置复杂的动态加密。
我曾用这套方法为三个影视分析项目采集基础数据,最快能在15分钟内完成全量抓取。相比直接调用API或购买商业数据集,自主爬取能获得更原始的网页信息(如用户生成的真实短评),这对后续的情感分析和内容挖掘至关重要。下面将分享从环境搭建到数据存储的完整流程,包含我处理豆瓣反爬的7个实战技巧。
2. 核心工具与原理解析
2.1 Scrapy框架架构剖析
Scrapy采用经典的"蜘蛛-引擎-调度器"架构(如图1)。当我们在命令行执行scrapy crawl douban时:
- 引擎从Spider获取初始Request对象
- 调度器将Request排队并过滤重复URL
- 下载器通过Downloader Middlewares发送HTTP请求
- 返回的Response经Spider Middlewares传递给Spider解析
- 生成的结果Item通过Item Pipeline持久化存储
这种异步流水线设计使得Scrapy的单机QPS能达到300+,远高于Requests+BeautifulSoup的传统方案。以豆瓣为例,配置CONCURRENT_REQUESTS=32时,完整爬取Top250仅需约80秒(含2秒延迟)。
2.2 豆瓣页面的技术特点
通过Chrome开发者工具分析豆瓣电影详情页,可以发现三个关键特征:
- 服务端渲染:核心数据直接嵌入HTML,无需处理JavaScript动态加载
- 数据层级:
html复制<div class="article"> <ol class="grid_view"> <li> <!-- 每个电影条目 --> <div class="info"> <span class="title">肖申克的救赎</span> <div class="star"> <span class="rating_num">9.7</span> </div> </div> </li> </ol> </div> - 反爬策略:
- Cookie验证(未登录状态只能访问前25条)
- 请求头检测(缺少Referer会返回418)
- 频率限制(连续请求间隔需≥2秒)
3. 完整实现步骤
3.1 环境准备与项目初始化
首先创建虚拟环境并安装依赖:
bash复制python -m venv douban_env
source douban_env/bin/activate # Linux/Mac
pip install scrapy fake-useragent scrapy-redis
初始化Scrapy项目:
bash复制scrapy startproject douban_top250
cd douban_top250
scrapy genspider movie movie.douban.com
目录结构关键文件说明:
code复制douban_top250/
├── scrapy.cfg
└── douban_top250/
├── items.py # 定义数据结构
├── middlewares.py # 反爬处理
├── pipelines.py # 数据存储
└── settings.py # 全局配置
3.2 定义数据模型(items.py)
根据页面元素设计Item字段:
python复制import scrapy
class DoubanItem(scrapy.Item):
rank = scrapy.Field() # 排名
title = scrapy.Field() # 中文名
orig_title = scrapy.Field() # 原名
rating = scrapy.Field() # 评分
votes = scrapy.Field() # 评价人数
genres = scrapy.Field() # 类型
directors = scrapy.Field() # 导演
actors = scrapy.Field() # 主演
year = scrapy.Field() # 年份
regions = scrapy.Field() # 国家/地区
quote = scrapy.Field() # 经典台词
link = scrapy.Field() # 详情页链接
3.3 编写爬虫核心逻辑(movie.py)
python复制import scrapy
from douban_top250.items import DoubanItem
from urllib.parse import urljoin
class MovieSpider(scrapy.Spider):
name = 'movie'
allowed_domains = ['movie.douban.com']
start_urls = ['https://movie.douban.com/top250']
def parse(self, response):
for movie in response.css('ol.grid_view li'):
item = DoubanItem()
item['rank'] = movie.css('em::text').get()
title_elem = movie.css('span.title::text').get()
item['title'] = title_elem.split('/')[0].strip()
item['orig_title'] = title_elem.split('/')[-1].strip()
item['rating'] = movie.css('span.rating_num::text').get()
item['votes'] = movie.css('div.star>span:last-child::text').re_first(r'(\d+)')
yield item
next_page = response.css('span.next>a::attr(href)').get()
if next_page:
yield response.follow(urljoin(response.url, next_page), self.parse)
3.4 反爬策略配置(settings.py)
关键配置项:
python复制USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
DOWNLOAD_DELAY = 2 # 请求间隔(秒)
CONCURRENT_REQUESTS = 4 # 并发数
COOKIES_ENABLED = True # 启用Cookie
# 中间件配置
DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
'scrapy_user_agents.middlewares.RandomUserAgentMiddleware': 400,
}
# 管道配置
ITEM_PIPELINES = {
'douban_top250.pipelines.DoubanPipeline': 300,
}
4. 高级技巧与问题排查
4.1 突破分页限制的三种方法
当爬取到第3页(50-75名)时可能会遇到403错误,解决方案:
-
模拟登录法:
python复制def start_requests(self): login_url = 'https://accounts.douban.com/login' yield scrapy.FormRequest( login_url, formdata={'email':'你的账号', 'password':'密码'}, callback=self.after_login ) def after_login(self, response): if "验证码" in response.text: print("需要处理验证码!") yield from super().start_requests() -
Cookie池轮询:
python复制custom_cookies = [ {'name':'key1', 'value':'value1'}, {'name':'key2', 'value':'value2'} ] -
代理IP池(推荐):
python复制class ProxyMiddleware: def process_request(self, request, spider): request.meta['proxy'] = 'http://proxy_ip:port'
4.2 数据清洗的常见问题
原始数据可能包含:
- 评分中的空白字符(
9.7→9.7) - 多国别拼接(
美国 / 法国→['美国', '法国']) - 导演姓名中的职务标注(
弗兰克·德拉邦特 导演→弗兰克·德拉邦特)
推荐使用item_loaders处理:
python复制from itemloaders.processors import TakeFirst, MapCompose
from scrapy.loaders import ItemLoader
def clean_text(text):
return text.strip().replace('\xa0', ' ')
class MovieLoader(ItemLoader):
default_output_processor = TakeFirst()
title_in = MapCompose(clean_text)
genres_in = MapCompose(clean_text, lambda x: x.split('/'))
4.3 存储方案选型对比
| 存储方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| CSV | 无需数据库,Excel可直接打开 | 无索引,查询效率低 | 快速验证数据 |
| MySQL | 支持复杂查询,事务安全 | 需要维护数据库 | 结构化数据分析 |
| MongoDB | 灵活Schema,适合非结构化数据 | 占用存储空间大 | 用户评论等文本数据 |
| JSON Lines | 易与Python生态集成 | 不支持增量更新 | 机器学习特征工程 |
推荐MySQL存储实现:
python复制import pymysql
class DoubanPipeline:
def __init__(self):
self.conn = pymysql.connect(
host='localhost',
user='root',
password='',
db='douban',
charset='utf8mb4'
)
self.cursor = self.conn.cursor()
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS movies (
id INT AUTO_INCREMENT PRIMARY KEY,
rank INT,
title VARCHAR(100),
orig_title VARCHAR(100),
rating FLOAT,
votes INT,
year INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
def process_item(self, item, spider):
sql = """
INSERT INTO movies (rank, title, orig_title, rating, votes, year)
VALUES (%s, %s, %s, %s, %s, %s)
"""
self.cursor.execute(sql, (
item['rank'],
item['title'],
item['orig_title'],
item['rating'],
item['votes'],
item['year']
))
self.conn.commit()
return item
5. 法律合规与伦理考量
虽然豆瓣电影Top250是公开数据,但需注意:
- 严格遵守
robots.txt协议(豆瓣允许爬取电影目录但禁止短评) - 设置合理爬取间隔(建议≥5秒/页)
- 数据仅用于个人学习,禁止商业用途
- 在存储的数据中保留来源声明
重要提示:大规模爬取前建议联系网站方获取API权限。根据《数据安全法》,非法获取超过5万条公民个人信息可能构成犯罪。
我在实际项目中会添加自动限速功能:
python复制class SpeedLimitMiddleware:
def process_request(self, request, spider):
time.sleep(random.uniform(3, 7))
6. 项目扩展方向
完成基础爬取后,可以尝试:
- 增量爬虫:记录最后爬取时间,只获取新增条目
python复制last_crawl_date = redis.get('last_crawl') if item['update_time'] > last_crawl_date: yield item - 分布式扩展:使用Scrapy-Redis搭建集群
python复制SCHEDULER = "scrapy_redis.scheduler.Scheduler" DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" - 情感分析:对短评进行LDA主题建模
python复制from sklearn.feature_extraction.text import CountVectorizer vectorizer = CountVectorizer(max_df=0.95, min_df=2) - 可视化展示:用Pyecharts生成评分分布热力图
这套方法稍作修改即可迁移到其他场景:
- 将选择器
span.title改为div.title可适配知乎热榜 - 调整分页逻辑后能爬取京东商品列表
- 添加Selenium中间件可应对动态加载的淘宝页面
最后分享一个调试技巧:在parse方法中添加print(response.text[:500])可以快速验证选择器是否生效。当遇到403错误时,先检查请求头是否包含完整的Referer和Cookie信息。
