1. 项目背景与核心价值
爬取豆瓣电影Top250数据是学习网络爬虫的经典练手项目。这个榜单包含了影史最受欢迎的250部电影,数据维度丰富(评分、导演、主演、类型、短评等),非常适合用来掌握Scrapy框架的核心功能。我在实际工作中曾多次用类似技术为企业抓取结构化数据,今天就把这套经过实战验证的方法完整分享出来。
相比简单requests爬虫,Scrapy的优势在于:
- 内置异步处理机制,速度提升3-5倍
- 完善的中间件系统,轻松应对反爬
- 自动化的数据管道,清洗存储一气呵成
- 可扩展的架构设计,方便后期功能增强
重要提示:爬取数据时请务必遵守robots.txt规则,控制请求频率(建议2秒/次),避免对目标服务器造成压力。本文示例代码仅用于学习交流。
2. 环境准备与项目搭建
2.1 基础环境配置
推荐使用Python 3.8+环境,这是我测试最稳定的版本。通过以下命令创建虚拟环境:
bash复制python -m venv douban_env
source douban_env/bin/activate # Linux/Mac
douban_env\Scripts\activate.bat # Windows
安装核心依赖库:
bash复制pip install scrapy==2.6.1 scrapy-user-agents==0.1.6 pandas
2.2 Scrapy项目初始化
执行项目生成命令:
bash复制scrapy startproject douban_top250
cd douban_top250
scrapy genspider movie "movie.douban.com"
生成的目录结构说明:
code复制douban_top250/
├── scrapy.cfg # 部署配置
├── douban_top250/ # 项目主目录
│ ├── __init__.py
│ ├── items.py # 数据模型定义
│ ├── middlewares.py # 中间件配置
│ ├── pipelines.py # 数据处理管道
│ ├── settings.py # 爬虫配置
│ └── spiders/ # 爬虫代码
│ └── movie.py # 生成的爬虫文件
3. 核心爬虫开发
3.1 数据模型定义(items.py)
首先明确需要抓取的数据字段:
python复制import scrapy
class DoubanItem(scrapy.Item):
rank = scrapy.Field() # 排名
title = scrapy.Field() # 电影名称
rating = scrapy.Field() # 评分
votes = scrapy.Field() # 评价人数
directors = scrapy.Field() # 导演
actors = scrapy.Field() # 主演
types = scrapy.Field() # 类型
release = scrapy.Field() # 上映时间
duration = scrapy.Field() # 片长
quote = scrapy.Field() # 经典台词
3.2 爬虫逻辑实现(spiders/movie.py)
关键点解析:
- 分页处理:Top250共有10页,每页25条
- 防反爬策略:随机User-Agent + 请求延迟
- 数据提取:混合使用CSS和XPath选择器
python复制import scrapy
from douban_top250.items import DoubanItem
import random
import time
class MovieSpider(scrapy.Spider):
name = 'movie'
allowed_domains = ['movie.douban.com']
# 自定义请求头
custom_headers = {
'Accept': 'text/html,application/xhtml+xml',
'Accept-Language': 'zh-CN,zh;q=0.9',
}
def start_requests(self):
base_url = "https://movie.douban.com/top250?start={}"
for page in range(0, 250, 25):
yield scrapy.Request(
url=base_url.format(page),
headers=self.custom_headers,
callback=self.parse
)
time.sleep(random.uniform(1, 3)) # 随机延迟
def parse(self, response):
items = response.css('.grid_view li')
for item in items:
movie = DoubanItem()
# 基础信息提取
movie['rank'] = item.css('.pic em::text').get()
movie['title'] = item.css('.title::text').get()
movie['rating'] = item.css('.rating_num::text').get()
movie['votes'] = item.css('.star span::text').re_first(r'(\d+)人评价')
# 详情页信息
detail_url = item.css('.hd a::attr(href)').get()
yield response.follow(
detail_url,
callback=self.parse_detail,
meta={'movie': movie},
headers=self.custom_headers
)
def parse_detail(self, response):
movie = response.meta['movie']
# 使用XPath处理复杂结构
info = response.xpath('//div[@id="info"]')
movie['directors'] = info.xpath('.//span[contains(text(),"导演")]/following::a[1]/text()').getall()
movie['actors'] = info.xpath('.//span[contains(text(),"主演")]/following::a/text()').getall()[:5]
movie['types'] = info.xpath('.//span[contains(text(),"类型")]/following::a/text()').getall()
# 处理混合文本
release_text = info.xpath('.//span[contains(text(),"上映日期")]/following::text()[1]').get()
movie['release'] = release_text.strip() if release_text else None
duration_text = info.xpath('.//span[contains(text(),"片长")]/following::text()[1]').get()
movie['duration'] = duration_text.strip() if duration_text else None
movie['quote'] = response.css('.related-info .indent span::text').get()
yield movie
3.3 反爬对抗策略
豆瓣有这些常见反爬机制:
- User-Agent检测
- 请求频率限制
- Cookie验证
- IP封禁
对应解决方案(middlewares.py):
python复制from scrapy.downloadermiddlewares.useragent import UserAgentMiddleware
import random
class RandomUserAgentMiddleware(UserAgentMiddleware):
def __init__(self, user_agent):
self.user_agent = user_agent
@classmethod
def from_crawler(cls, crawler):
return cls(
user_agent=crawler.settings.get('USER_AGENT_LIST')
)
def process_request(self, request, spider):
ua = random.choice(self.user_agent)
request.headers.setdefault('User-Agent', ua)
# settings.py中配置
USER_AGENT_LIST = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15...',
# 至少准备10个常见UA
]
4. 数据存储与处理
4.1 数据清洗管道(pipelines.py)
python复制import pandas as pd
from itemadapter import ItemAdapter
class DoubanPipeline:
def __init__(self):
self.data = []
def process_item(self, item, spider):
# 统一评分格式
if item.get('rating'):
item['rating'] = float(item['rating'])
# 处理多值字段
for field in ['directors', 'actors', 'types']:
if item.get(field):
item[field] = '|'.join(item[field])
self.data.append(item)
return item
def close_spider(self, spider):
df = pd.DataFrame(self.data)
df.to_excel('douban_top250.xlsx', index=False)
4.2 存储优化方案
针对不同需求场景的存储方案对比:
| 存储方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| CSV | 无需依赖,直接查看 | 不支持复杂结构 | 快速验证 |
| Excel | 可视化友好 | 大数据性能差 | 业务人员查看 |
| MySQL | 支持复杂查询 | 需要数据库环境 | 长期存储 |
| MongoDB | 灵活schema | 内存占用高 | 非结构化数据 |
推荐使用MongoDB存储的配置示例:
python复制import pymongo
class MongoPipeline:
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db
@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DB')
)
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]
def process_item(self, item, spider):
self.db['movies'].insert_one(ItemAdapter(item).asdict())
return item
def close_spider(self, spider):
self.client.close()
5. 完整配置与运行
5.1 关键配置(settings.py)
python复制BOT_NAME = 'douban_top250'
ROBOTSTXT_OBEY = True # 遵守robots协议
# 并发控制
CONCURRENT_REQUESTS = 4
DOWNLOAD_DELAY = 2.5
# 中间件配置
DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
'douban_top250.middlewares.RandomUserAgentMiddleware': 400,
}
# 管道配置
ITEM_PIPELINES = {
'douban_top250.pipelines.DoubanPipeline': 300,
# 'douban_top250.pipelines.MongoPipeline': 800,
}
# MongoDB配置
MONGO_URI = 'mongodb://localhost:27017'
MONGO_DB = 'douban'
5.2 运行与调试
启动爬虫:
bash复制scrapy crawl movie -o results.json
调试技巧:
- 使用
scrapy shell <url>交互式测试选择器 - 添加
-L INFO参数查看详细日志 - 遇到403时检查请求头是否完整
6. 常见问题解决方案
6.1 被封IP怎么办?
- 立即停止爬虫
- 切换网络环境
- 增加DOWNLOAD_DELAY到5秒以上
- 使用代理IP池(需自行搭建)
6.2 数据缺失如何处理?
- 增加重试机制:
python复制RETRY_TIMES = 3 RETRY_HTTP_CODES = [500, 502, 503, 504, 400, 403, 404] - 添加字段存在性检查:
python复制def parse_detail(self, response): movie = response.meta['movie'] if not response.css('#info'): self.logger.warning(f"详情页结构异常: {response.url}") return None
6.3 如何提高爬取效率?
- 启用并发下载:
python复制CONCURRENT_REQUESTS = 16 # 根据机器配置调整 - 使用缓存:
python复制HTTPCACHE_ENABLED = True HTTPCACHE_EXPIRATION_SECS = 86400 # 缓存1天
7. 项目扩展方向
-
数据可视化:用Pyecharts制作评分分布图、类型词云
python复制from pyecharts.charts import Bar bar = Bar().add_xaxis(types).add_yaxis("电影数量", counts) -
情感分析:对短评进行NLP处理
python复制from snownlp import SnowNLP sentiment = SnowNLP(comment).sentiments -
定时任务:使用Scrapyd+APScheduler实现自动抓取
-
分布式扩展:改用Scrapy-Redis架构
我在实际项目中发现,完整跑完Top250大约需要15-20分钟(配置2秒延迟的情况下)。如果要做商业用途,建议进一步优化这些方面:
- 使用更稳定的代理服务
- 实现验证码自动识别
- 建立异常重试机制
- 添加数据去重功能