1. 项目概述:爬取豆瓣电影Top 250的技术价值
爬取豆瓣电影Top 250这个经典项目,几乎是每个爬虫学习者的必经之路。我十年前第一次用Python写爬虫时,就是从豆瓣电影开始的。这个项目看似简单,但完整实现需要处理反爬机制、数据清洗、存储优化等多个技术环节,特别适合用来掌握Scrapy框架的核心用法。
为什么选择Scrapy而不是Requests?当需要爬取多个页面时,Scrapy的异步处理能力能让效率提升5-10倍。我实测过,用Requests顺序抓取250条电影数据需要3分钟左右,而Scrapy只需要20秒。更重要的是,Scrapy内置的中间件和管道机制,能优雅地处理这个项目中会遇到的三个关键难点:豆瓣的访问频率限制、动态加载的内容、以及数据存储的规范化。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与项目创建
2.1 基础环境配置
建议使用Python 3.8+版本,这个版本对异步IO的支持已经非常成熟。我习惯用virtualenv创建隔离环境:
bash复制python -m venv douban_env
source douban_env/bin/activate # Linux/Mac
douban_env\Scripts\activate # Windows
安装必要的包:
bash复制pip install scrapy scrapy-playwright fake-useragent
注意:这里特意加入了scrapy-playwright,因为豆瓣的部分页面元素是通过JavaScript动态加载的,传统爬取方式会漏掉这些内容。
2.2 创建Scrapy项目
执行以下命令初始化项目结构:
bash复制scrapy startproject douban_top250
cd douban_top250
scrapy genspider movie "movie.douban.com/top250"
生成的项目结构中,有几个关键文件需要特别关注:
items.py- 定义数据结构middlewares.py- 处理反爬策略pipelines.py- 数据存储处理settings.py- 爬虫配置
3. 核心爬虫逻辑实现
3.1 分析页面结构
打开豆瓣电影Top250页面,按F12进入开发者工具观察:
- 每页显示25部电影
- 分页通过?start=参数控制
- 电影详情页URL在class为"hd"的div下的a标签href属性中
关键数据位置:
- 电影名称:
<span class="title"> - 评分:
<span class="rating_num"> - 评价人数:
<div class="star">下的最后一个span - 经典台词:
<span class="inq">(可能不存在)
3.2 编写Items
在items.py中定义数据结构:
python复制import scrapy
class DoubanItem(scrapy.Item):
rank = scrapy.Field() # 排名
title = scrapy.Field() # 中文名
original_title = scrapy.Field() # 原名
rating = scrapy.Field() # 评分
votes = scrapy.Field() # 评价人数
quote = scrapy.Field() # 经典台词
url = scrapy.Field() # 详情页链接
cover_url = scrapy.Field() # 封面图链接
directors = scrapy.Field() # 导演
casts = scrapy.Field() # 主演
year = scrapy.Field() # 年份
genres = scrapy.Field() # 类型
countries = 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']
custom_settings = {
'DOWNLOAD_DELAY': 2,
'CONCURRENT_REQUESTS_PER_DOMAIN': 1,
'USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
def parse(self, response):
movies = response.css('.item')
for movie in movies:
item = DoubanItem()
item['rank'] = movie.css('.pic em::text').get()
item['title'] = movie.css('.title::text').get()
item['original_title'] = movie.css('.title::text').getall()[1].strip() if len(movie.css('.title::text').getall()) > 1 else ''
item['rating'] = movie.css('.rating_num::text').get()
item['votes'] = movie.css('.star span::text').re(r'(\d+)人评价')[0]
item['quote'] = movie.css('.inq::text').get('')
item['url'] = movie.css('.hd a::attr(href)').get()
item['cover_url'] = movie.css('.pic img::attr(src)').get()
# 进入详情页获取更多信息
yield scrapy.Request(url=item['url'],
callback=self.parse_detail,
meta={'item': item})
# 处理分页
next_page = response.css('.next a::attr(href)').get()
if next_page:
yield scrapy.Request(url=urljoin(response.url, next_page),
callback=self.parse)
def parse_detail(self, response):
item = response.meta['item']
info = response.css('#info')
item['directors'] = info.css('span:contains("导演") + span a::text').getall()
item['casts'] = info.css('span:contains("主演") + span a::text').getall()[:3] # 只取前三位主演
item['year'] = info.re_first(r'<span class="year">\((\d+)\)</span>')
item['genres'] = info.css('span[property="v:genre"]::text').getall()
item['countries'] = info.re(r'<span class="pl">制片国家/地区:</span>(.*?)<br>')[0].strip().split(' / ') if info.re(r'<span class="pl">制片国家/地区:</span>(.*?)<br>') else []
yield item
4. 反爬策略与优化
4.1 应对反爬机制
豆瓣有比较完善的反爬系统,我们需要在middlewares.py中添加以下策略:
python复制from urllib.parse import urlparse
import random
from fake_useragent import UserAgent
class RandomUserAgentMiddleware:
def process_request(self, request, spider):
ua = UserAgent()
request.headers['User-Agent'] = ua.random
request.headers['Referer'] = 'https://www.douban.com/'
request.headers['Host'] = urlparse(request.url).netloc
class ProxyMiddleware:
def process_request(self, request, spider):
# 这里使用本地代理池示例,实际使用时需要替换为真实代理
proxy_list = [
'http://127.0.0.1:8080',
'http://127.0.0.1:8888'
]
request.meta['proxy'] = random.choice(proxy_list)
在settings.py中启用中间件:
python复制DOWNLOADER_MIDDLEWARES = {
'douban_top250.middlewares.RandomUserAgentMiddleware': 543,
'douban_top250.middlewares.ProxyMiddleware': 544,
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
}
4.2 处理动态内容
对于JavaScript渲染的内容,我们使用Playwright:
python复制from scrapy_playwright.page import PageMethod
def parse(self, response):
yield scrapy.Request(
url=response.url,
callback=self.parse,
meta={
"playwright": True,
"playwright_page_methods": [
PageMethod("wait_for_selector", ".item"),
],
}
)
在settings.py中添加:
python复制DOWNLOAD_HANDLERS = {
"http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
"https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
}
5. 数据存储与导出
5.1 数据清洗管道
在pipelines.py中添加数据清洗逻辑:
python复制import pymongo
from itemadapter import ItemAdapter
class DoubanPipeline:
def process_item(self, item, spider):
# 清洗评分数据
item['rating'] = float(item['rating']) if item['rating'] else 0.0
# 清洗评价人数
item['votes'] = int(item['votes'].replace(',', '')) if item['votes'] else 0
# 处理空值
for field in item.fields:
item.setdefault(field, '')
return item
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_DATABASE', 'douban')
)
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]
def close_spider(self, spider):
self.client.close()
def process_item(self, item, spider):
self.db['top250'].update_one(
{'url': item['url']},
{'$set': dict(item)},
upsert=True
)
return item
5.2 导出为CSV和JSON
在settings.py中配置:
python复制FEEDS = {
'top250.csv': {
'format': 'csv',
'fields': ['rank', 'title', 'original_title', 'rating', 'votes', 'year'],
'overwrite': True
},
'top250.json': {
'format': 'json',
'encoding': 'utf8',
'overwrite': True
}
}
ITEM_PIPELINES = {
'douban_top250.pipelines.DoubanPipeline': 300,
'douban_top250.pipelines.MongoPipeline': 800,
}
6. 运行与调试技巧
6.1 启动爬虫
使用以下命令启动爬虫:
bash复制scrapy crawl movie -s JOBDIR=crawls/movie
提示:JOBDIR参数可以保存爬取状态,当爬虫意外中断时可以恢复
6.2 常见问题解决
-
403 Forbidden错误
- 解决方案:更换User-Agent,增加请求间隔
- 修改settings.py:
python复制DOWNLOAD_DELAY = 3 RANDOMIZE_DOWNLOAD_DELAY = True
-
数据缺失问题
- 原因:页面结构变化或动态加载失败
- 检查点:
- 使用
scrapy shell <url>测试选择器 - 启用Playwright处理动态内容
- 使用
-
数据库连接失败
- 检查MongoDB服务是否启动
- 确认settings.py中的MONGO_URI配置正确
6.3 性能优化建议
- 使用
CONCURRENT_REQUESTS控制并发数,建议设置为3-5 - 对高频变化的字段建立数据库索引:
python复制self.db['top250'].create_index([('rating', pymongo.DESCENDING)]) - 使用缓存中间件减少重复请求:
python复制HTTPCACHE_ENABLED = True HTTPCACHE_EXPIRATION_SECS = 86400 # 1天
7. 项目扩展方向
这个基础项目可以进一步扩展:
- 情感分析:对电影评论进行情感倾向分析
- 推荐系统:基于用户评分数据构建推荐模型
- 可视化展示:用Echarts等库展示电影数据分布
- 定时任务:设置定期爬取跟踪排名变化
python复制# 示例:使用APScheduler定时运行爬虫
from apscheduler.schedulers.twisted import TwistedScheduler
from scrapy.crawler import CrawlerProcess
from scrapy.utils.project import get_project_settings
scheduler = TwistedScheduler()
process = CrawlerProcess(get_project_settings())
@scheduler.scheduled_job('cron', hour=3)
def scheduled_job():
process.crawl('movie')
process.start()
scheduler.start()
在实际项目中,我通常会添加日志记录和错误报警机制,当爬虫异常时可以及时收到通知。对于需要长期运行的爬虫,建议使用Scrapy的Telnet控制台或Scrapyd服务进行监控和管理。
