简介:这是一套面向Python爬虫初学者与数据采集实践者的完整Scrapy项目实战资源,聚焦豆瓣电影Top250榜单的数据抓取与结构化存储。资源提供从请求调度、页面解析(XPath)、数据清洗到MySQL入库的全流程实现,涵盖反爬应对基础逻辑与可扩展架构设计,适用于课程实训、数据分析前置准备及Web爬虫入门项目开发。压缩包共26个文件,含8个核心Python模块(如Spider主爬虫、Item定义、Pipeline管道、Settings配置等)、8个XML配置与IDE元数据文件、5个编译缓存pyc及1个建表SQL脚本,整体仅14KB,轻量但结构完整,便于快速部署与二次开发。已有1757人学习下载,读者可直接运行获取250部电影的标题、主演信息、评分与经典台词等结构化数据,并基于Movie.sql一键初始化数据库表结构,显著降低爬虫工程落地门槛。
1. 项目概述与核心价值
最近在整理个人项目库,翻出了几年前写的一个豆瓣电影数据采集爬虫,当时是为了做一个电影推荐系统的数据源。这个项目用Python的Scrapy框架实现,包含了完整的爬虫逻辑、数据清洗、以及将数据持久化到MySQL数据库的流程。今天把它重新梳理了一遍,修复了一些因豆瓣页面改版导致的小问题,并把全部源代码和数据库SQL文件整理了出来。如果你正在学习Python爬虫,或者想找一个结构清晰、可直接复用的Scrapy项目来练手,这个项目应该能给你不少启发。
简单来说,这个系统能自动化地从豆瓣电影Top250、正在热映、即将上映等页面,抓取电影的名称、导演、主演、评分、评价人数、简介、标签等信息。它不仅仅是一个简单的“请求-解析”脚本,而是一个考虑了反爬策略、数据去重、错误重试和结构化存储的完整系统。对于数据分析、内容推荐或者只是想建一个个人电影库的朋友,这套代码提供了从数据获取到入库的“一条龙”解决方案。接下来,我会详细拆解这个系统的设计思路、关键技术实现以及我踩过的那些坑。
2. 系统整体架构与设计思路
2.1 为什么选择Scrapy框架?
在Python生态里,写爬虫的选择很多,从基础的requests+BeautifulSoup,到更高级的Selenium、Playwright。我选择Scrapy,主要基于几个核心考量:
第一是工程化与可维护性。当你的爬虫任务超出几十个页面,涉及到多个数据源、复杂的解析逻辑和数据处理流程时,用脚本堆砌的方式会很快变得难以维护。Scrapy强制你按照它的项目结构来组织代码,比如独立的Spider(爬虫)、Item(数据模型)、Pipeline(数据处理管道)、Middleware(中间件)。这种模块化设计,让代码逻辑清晰,后期添加新功能或修改现有逻辑都非常方便。例如,如果你想增加对豆瓣电影评论的抓取,只需要新建一个Spider,并复用已有的Item和Pipeline即可。
第二是内置的强大功能。Scrapy不是一个简单的库,而是一个框架,它为你解决了爬虫开发中80%的通用难题。比如:
- 异步处理与高性能:基于Twisted的异步网络库,可以轻松实现并发请求,爬取效率远高于同步请求。
- 请求调度与去重:内置的调度器(Scheduler)会自动管理请求队列,并默认基于请求指纹进行去重,防止重复爬取。
- 中间件扩展性:通过下载器中间件(Downloader Middleware),你可以非常方便地集成代理IP池、自定义请求头、处理Cookie、设置下载延迟等,这些都是对抗反爬的必备手段。
- 健壮的错误处理:可以灵活设置重试次数、重试延迟、忽略的HTTP状态码等,让爬虫在遇到网络波动或临时反爬时更加稳定。
第三是丰富的生态系统。围绕Scrapy有大量成熟的扩展(Extension)和中间件,比如scrapy-redis用于分布式爬取,scrapy-splash或scrapy-playwright用于处理JavaScript渲染的页面。虽然我们这个豆瓣电影爬虫暂时用不到这些高级功能,但框架本身为未来的扩展留足了空间。
注意:对于豆瓣电影这类主要以静态HTML呈现、分页规则清晰的网站,Scrapy是“杀鸡用牛刀”吗?恰恰相反,正是这种“牛刀”让你能专注于业务逻辑(解析数据),而不用在请求管理、并发控制、异常处理等底层细节上耗费精力,从长远看开发效率更高。
2.2 系统核心组件与数据流
整个系统的运行遵循Scrapy的标准数据流,我将其梳理为下图所示的流程,并附上每个环节的关键实现要点:
graph TD A[启动爬虫] --> B[Spider: 生成初始请求]; B --> C[引擎: 调度请求]; C --> D[下载器: 获取网页]; D --> E{下载成功?}; E -- 是 --> F[Spider: 解析响应, 提取数据]; E -- 否 --> G[重试/记录错误]; G --> C; F --> H[生成Items]; H --> I[Item Pipeline]; I --> J[数据清洗/验证]; J --> K[数据去重]; K --> L[写入MySQL数据库]; L --> M[爬取结束/新请求]; F --> M; M --> C;1. Spider(爬虫):这是系统的“大脑”。我定义了多个Spider类,分别对应不同的抓取目标(如DoubanTop250Spider,DoubanInTheatersSpider)。每个Spider的职责是:
- 生成起始URL(
start_urls)。 - 定义解析函数(
parse),从下载器返回的HTML响应中,使用XPath或CSS选择器提取出我们需要的电影数据字段。 - 生成
Item对象(即结构化数据),并将其yield给引擎,进入Pipeline。 - 发现并生成新的“下一页”或“详情页”请求,
yield回引擎进行调度,实现自动翻页和深度爬取。
2. Item:这是系统的“数据模型”。我定义了一个MovieItem类,它像一张数据表的蓝图,明确了我们要抓取的每个电影应该包含哪些字段,比如title(标题)、directors(导演)、rating(评分)等。这保证了数据的结构一致性。
3. Item Pipeline(数据管道):这是系统的“消化系统”。当Spider产生一个Item后,它会依次通过多个配置好的Pipeline进行处理。在我的项目中,Pipeline主要完成三件事:
- 数据清洗与验证:检查字段是否为空、格式化评分(字符串转浮点数)、处理导演/主演列表(字符串转数组)。
- 数据去重:基于电影ID或标题+年份的组合,判断该条电影记录是否已经存在于数据库或本次抓取批次中,避免重复存储。
- 数据存储:将清洗和去重后的
Item数据,通过pymysql或SQLAlchemy等库,持久化到MySQL数据库中。
4. Downloader Middleware(下载器中间件):这是系统的“外交官”和“防护盾”。我在这里集中实现了反爬策略:
- User-Agent轮换:准备一个列表,每次请求随机选择一个,模拟不同浏览器。
- 请求延迟:在
settings.py中设置DOWNLOAD_DELAY,并可以在中间件中实现更智能的随机延迟,避免请求过快被封。 - 代理IP集成(可选):虽然豆瓣对个人低频爬取相对友好,但中间件为接入代理IP池预留了接口。你可以在
process_request方法中为请求设置代理。 - Cookie处理:处理登录态或会话(本项目未涉及登录,但机制在此)。
5. 调度器与去重过滤器:Scrapy内置,我们主要通过settings.py进行配置,比如设置并发请求数(CONCURRENT_REQUESTS)、深度优先/广度优先(DEPTH_PRIORITY)等。
这个架构的好处是高内聚、低耦合。每个组件职责单一,修改一个部分(比如更换数据库从MySQL到MongoDB)几乎不会影响其他部分。你完全可以根据自己的需求,增删Pipeline,或者替换Spider的解析规则。
3. 核心实现细节与关键技术点
3.1 针对豆瓣页面的解析策略
豆瓣电影页面结构清晰,但也有一些小陷阱。我的解析策略基于XPath,因为它比CSS选择器在处理复杂嵌套节点时更灵活。
1. 定位电影条目列表:无论是Top250列表页还是搜索结果页,电影条目通常包裹在<div class="item">这样的容器里。我的首要任务是精准地定位到所有这些容器。
# 以Top250页面为例 movie_list = response.xpath('//div[@class="item"]') for movie in movie_list: # 对每个movie元素进行细粒度数据提取如果定位不准,可能会抓到无关的广告或推荐模块。务必使用浏览器的开发者工具(F12)仔细检查元素,确认选择器的唯一性。
2. 提取关键字段:在每一个电影条目容器内,再使用相对XPath提取具体信息。这里的关键是处理可能缺失的字段和数据的清洗。
- 标题与年份:标题通常在一个
<a>标签或<span>里,年份可能包含在标题字符串中,需要用正则表达式re提取出来。title = movie.xpath('.//span[@class="title"][1]/text()').get() # 获取中文名 year_str = movie.xpath('.//div[@class="bd"]/p/text()').re_first(r'(\d{4})') # 用正则从简介文本中提取年份 - 评分与评价人数:评分是数值,评价人数需要从字符串中提取数字。
rating = movie.xpath('.//span[@class="rating_num"]/text()').get() # 可能为“暂无评分”,需要处理 rating = float(rating) if rating and rating != '暂无评分' else None rating_people_text = movie.xpath('.//div[@class="star"]/span[last()]/text()').get() # 例如:“125683人评价” rating_people = int(re.search(r'(\d+)', rating_people_text).group(1)) if rating_people_text else 0 - 导演与主演:这部分信息在一个段落里,用
/分隔。需要先获取整个字符串,然后按/分割,再去除空白和“导演: ”、“主演: ”这样的前缀。info_text = movie.xpath('.//div[@class="bd"]/p/text()').get() # 示例: “导演: 弗兰克·德拉邦特 / 主演: 蒂姆·罗宾斯 / ...” # 需要复杂的字符串分割和清理逻辑
3. 处理分页:豆瓣Top250的分页规则很简单,URL参数是?start=。在解析完一页后,需要计算下一页的起始值,并构造新的请求。
current_start = int(response.url.split('start=')[-1].split('&')[0]) if 'start=' in response.url else 0 next_start = current_start + 25 if next_start < 250: # Top250只有10页 next_page_url = f'https://movie.douban.com/top250?start={next_start}' yield scrapy.Request(url=next_page_url, callback=self.parse)对于“正在热映”这类页面,分页逻辑可能不同,需要单独分析。
实操心得:豆瓣的HTML结构偶尔会有微调,所以解析规则不能写得太死。一个好的实践是,对于关键字段,使用
.get()(获取第一个)或.getall()(获取所有)方法,并做好空值判断。此外,将复杂的字符串处理逻辑(如分割导演信息)封装成独立的工具函数,能让Spider的parse方法更清晰。
3.2 数据清洗与去重策略
从网页上抓下来的数据是“脏”的,必须经过清洗才能入库。这部分工作在Item Pipeline中完成。
1. 清洗(在process_item方法中):
- 去除空白字符:对字符串字段,使用
.strip()。 - 格式转换:将评分、评价人数等字符串转换为数值类型(
float,int),转换失败时记录日志并置为None。 - 列表字段处理:导演、主演、类型等,在网页上可能是用
/分隔的字符串。在Pipeline中,我将其按分隔符分割,去除空项,最终存储为JSON字符串或数据库的SET类型(取决于数据库设计)。例如:def process_item(self, item, spider): if 'directors' in item and item['directors']: # 假设原始数据是“导演: 张三 / 李四” directors_str = item['directors'].replace('导演:', '').strip() item['directors'] = [d.strip() for d in directors_str.split('/') if d.strip()] return item - 统一编码:确保所有文本字段是统一的UTF-8编码。
2. 去重(在同一个或另一个Pipeline中):去重是保证数据质量的关键,我采用两级去重策略。
- 内存级去重(单次运行内):在Pipeline中维护一个
set,存储本次爬取已处理过的电影唯一标识(如豆瓣电影IDdouban_id)。如果当前item的ID已在集合中,则直接drop掉。这主要防止同一页面内因解析逻辑问题导致的重复。 - 数据库级去重(持久化时):这是更重要的防线。在将数据插入MySQL前,执行一次查询,检查是否已存在相同
douban_id的记录。- 方案一(查询后插入):先
SELECT,如果不存在则INSERT。逻辑简单,但每次插入前多一次查询,效率较低。 - 方案二(使用数据库特性):我推荐的方式。在MySQL中,将
douban_id字段设置为UNIQUE KEY。然后使用INSERT IGNORE或ON DUPLICATE KEY UPDATE语句。# 使用 INSERT IGNORE,如果重复则静默忽略 sql = """ INSERT IGNORE INTO movies (douban_id, title, rating, ...) VALUES (%s, %s, %s, ...) """ # 或者使用 ON DUPLICATE KEY UPDATE,如果重复则更新某些字段(如评分、评价人数) sql = """ INSERT INTO movies (douban_id, title, rating, ...) VALUES (%s, %s, %s, ...) ON DUPLICATE KEY UPDATE rating=VALUES(rating), ... """
INSERT IGNORE适合只新增不更新的场景;ON DUPLICATE KEY UPDATE适合需要更新动态数据(如评分、短评数)的场景。 - 方案一(查询后插入):先
3.3 反爬虫策略与稳健性设计
豆瓣有一定的反爬机制,虽然不如一些电商网站严格,但毫无顾忌地爬取很快就会收到403错误。我的系统从以下几个层面来应对:
1. 请求头(Headers)伪装:这是最基本也是最有效的一步。在settings.py中设置默认的DEFAULT_REQUEST_HEADERS,并务必包含一个看起来真实的User-Agent。更好的做法是在下载中间件中实现User-Agent轮换。
# 在 middlewares.py 中 import random class RandomUserAgentMiddleware: def __init__(self, user_agents): self.user_agents = user_agents @classmethod def from_crawler(cls, crawler): return cls(crawler.settings.getlist('USER_AGENTS')) def process_request(self, request, spider): request.headers['User-Agent'] = random.choice(self.user_agents)在settings.py中提供一个USER_AGENTS列表。
2. 请求速率控制:
DOWNLOAD_DELAY:设置一个基础下载延迟,例如2.0秒,表示每两次请求之间至少间隔2秒。这能显著降低被封风险。RANDOMIZE_DOWNLOAD_DELAY:设置为True,让Scrapy在DOWNLOAD_DELAY的基础上增加一个随机时间(0.5到1.5倍之间),使请求间隔更“人性化”。CONCURRENT_REQUESTS_PER_DOMAIN:限制对同一域名的并发请求数,设置为一个较小的值,如4或8。
3. 处理Cookies与会话:COOKIES_ENABLED默认是True,Scrapy会自动维护会话。对于豆瓣,保持开启即可。除非有特殊需求(如需要保持登录态),否则一般不需要手动处理。
4. 错误重试与异常处理:
RETRY_ENABLED = True:启用重试。RETRY_TIMES = 2:设置重试次数。对于403、500等错误,重试可能有用;对于404,重试无意义。RETRY_HTTP_CODES = [500, 502, 503, 504, 408, 403]:指定需要重试的HTTP状态码。谨慎添加403,因为频繁403可能是被明确封禁,重试只会加剧问题。- 在Spider中编写
errback回调函数,可以更精细地处理失败请求,比如记录日志、更换代理等。
5. 使用代理IP(高级/必要时):如果上述方法仍频繁触发反爬,就需要考虑使用代理IP池。在下载中间件的process_request方法中,可以从一个池子里获取一个代理IP,并赋值给request.meta['proxy']。
def process_request(self, request, spider): proxy = get_proxy_from_pool() # 你的代理获取函数 if proxy: request.meta['proxy'] = proxy管理代理IP池本身是一个复杂的子课题,涉及IP的获取、验证、淘汰和调度。
避坑指南:不要一开始就上代理IP。首先优化你的请求头、控制好请求频率。很多封禁是因为请求太快、太规律。将
DOWNLOAD_DELAY设为3-5秒,并开启随机延迟,往往就能解决大部分问题。过早引入代理会增加系统复杂度和不稳定因素。
4. 数据库设计与数据持久化
4.1 MySQL表结构设计
一个合理的数据库设计是数据能被有效查询和分析的基础。我为电影数据设计了核心表movies,其结构如下:
CREATE TABLE `movies` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键', `douban_id` varchar(20) NOT NULL COMMENT '豆瓣电影ID,唯一标识', `title` varchar(255) NOT NULL COMMENT '电影标题', `original_title` varchar(255) DEFAULT NULL COMMENT '原始/外文标题', `directors` json DEFAULT NULL COMMENT '导演列表,JSON格式存储', `actors` json DEFAULT NULL COMMENT '主演列表,JSON格式存储', `genres` json DEFAULT NULL COMMENT '类型列表,JSON格式存储', `rating` decimal(3,1) DEFAULT NULL COMMENT '评分,如9.3', `rating_people` int(11) DEFAULT '0' COMMENT '评分人数', `release_year` year(4) DEFAULT NULL COMMENT '上映年份', `regions` json DEFAULT NULL COMMENT '制片国家/地区列表', `summary` text COMMENT '剧情简介', `cover_url` varchar(500) DEFAULT NULL COMMENT '封面图URL', `detail_url` varchar(500) DEFAULT NULL COMMENT '豆瓣详情页URL', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_douban_id` (`douban_id`), KEY `idx_rating` (`rating`), KEY `idx_release_year` (`release_year`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电影信息表';设计要点解析:
- 唯一标识:
douban_id是豆瓣电影的唯一标识符(通常出现在URL中,如subject/1292052/),将其设为UNIQUE KEY,是实现高效去重的核心。 - 字段类型选择:
rating使用DECIMAL(3,1),足够存储像9.9这样的评分。release_year使用YEAR类型,语义清晰且节省空间。directors,actors,genres,regions这些是多值字段,我选择了MySQL 5.7+支持的JSON类型。相比用逗号分隔的字符串,JSON类型支持更复杂的查询(如查找包含“科幻”类型的电影),也比另建关联表更简单。如果数据库版本低,可以用VARCHAR存储,或用逗号分隔。
- 索引优化:除了主键和唯一索引,我还为
rating和release_year建立了普通索引。因为未来很可能需要“按评分排序”或“按年份筛选”,索引能大幅提升这类查询的速度。title字段较长,建立前缀索引需谨慎。 - 时间戳:
create_time和update_time是数据表的良好实践,便于追踪数据何时入库、何时更新。
4.2 使用Pipeline将数据写入MySQL
在Scrapy中,数据库操作通常在Item Pipeline的最后一个环节完成。我创建了一个MySQLPipeline类。
1. 连接管理:在open_spider方法中初始化数据库连接,在close_spider方法中关闭连接。这样能避免为每个Item都建立/断开连接,提升效率。
import pymysql class MySQLPipeline: def open_spider(self, spider): # 从settings.py读取数据库配置 self.conn = pymysql.connect( host=spider.settings.get('MYSQL_HOST'), user=spider.settings.get('MYSQL_USER'), password=spider.settings.get('MYSQL_PASSWORD'), database=spider.settings.get('MYSQL_DATABASE'), charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor ) self.cursor = self.conn.cursor() def close_spider(self, spider): self.cursor.close() self.conn.close()2. 数据插入逻辑(在process_item中):使用ON DUPLICATE KEY UPDATE语句实现“存在即更新,不存在则插入”的语义。这是处理电影数据(评分、评价人数会变)的常用模式。
def process_item(self, item, spider): # 假设item已经过清洗,字段与表结构对应 sql = """ INSERT INTO movies ( douban_id, title, original_title, directors, actors, genres, rating, rating_people, release_year, regions, summary, cover_url, detail_url ) VALUES ( %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s ) ON DUPLICATE KEY UPDATE rating = VALUES(rating), rating_people = VALUES(rating_people), summary = VALUES(summary), update_time = CURRENT_TIMESTAMP """ # 将item中的字段转换为适合MySQL的格式 # 例如,将Python列表转为JSON字符串 directors_json = json.dumps(item.get('directors', []), ensure_ascii=False) # ... 处理其他JSON字段 values = ( item.get('douban_id'), item.get('title'), item.get('original_title'), directors_json, actors_json, genres_json, item.get('rating'), item.get('rating_people'), item.get('release_year'), regions_json, item.get('summary'), item.get('cover_url'), item.get('detail_url') ) try: self.cursor.execute(sql, values) self.conn.commit() except pymysql.Error as e: spider.logger.error(f"Error inserting item {item.get('title')}: {e}") self.conn.rollback() return item3. 批量插入优化:如果爬取速度很快,逐条插入效率较低。可以引入批量插入机制,例如,在内存中积累一定数量(如100条)的Item后,使用executemany一次性插入。但需要注意,批量插入时如果某条数据违反唯一键约束,可能导致整批插入失败。需要根据业务容忍度设计错误处理逻辑。
5. 项目配置、运行与扩展
5.1 关键配置文件详解
Scrapy项目的核心配置在settings.py文件中。以下是我这个项目中一些关键的设置:
# settings.py BOT_NAME = 'douban_movie_crawler' SPIDER_MODULES = ['douban_movie_crawler.spiders'] NEWSPIDER_MODULE = 'douban_movie_crawler.spiders' ROBOTSTXT_OBEY = False # 豆瓣的robots.txt通常禁止爬取,根据实际情况决定是否遵守 # 并发与延迟设置(反爬核心) CONCURRENT_REQUESTS = 16 # 全局并发请求数 CONCURRENT_REQUESTS_PER_DOMAIN = 4 # 对单个域名的并发数,建议调低 DOWNLOAD_DELAY = 3 # 基础下载延迟(秒) RANDOMIZE_DOWNLOAD_DELAY = True # 启用随机延迟 AUTOTHROTTLE_ENABLED = True # 启用自动限速扩展,它会根据服务器响应动态调整延迟 AUTOTHROTTLE_START_DELAY = 5.0 # 初始延迟 AUTOTHROTTLE_MAX_DELAY = 60.0 # 最大延迟 # 重试设置 RETRY_ENABLED = True RETRY_TIMES = 2 RETRY_HTTP_CODES = [500, 502, 503, 504, 408] # 通常不包含403 # 下载中间件与Pipeline DOWNLOADER_MIDDLEWARES = { 'douban_movie_crawler.middlewares.RandomUserAgentMiddleware': 543, 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None, # 禁用默认的 } ITEM_PIPELINES = { 'douban_movie_crawler.pipelines.DataCleaningPipeline': 300, # 优先级数字越小越先执行 'douban_movie_crawler.pipelines.DuplicatesPipeline': 400, 'douban_movie_crawler.pipelines.MySQLPipeline': 500, } # 自定义配置(数据库) MYSQL_HOST = 'localhost' MYSQL_PORT = 3306 MYSQL_USER = 'your_username' MYSQL_PASSWORD = 'your_password' MYSQL_DATABASE = 'douban_movies'5.2 如何运行与部署
环境准备:
- 安装Python 3.7+。
- 安装依赖:
pip install scrapy pymysql。 - 创建MySQL数据库,并运行项目附带的
init_database.sql文件建表。
配置修改:
- 在
settings.py中填入你正确的MySQL连接信息。 - 根据你的网络环境和反爬情况,微调
DOWNLOAD_DELAY、CONCURRENT_REQUESTS_PER_DOMAIN等参数。
- 在
运行爬虫:
- 进入项目根目录。
- 运行指定Spider:
scrapy crawl douban_top250 -o top250.csv(这里-o选项可以将结果导出为CSV,方便调试,正式运行应通过Pipeline入库)。 - 运行所有Spider(如果定义了多个):可以写一个简单的Python脚本依次调用,或使用
scrapy.crawler.CrawlerProcess。
定时任务与部署:
- 本地定时:在Linux/Mac上可以使用
crontab,在Windows上可以使用“任务计划程序”,定期执行爬虫命令。 - 服务器部署:将代码部署到云服务器。除了
crontab,更专业的做法是使用进程管理工具如supervisor来管理爬虫进程,确保崩溃后能自动重启。 - 容器化:使用Docker将爬虫及其环境打包成镜像,可以更方便地在不同环境部署和扩展。
- 本地定时:在Linux/Mac上可以使用
5.3 系统扩展方向
这个基础系统可以沿多个方向扩展,以满足更复杂的需求:
爬取维度扩展:
- 电影评论与短评:新增一个
CommentSpider,从电影详情页进入评论列表页进行抓取。需要注意频率控制,评论数据量大且敏感。 - 影人详情:新增一个
CelebritySpider,抓取导演、演员的个人主页信息,构建关系网络。 - 电影标签与分类:更深入地抓取豆瓣的标签系统,用于内容分析。
- 电影评论与短评:新增一个
技术架构扩展:
- 分布式爬取:使用
scrapy-redis将调度队列和去重指纹存储到Redis中,可以让多个爬虫实例协同工作,大幅提升爬取速度。 - 处理动态内容:如果豆瓣未来将更多内容改用JavaScript加载(如评论的“展开更多”),可以集成
scrapy-playwright或scrapy-splash来渲染页面。 - 数据流集成:爬取的数据除了存入MySQL,还可以同时发送到消息队列(如Kafka),供下游的实时推荐系统或数据分析平台消费。
- 分布式爬取:使用
数据应用扩展:
- 构建推荐系统:基于电影的类型、导演、演员、评分等数据,实现简单的协同过滤或内容推荐算法。
- 数据分析与可视化:使用Pandas、Matplotlib或BI工具,分析电影评分分布、导演/演员产出规律、类型趋势等。
- 建立搜索服务:将数据导入Elasticsearch,为你的个人电影库提供全文搜索功能。
6. 常见问题与排查技巧实录
在实际运行中,你肯定会遇到各种问题。下面是我总结的一些典型问题及其解决方法。
6.1 爬虫被禁或返回403错误
这是最常见的问题。
- 症状:请求大量返回HTTP 403状态码,或者返回的HTML中包含“检测到异常请求”等提示。
- 排查与解决:
- 首先,立即停止爬虫。继续请求只会让封禁更严重。
- 检查请求头:用
scrapy shell '目标URL'命令进入交互模式,查看response.request.headers,确认User-Agent、Referer等是否设置正确且看起来像浏览器。确保你的RandomUserAgentMiddleware生效。 - 大幅降低请求频率:将
DOWNLOAD_DELAY提高到5秒甚至10秒,将CONCURRENT_REQUESTS_PER_DOMAIN降到1或2。这是最可能的原因。 - 检查Cookie:尝试在浏览器中手动访问目标页面,然后将浏览器中的Cookie复制到Scrapy的
DEFAULT_REQUEST_HEADERS中,看是否可行。这能判断是否是简单的会话验证。 - 使用代理IP:如果以上方法无效,说明你的IP可能被短期封禁。此时需要暂停爬取数小时或更换IP(重启家庭路由器可能获取新IP,或使用代理服务)。
- 模拟更真实的行为:可以尝试在中间件中随机添加
Accept-Language、Accept-Encoding等请求头。
6.2 数据解析失败或字段为空
- 症状:日志中不报错,但生成的Item中很多字段是
None或空列表。 - 排查与解决:
- 使用
scrapy shell进行调试:这是最强大的工具。在命令行输入scrapy shell '具体的电影页面URL',然后你可以交互式地测试你的XPath或CSS选择器是否正确。$ scrapy shell 'https://movie.douban.com/subject/1292052/' >>> response.xpath('//span[@property="v:itemreviewed"]/text()').get() '肖申克的救赎' - 检查页面结构是否变化:豆瓣前端偶尔会改版。用浏览器打开目标页面,查看元素,确认你使用的CSS类名或HTML结构是否依然存在。
- 处理动态加载内容:有些信息(如部分短评)可能是通过AJAX加载的。查看浏览器开发者工具的“网络”(Network)选项卡,寻找XHR请求,尝试直接抓取这些API接口的数据(通常返回JSON,更容易解析)。
- 加强解析代码的健壮性:不要假设某个元素一定存在。多用
.get()(返回None)替代旧版的.extract_first(),并对结果进行判空处理。对于可能缺失的字段,提供默认值。
- 使用
6.3 数据库写入错误
- 症状:Pipeline中抛出
pymysql.err.IntegrityError(如重复键错误)或pymysql.err.DataError(如数据太长)。 - 排查与解决:
- 重复键错误 (Duplicate entry):检查你的去重逻辑。确认
INSERT ... ON DUPLICATE KEY UPDATE语句是否正确,或者是否在插入前已经进行了内存去重但逻辑有误。检查douban_id字段是否确实唯一。 - 数据截断错误 (Data too long):检查插入的数据长度是否超过了数据库字段定义(如
VARCHAR(255))。在清洗Pipeline中,对字符串字段进行长度检查并适当截断。 - 编码错误:确保数据库、表和连接都使用
utf8mb4字符集,以支持所有Emoji和生僻字。在pymysql.connect()时明确指定charset='utf8mb4'。 - 连接超时或断开:对于长时间运行的爬虫,数据库连接可能超时。可以在Pipeline中捕获相关的异常,并尝试重新建立连接。或者使用连接池技术。
- 重复键错误 (Duplicate entry):检查你的去重逻辑。确认
6.4 性能瓶颈与优化
- 症状:爬取速度很慢,CPU或内存占用不高。
- 排查与解决:
- 瓶颈在下载延迟:如果设置了较大的
DOWNLOAD_DELAY,那么并发数再高也没用。这是为了遵守反爬策略的必要牺牲。优化方向是让爬虫在“等待”时去处理其他任务(Scrapy的异步架构已很好),或者寻找API接口替代HTML页面(通常API更快且数据更规整)。 - 瓶颈在数据库写入:如果是逐条插入,I/O会成为瓶颈。考虑实现批量插入,例如每积累100个Item执行一次
executemany。 - 启用Scrapy的统计信息:在
settings.py中设置STATS_CLASS为'scrapy.statscollectors.MemoryStatsCollector',并在close_spider时打印日志,查看item_scraped_count、downloader/request_count、downloader/response_status_count/200等指标,分析哪个环节耗时最多。
- 瓶颈在下载延迟:如果设置了较大的
这个基于Scrapy的豆瓣电影爬虫项目,从架构设计到反爬策略,从数据解析到持久化,涵盖了一个生产级爬虫的核心要素。它不仅仅是一段可运行的代码,更是一个展示了如何系统化、工程化地解决数据获取问题的范例。希望这份详细的拆解,能帮助你理解其背后的设计逻辑,并顺利搭建起属于自己的数据采集系统。在实际操作中,最宝贵的经验往往来自于解决那些意想不到的bug和反爬挑战,多动手,多调试,你的爬虫技术会越来越稳。
本文还有配套的精品资源,点击获取