基于Scrapy框架构建豆瓣电影数据采集系统:从爬虫到数据库存储的完整实践
2026/9/24 19:34:57 网站建设 项目流程

简介:这是一套基于Python Scrapy框架实现的豆瓣电影TOP250数据采集系统,面向Python初学者与网络爬虫入门学习者,解决结构化影视数据获取、Scrapy项目工程化搭建及MySQL数据持久化等典型实践问题。资源包共26个文件,含8个核心Python源码(如Spider主爬虫、Items定义、Pipelines管道、Settings配置)、5个编译字节码文件(pyc)、8个IDE配置与项目元数据XML文件,以及关键的Movie.sql建表语句和scrapy.cfg项目配置,整体仅14KB,轻量易读。已有1757人学习下载,适合快速理解Scrapy请求调度、XPath解析、分页翻页逻辑、数据清洗与MySQL入库全流程。读者可直接运行完整项目,获得包含片名、评分、简介、经典台词等字段的结构化电影数据,并通过SQL脚本一键初始化数据库,是掌握工业级爬虫开发范式的优质入门范例。

1. 项目概述:从零构建一个健壮的豆瓣电影数据采集系统

最近在整理个人项目库,翻到了一个几年前写的豆瓣电影爬虫,当时是为了做电影推荐系统的数据准备。重新审视和优化后,我觉得这个基于Scrapy框架的完整爬虫系统,对于想学习Python爬虫、数据采集以及后端数据处理的开发者来说,是一个非常好的练手项目。它不仅仅是一个简单的脚本,而是一个包含完整数据流(采集、清洗、存储)和工程化考虑(反爬策略、异常处理、代码结构)的系统。很多新手在学爬虫时,往往只停留在获取网页内容的层面,对于如何将零散的数据结构化、持久化,以及如何让爬虫长期稳定运行缺乏经验。这个项目正好可以填补这块空白。

这个系统能做什么?简单说,它能自动化地从豆瓣电影Top250、正在热映、即将上映等页面,抓取电影的名称、导演、演员、评分、评价人数、简介、标签等核心信息,并将这些数据清洗后存入你指定的数据库(如MySQL、SQLite)。你拿到的不再是一堆杂乱的文本文件,而是一个可以直接用于数据分析、可视化或推荐算法训练的、结构良好的数据库。无论你是想学习Scrapy框架的实战应用,还是需要一个电影数据集来做数据分析项目,或者想了解如何设计一个可维护的爬虫系统,这个项目都能提供直接的参考。

2. 核心需求与系统设计思路拆解

2.1 为什么选择Scrapy而不是Requests?

很多Python爬虫入门教程都是从Requests库开始的,它简单直接,对于小规模、简单的页面抓取非常有效。但当我们面对像豆瓣电影这样具有一定反爬措施、需要采集多个关联页面、并且希望代码易于维护和扩展的场景时,Scrapy框架的优势就凸显出来了。

Scrapy是一个为爬取网站数据、提取结构化数据而设计的应用程序框架。它内置了异步处理、请求调度、数据管道(Item Pipeline)、中间件等组件。这意味着你不用从零开始写网络请求队列、不用自己处理复杂的异常重试逻辑、也不用手动管理数据的清洗和存储流程。框架已经为你搭好了舞台,你只需要专注于编写爬取规则(Spider)和数据提取逻辑(Selectors)。对于豆瓣电影这种需要翻页、需要进入详情页抓取更多信息的任务,Scrapy的CrawlSpider配合LinkExtractor可以非常优雅地实现,代码简洁且高效。

另一个关键点是工程化。用Requests写脚本,通常所有逻辑都堆在一个文件里,随着功能增加,代码会变得难以阅读和维护。而Scrapy的项目结构是标准化的(spiders,items.py,pipelines.py,middlewares.py,settings.py),天然支持模块化开发。数据定义、爬虫逻辑、数据处理、配置相互分离,这不仅让代码更清晰,也便于团队协作和后期功能扩展。

2.2 系统架构与数据流设计

一个健壮的爬虫系统,其核心设计思想应该是“高内聚、低耦合”。我们将整个数据采集流程分解为几个独立的阶段,每个阶段负责单一职责。

  1. 调度与下载器(Scrapy Engine & Downloader):这是Scrapy框架的核心。引擎负责控制所有模块的数据流,根据调度器的请求,调用下载器去互联网抓取网页。我们需要在settings.py中配置下载延迟、并发数、User-Agent池等参数,以模拟人类行为,应对反爬。

  2. 爬虫模块(Spiders):这是我们的业务逻辑核心。在这里,我们定义起始URL(如豆瓣电影Top250列表页),并编写解析函数。解析函数有两个主要任务:一是使用XPath或CSS选择器从当前页面提取我们需要的数据(即生成Item);二是发现新的链接(如“下一页”的链接,或每个电影的详情页链接),并生成新的Request对象交给引擎去调度。对于豆瓣电影,我们通常需要两级抓取:先抓列表页获取电影基本信息和详情页链接,再进入详情页抓取更丰富的信息如剧情简介、更多演员、标签等。

  3. 数据管道(Item Pipeline):爬虫提取出的原始数据(Item)会依次通过多个配置好的管道进行处理。这是进行数据清洗和持久化的关键环节。典型的管道包括:

    • 数据清洗管道:检查字段是否为空、去除字符串首尾空格、统一日期格式、处理特殊字符等。
    • 去重管道:基于电影ID或URL,确保同一条数据不会被重复存储。我们可以利用Scrapy内置的dupefilter,也可以在管道中自己实现基于数据库的判重逻辑。
    • 数据库存储管道:这是本项目的重点。我们将清洗后的数据,通过pymysqlsqlite3等库,写入到MySQL或SQLite数据库中。这里需要设计合理的数据库表结构。
  4. 数据存储层(Database):我们选择关系型数据库(如MySQL)来存储数据,主要是因为电影数据是高度结构化的,并且后续可能需要进行复杂的查询和分析(例如,“找出评分高于8.5分的喜剧片”)。在项目附带的SQL文件中,会包含创建数据库和表的语句。一个基本的movies表可能包含以下字段:id(主键,可使用豆瓣电影ID)、titledirectorsactorsratingrating_countgenrespubdatesummary等。

整个数据流就像一条流水线:URL经过调度进入下载器,下载的网页交给爬虫解析,解析出的数据项流入管道进行清洗和存储,最终整齐地码放在数据库的货架上。这种设计使得任何一个环节出问题(如下载失败、页面结构变化),都容易定位和修复,而不会影响其他环节。

3. 环境准备与项目初始化

3.1 Python与依赖库安装

首先确保你安装了Python 3.6或更高版本。可以通过命令行输入python --version来检查。接下来,我们需要安装Scrapy框架和数据库驱动。

强烈建议使用虚拟环境来管理项目依赖,这可以避免不同项目间的库版本冲突。使用venv创建虚拟环境:

# 在项目目录下 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate

激活虚拟环境后,命令行提示符前通常会显示(venv)。接下来安装核心依赖:

pip install scrapy

Scrapy是核心框架。根据你选择的数据库,安装相应的驱动:

# 如果使用MySQL pip install pymysql # 如果使用SQLite(Python标准库自带,无需额外安装) # 如果需要更强大的连接池等功能,也可以安装 # pip install scrapy-redis # 用于分布式爬虫,非必需

注意事项:安装pymysql时如果遇到关于Microsoft Visual C++的错误,通常是因为缺少编译环境。最简单的解决办法是访问[一个非官方的Windows二进制包网站],下载对应Python版本和系统位数的pymysql.whl文件,然后通过pip install 文件名.whl进行安装。对于初学者,如果觉得MySQL配置麻烦,可以优先使用SQLite,它无需安装服务器,所有数据存储在一个本地文件中,非常适合学习和测试。

3.2 创建Scrapy项目与基础结构

在项目目录下,使用Scrapy命令行工具创建项目骨架:

scrapy startproject douban_movie cd douban_movie

这会生成一个标准的Scrapy项目目录结构:

douban_movie/ ├── scrapy.cfg # 项目部署配置文件 └── douban_movie/ # 项目Python模块 ├── __init__.py ├── items.py # 定义数据模型(Item) ├── middlewares.py # 自定义中间件(如代理、User-Agent) ├── pipelines.py # 数据管道,用于清洗和存储 ├── settings.py # 项目全局设置 └── spiders/ # 爬虫文件存放目录 └── __init__.py

这个结构是我们所有代码的容器。items.py定义了我们要抓取的数据字段,pipelines.py是数据处理的流水线,settings.py是控制爬虫行为的中枢,而spiders目录下则是我们编写具体抓取逻辑的地方。

3.3 数据库设计与SQL文件解析

在开始写爬虫代码前,我们先设计好数据的归宿。这里以MySQL为例,提供一个简单的表结构设计。项目附带的SQL文件init_database.sql通常包含如下内容:

-- 创建数据库 CREATE DATABASE IF NOT EXISTS `douban_movie` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE `douban_movie`; -- 创建电影信息表 CREATE TABLE IF NOT EXISTS `movies` ( `id` int(11) NOT NULL COMMENT '豆瓣电影ID,作为主键', `title` varchar(255) NOT NULL COMMENT '电影标题', `directors` varchar(500) DEFAULT NULL COMMENT '导演,多个用/分隔', `actors` text COMMENT '主演,多个用/分隔', `rating` decimal(3,1) DEFAULT NULL COMMENT '评分,如9.2', `rating_count` int(11) DEFAULT NULL COMMENT '评分人数', `genres` varchar(255) DEFAULT NULL COMMENT '类型,多个用/分隔', `pubdate` varchar(100) DEFAULT NULL COMMENT '上映日期,可能包含多个地区', `summary` text COMMENT '剧情简介', `tags` varchar(500) DEFAULT NULL 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 '记录创建时间', PRIMARY KEY (`id`), KEY `idx_rating` (`rating`), KEY `idx_genres` (`genres`(191)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='电影信息表';

设计思路解析

  • id:使用豆瓣电影自身的ID(如1292052对应《肖申克的救赎》),具有唯一性,适合作为主键。这比自增ID更有业务意义,也便于后续根据ID更新数据。
  • directors,actors,genres,tags:这些字段在豆瓣页面上通常是多个值。我们选择用特定分隔符(如/)将它们拼接成一个字符串存储,而不是设计多个关联表。这样做的好处是简单、查询方便(例如用LIKE '%喜剧%'查找喜剧片),缺点是违反了数据库第一范式,且无法高效地进行多值查询(如“找出同时是喜剧和爱情的电影”)。对于这个爬虫项目的目标——数据采集和初步分析,这种设计是权衡之下的合理选择。如果后续需要做复杂的关联分析,可以再将其规范化。
  • rating: 使用DECIMAL(3,1)类型,可以精确存储一位小数的评分。
  • pubdate:存储为字符串,因为豆瓣的上映日期格式不统一,可能包含“2023-05-01(中国大陆)”这样的信息。
  • create_time:记录数据插入数据库的时间,用于区分不同批次抓取的数据。

你可以使用MySQL客户端(如MySQL Workbench, Navicat)或命令行执行这个SQL文件来创建数据库和表。对于SQLite,语法略有不同,但逻辑相似,项目源代码中通常会提供适配SQLite的版本或配置。

4. 核心爬虫逻辑实现详解

4.1 定义数据模型(Items)

items.py中,我们定义爬虫要抓取的数据结构。这相当于为我们的数据定义一个强类型的容器。

import scrapy class DoubanMovieItem(scrapy.Item): # 定义电影数据项的各字段 douban_id = scrapy.Field() # 豆瓣ID title = scrapy.Field() # 标题 directors = scrapy.Field() # 导演 actors = scrapy.Field() # 演员 rating = scrapy.Field() # 评分 rating_count = scrapy.Field() # 评分人数 genres = scrapy.Field() # 类型 pubdate = scrapy.Field() # 上映日期 summary = scrapy.Field() # 简介 tags = scrapy.Field() # 标签 cover_url = scrapy.Field() # 封面图 detail_url = scrapy.Field() # 详情页链接

scrapy.Field()对象可以接受元数据,但在这个简单模型中,我们只需声明字段即可。在爬虫的解析函数中,我们会实例化这个Item类,并为每个字段赋值。

4.2 编写爬虫主体(Spider)

我们在spiders目录下创建一个新文件,比如top250_spider.py。这里我们以抓取豆瓣电影Top250为例,因为它结构清晰,适合教学。

import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from douban_movie.items import DoubanMovieItem import re class Top250Spider(CrawlSpider): name = 'top250' # 爬虫的唯一标识,运行爬虫时使用 allowed_domains = ['movie.douban.com'] # 限制爬取的域名 start_urls = ['https://movie.douban.com/top250'] # 起始URL # 定义规则:从列表页提取电影详情页链接,并指定回调函数 rules = ( Rule(LinkExtractor(restrict_css='div.hd > a'), callback='parse_movie_detail', follow=False), Rule(LinkExtractor(restrict_css='span.next > a'), follow=True), # 处理“下一页” ) def parse_movie_detail(self, response): """ 解析电影详情页,提取具体信息。 """ item = DoubanMovieItem() item['detail_url'] = response.url # 提取豆瓣ID(从URL中正则匹配) match = re.search(r'subject/(\d+)/', response.url) if match: item['douban_id'] = match.group(1) # 使用CSS选择器提取信息 item['title'] = response.css('h1 span::text').get() # 电影标题 item['directors'] = '/'.join(response.css('a[rel="v:directedBy"]::text').getall()) # 导演 item['actors'] = '/'.join(response.css('.actor a[rel="v:starring"]::text').getall()[:5]) # 主要演员,取前5个 item['rating'] = response.css('strong.ll.rating_num::text').get() # 评分 # 评分人数需要处理掉“人评价”这几个字 rating_count_text = response.css('div.rating_sum span::text').get() if rating_count_text: item['rating_count'] = int(rating_count_text.replace('人评价', '').strip()) item['genres'] = '/'.join(response.css('span[property="v:genre"]::text').getall()) # 类型 item['pubdate'] = '/'.join(response.css('span[property="v:initialReleaseDate"]::text').getall()) # 上映日期 item['summary'] = response.css('span[property="v:summary"]::text').get('').strip() # 简介 # 标签可能在另一个区块 item['tags'] = '/'.join(response.css('.tags-body .tag::text').getall()) item['cover_url'] = response.css('#mainpic img::attr(src)').get() # 封面图URL yield item # 将Item返回给引擎,进入Pipeline处理

代码逻辑解析

  1. 继承CrawlSpider:这是Scrapy中用于处理规则化链接爬取的强大Spider。我们通过rules元组来定义链接提取和跟踪规则。
  2. LinkExtractor:用于从页面中自动提取符合特定条件的链接。
    • 第一条规则:使用CSS选择器div.hd > a提取每个电影条目的详情页链接。callback='parse_movie_detail'指定了处理这些链接的回调函数。follow=False表示不继续跟踪从这些详情页里提取的链接(避免无限深入)。
    • 第二条规则:提取“下一页”的链接(span.next > a)。follow=True表示Scrapy会跟进这些链接,并在新页面上继续应用所有规则。这样就实现了自动翻页。
  3. parse_movie_detail函数:这是数据提取的核心。我们使用CSS选择器(也可以用XPath)来定位页面元素并提取文本。选择器的编写需要分析豆瓣电影详情页的HTML结构。
    • 字段提取:例如,response.css('h1 span::text').get()获取<h1>标签内<span>的文本内容,即电影标题。.getall()返回所有匹配结果的列表,我们用'/'.join(...)将其合并为一个字符串。
    • 数据清洗:在提取过程中直接进行简单清洗。例如,评分人数文本是“2000000人评价”,我们用.replace().strip()去掉“人评价”并转换为整数。
    • 正则匹配:豆瓣ID从URL中提取更可靠,使用re.search进行匹配。
    • yield item:这是关键。它将填充好的Item对象抛给Scrapy引擎,引擎会将其送入配置好的Item Pipeline进行处理。使用yield而非return,使得这个函数成为一个生成器,可以逐条产出数据,节省内存。

实操心得:编写选择器时,浏览器的开发者工具(F12)是你的最佳伙伴。使用“检查元素”功能,可以直观地看到目标元素的HTML结构和可能的CSS路径。尽量选择具有唯一性、稳定性的属性(如id,class,property)来定位元素,避免使用可能变化的索引位置。豆瓣页面的property="v:xxx"这类属性是遵循微格式的,通常比较稳定。

4.3 反爬策略与中间件配置

豆瓣有基本的反爬机制,过于频繁的访问会导致IP被暂时封锁或返回验证码。我们需要在settings.py中配置一些参数来模拟人类行为,并可能编写自定义中间件。

基础反爬配置(settings.py)

# 遵守robots.txt协议,对于学习项目可以设为False以抓取更多内容,但生产环境建议遵守 ROBOTSTXT_OBEY = False # 配置默认的请求头,特别是User-Agent DEFAULT_REQUEST_HEADERS = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', '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' } # 下载延迟,单位秒。设置一个合理的值,如1-3秒,避免请求过快 DOWNLOAD_DELAY = 2 # 自动限速扩展,根据服务器响应动态调整延迟 AUTOTHROTTLE_ENABLED = True AUTOTHROTTLE_START_DELAY = 2 AUTOTHROTTLE_MAX_DELAY = 10 AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0 # 启用Cookies(有时需要维持会话) COOKIES_ENABLED = True # 并发请求数,不要设置太高 CONCURRENT_REQUESTS = 4 # 对单个网站的最大并发数 CONCURRENT_REQUESTS_PER_DOMAIN = 2

自定义User-Agent中间件(middlewares.py): 单一的User-Agent容易被识别。我们可以创建一个中间件,每次请求时随机从列表中选一个。

import random from douban_movie.settings import USER_AGENT_LIST # 需要在settings.py中定义这个列表 class RandomUserAgentMiddleware: """随机User-Agent中间件""" def process_request(self, request, spider): ua = random.choice(USER_AGENT_LIST) if ua: request.headers['User-Agent'] = ua # 不要return任何值,让请求继续处理

然后在settings.py中启用这个中间件,并定义USER_AGENT_LIST

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 ...', # ... 添加更多常见的浏览器User-Agent字符串 ] DOWNLOADER_MIDDLEWARES = { 'douban_movie.middlewares.RandomUserAgentMiddleware': 543, # 数值越小优先级越高 }

注意事项:对于更严格的反爬,可能需要使用IP代理池。但搭建和维护代理池成本较高,对于豆瓣这类对个人学习相对友好的网站,通过控制请求频率、使用随机User-Agent、合理设置DOWNLOAD_DELAYCONCURRENT_REQUESTS,通常就能稳定运行。如果遇到封禁,可以暂停一段时间(如半小时)再继续。切勿在短时间内发起海量请求,这不仅不道德,也容易导致你的IP被永久封禁。

5. 数据清洗与数据库存储管道实现

爬虫抓取到的原始数据往往存在缺失、格式不一致、包含无关字符等问题。数据管道(Pipeline)就是负责在数据入库前进行最后一道清洗和格式化,并处理存储逻辑。

5.1 数据清洗管道

我们在pipelines.py中创建一个清洗管道。Scrapy的管道是按settings.py中定义的顺序执行的。

class DataCleanPipeline: """ 数据清洗管道:负责清洗和格式化Item中的字段。 """ def process_item(self, item, spider): # 1. 处理空值和默认值 for field in item.fields: if field not in item or item[field] is None: item[field] = '' # 或者根据字段类型设置默认值,如0.0 for rating # 2. 清洗特定字段 # 清洗评分:确保是数字,并保留一位小数 if item['rating']: try: item['rating'] = float(item['rating']) except ValueError: item['rating'] = 0.0 # 清洗评分人数:转换为整数 if item['rating_count']: if isinstance(item['rating_count'], str): # 移除逗号等千位分隔符 item['rating_count'] = int(item['rating_count'].replace(',', '')) # 如果已经是int,则保持 # 3. 字符串字段去除首尾空格 string_fields = ['title', 'directors', 'actors', 'genres', 'pubdate', 'summary', 'tags'] for field in string_fields: if item.get(field): item[field] = item[field].strip() # 4. 处理简介中的换行和多余空格 if item['summary']: # 将多个换行和空格合并为一个空格 import re item['summary'] = re.sub(r'\s+', ' ', item['summary']) return item # 必须返回item,传递给下一个管道

这个管道做了几件事:确保所有字段都有值(即使是空字符串)、将评分和评分人数转换为正确的数值类型、去除字符串字段的无用空格、清理简介文本的格式。这些操作能极大提高后续数据分析和使用的便利性。

5.2 数据库存储管道

清洗后的数据需要存入数据库。我们创建一个专门的存储管道。这里以MySQL为例,使用pymysql库。

import pymysql from itemadapter import ItemAdapter class MySQLPipeline: """ MySQL存储管道:将Item存入MySQL数据库。 """ def __init__(self, mysql_host, mysql_db, mysql_user, mysql_password): self.mysql_host = mysql_host self.mysql_db = mysql_db self.mysql_user = mysql_user self.mysql_password = mysql_password self.connection = None self.cursor = None @classmethod def from_crawler(cls, crawler): # 从settings.py读取数据库配置 return cls( mysql_host=crawler.settings.get('MYSQL_HOST', 'localhost'), mysql_db=crawler.settings.get('MYSQL_DATABASE', 'douban_movie'), mysql_user=crawler.settings.get('MYSQL_USER', 'root'), mysql_password=crawler.settings.get('MYSQL_PASSWORD', ''), ) def open_spider(self, spider): """爬虫启动时,建立数据库连接。""" self.connection = pymysql.connect( host=self.mysql_host, user=self.mysql_user, password=self.mysql_password, db=self.mysql_db, charset='utf8mb4', # 重要!支持存储Emoji等四字节字符 cursorclass=pymysql.cursors.DictCursor ) self.cursor = self.connection.cursor() spider.logger.info(f"成功连接到MySQL数据库: {self.mysql_db}") def close_spider(self, spider): """爬虫关闭时,关闭数据库连接。""" if self.connection: self.connection.close() spider.logger.info("MySQL数据库连接已关闭。") def process_item(self, item, spider): # 使用ItemAdapter获取字典形式的item数据 adapter = ItemAdapter(item) data = adapter.asdict() # 构建SQL插入语句,使用ON DUPLICATE KEY UPDATE实现“存在则更新” # 假设豆瓣ID是主键或唯一索引 placeholders = ', '.join(['%s'] * len(data)) columns = ', '.join(data.keys()) updates = ', '.join([f"{k}=VALUES({k})" for k in data.keys() if k != 'douban_id']) # 主键不更新 sql = f""" INSERT INTO movies ({columns}) VALUES ({placeholders}) ON DUPLICATE KEY UPDATE {updates} """ try: self.cursor.execute(sql, list(data.values())) self.connection.commit() spider.logger.debug(f"成功插入/更新电影: {item.get('title')} (ID: {item.get('douban_id')})") except pymysql.Error as e: spider.logger.error(f"插入数据失败: {e}, SQL: {sql}, Data: {data}") self.connection.rollback() # 发生错误时回滚 # 可以选择抛出DropItem异常来丢弃这个item,或者不抛出让爬虫继续 # raise DropItem(f"MySQL error: {e}") return item

关键点解析

  1. from_crawler类方法:这是Scrapy管道的标准初始化方式,用于从项目设置(settings.py)中读取配置信息(如数据库连接参数)。这样可以将配置与代码分离,更灵活。
  2. open_spiderclose_spider:这两个方法分别在爬虫开始和结束时被Scrapy自动调用。我们在open_spider中建立数据库连接,在close_spider中关闭连接,确保资源正确管理。
  3. ON DUPLICATE KEY UPDATE:这是MySQL的一个强大特性。当插入的数据与表中现有数据的主键或唯一索引冲突时,执行更新操作而不是报错。这对于爬虫非常有用,因为我们可以定期重新爬取同一部电影来更新它的评分、评价人数等信息,而不会产生重复记录。VALUES(column_name)函数引用的是INSERT语句中试图插入的值。
  4. 异常处理与日志:数据库操作可能失败(网络问题、重复键、字段超长等)。我们使用try...except捕获异常,记录错误日志,并执行回滚。spider.logger是Scrapy提供的日志工具,非常好用。
  5. 字符集utf8mb4:这是必须的。标准的utf8在MySQL中只支持最多三个字节的字符(基本多文种平面),而utf8mb4支持四字节字符(如Emoji表情、某些生僻汉字)。豆瓣的评论或简介里有可能出现Emoji。

对应的settings.py配置

# 启用Item Pipeline,并设置执行顺序。数值越小优先级越高。 ITEM_PIPELINES = { 'douban_movie.pipelines.DataCleanPipeline': 300, # 先清洗 'douban_movie.pipelines.MySQLPipeline': 800, # 后存储 } # MySQL数据库配置 MYSQL_HOST = 'localhost' MYSQL_DATABASE = 'douban_movie' MYSQL_USER = 'your_username' MYSQL_PASSWORD = 'your_password'

对于SQLite,管道逻辑类似,只是连接库换成sqlite3,SQL语句略有不同(SQLite使用INSERT OR REPLACE来实现类似ON DUPLICATE KEY UPDATE的功能)。

6. 项目运行、监控与数据验证

6.1 运行爬虫与常用命令

项目代码编写完成后,就可以运行了。在项目根目录(有scrapy.cfg的目录)下,使用命令行:

# 运行名为 'top250' 的爬虫,并将日志输出到文件 scrapy crawl top250 -s LOG_FILE=top250.log # 更详细的日志输出 scrapy crawl top250 --loglevel=INFO # 以安静模式运行,只输出错误信息 scrapy crawl top250 --loglevel=ERROR # 将爬取到的数据导出为JSON文件(用于调试,不经过Pipeline) scrapy crawl top250 -o movies.json # 将爬取到的数据导出为CSV文件 scrapy crawl top250 -o movies.csv

使用-o选项导出文件时,数据不会经过你在settings.py中配置的ITEM_PIPELINES。这通常用于快速检查爬虫解析是否正确。正式运行采集数据到数据库时,不要加-o参数。

实操心得:在开发调试阶段,可以先使用scrapy shell 'https://movie.douban.com/subject/1292052/'命令进入交互式shell。这是一个极其强大的工具,你可以快速测试CSS或XPath选择器是否正确,而无需反复运行整个爬虫。例如,在shell中输入response.css('h1 span::text').get(),立刻就能看到提取结果。

6.2 监控爬虫状态与日志分析

爬虫运行时,控制台会输出大量日志。关注以下几点:

  • DEBUG级别日志:可以看到每个请求的发出和响应,以及Item的生成,信息量最大,但也很冗长。
  • INFO级别日志:会显示爬取的页面数量、Item数量、请求统计等,适合监控运行概况。
  • ERROR/WARNING级别日志:必须重点关注。常见的错误包括:
    • Ignoring response <xxx>: HTTP status code is not handled or not allowed:请求被服务器拒绝(如403、404、503)。可能是触发了反爬。
    • Error downloading <request url>:网络错误或超时。
    • 在管道中记录的数据库插入错误。

将日志输出到文件(-s LOG_FILE=...)便于事后分析。如果爬虫中途因错误停止,可以查看日志文件定位问题。

6.3 数据验证与简单分析

爬虫运行结束后,我们需要验证数据是否正确地存入了数据库。连接到你的MySQL数据库,执行一些查询:

-- 查看总共爬取了多少条电影记录 SELECT COUNT(*) FROM movies; -- 查看评分最高的10部电影 SELECT title, rating, rating_count FROM movies ORDER BY rating DESC LIMIT 10; -- 查看哪种类型的电影最多 SELECT genres, COUNT(*) as count FROM movies GROUP BY genres ORDER BY count DESC LIMIT 10; -- 检查是否有数据缺失(例如评分为空) SELECT * FROM movies WHERE rating IS NULL OR rating = '';

通过这些查询,可以直观地感受数据的质量。你可能会发现一些问题,比如某些电影的“上映日期”字段格式很奇怪,或者“标签”字段为空。这可能需要你回头调整爬虫的解析逻辑或数据清洗管道。

7. 常见问题排查与优化技巧

在实际运行中,你几乎一定会遇到各种问题。这里记录了一些典型问题的排查思路和解决技巧。

7.1 爬虫被屏蔽或返回403错误

这是最常见的问题。

  • 症状:日志中出现大量403 ForbiddenIgnoring response,爬取速度变慢直至停止,或者返回的HTML内容是验证码页面。
  • 排查与解决
    1. 检查请求头:确保User-Agent是有效的浏览器标识。使用我们上面编写的随机User-Agent中间件。
    2. 降低请求频率:大幅增加DOWNLOAD_DELAY(比如到5秒或10秒),并减少CONCURRENT_REQUESTS_PER_DOMAIN(比如到1)。让爬虫“慢下来”。
    3. 检查Cookies:有些页面需要登录或简单的会话状态。确保COOKIES_ENABLED = True。对于更复杂的情况,可能需要先在浏览器中登录,然后将Cookies复制到爬虫的start_requests方法中。
    4. 模拟浏览器行为:有些网站会检查JavaScript执行环境。对于这类动态加载的网站,Scrapy本身可能不够。这时可以考虑:
      • 使用scrapy-splashscrapy-playwright中间件来渲染JavaScript。
      • 直接分析网站的API接口。通过浏览器开发者工具的“网络”选项卡,查看数据是如何通过XHR或Fetch请求加载的,然后让爬虫直接去请求这些API接口,效率更高且不易被反爬。豆瓣的部分数据就有API接口。
    5. 使用代理IP:如果IP被封锁,这是终极方案。可以在middlewares.py中编写代理中间件,并从代理IP池中随机选取IP。但免费代理质量不稳定,高质量代理需要付费。

7.2 数据提取失败或为空

  • 症状:Item中的某些字段总是为空,或者提取到了错误的内容。
  • 排查与解决
    1. 使用scrapy shell调试:这是最有效的工具。在shell中加载有问题的页面URL,然后逐一测试你的CSS/XPath选择器,看是否能准确提取到目标数据。
    2. 检查页面结构变化:网站前端可能会改版,导致你之前写的选择器失效。定期检查并更新选择器。
    3. 处理动态加载内容:如果数据是通过JavaScript异步加载的,在初始HTML中不存在。解决方法同上一点(分析API或使用渲染中间件)。
    4. 注意默认值和异常处理:在解析函数中,对每个字段的提取都使用.get()并提供默认值(如.get('')),避免因为某个元素缺失导致整个Item解析失败。

7.3 数据库相关错误

  • 症状MySQLPipeline中抛出pymysql.Error,数据无法插入。
  • 排查与解决
    1. 连接失败:检查settings.py中的数据库主机、端口、用户名、密码、数据库名是否正确。检查MySQL服务是否启动,以及用户是否有远程连接的权限(如果主机不是localhost)。
    2. 字符编码错误:确保数据库、表和连接都使用utf8mb4字符集。中文字符或Emoji插入失败多半是这个问题。
    3. 数据字段超长VARCHAR(255)字段可能存不下过长的字符串(比如演员列表非常长)。根据实际情况调整表结构,增加字段长度,或者将超长文本字段改为TEXT类型。
    4. 主键或唯一键冲突:如果你没有使用ON DUPLICATE KEY UPDATE,并且试图插入重复的豆瓣ID,就会报错。确保你的SQL语句能正确处理重复数据。
    5. 网络超时:数据库服务器响应慢或网络不稳定。可以在连接数据库时设置connect_timeout参数。

7.4 性能优化与代码健壮性

  1. 启用缓存:在settings.py中设置HTTPCACHE_ENABLED = True。这在开发调试时非常有用,可以避免重复下载相同的页面,加快测试速度。
  2. 合理设置并发:不要盲目提高CONCURRENT_REQUESTS。过高的并发会给目标服务器带来压力,也更容易触发反爬。根据网络条件和目标服务器的承受能力,从低并发开始测试。
  3. 实现断点续爬:Scrapy本身不支持真正的断点续爬(即从上次停止的URL继续)。一种简单的实现方式是使用JOBDIR设置:scrapy crawl top250 -s JOBDIR=jobs/top250。这会将请求队列和去重指纹保存到磁盘,下次用相同命令启动时会尝试恢复。更复杂的需求可以考虑使用scrapy-redis将队列和去重中心化到Redis,实现分布式和持久化。
  4. 日志分级:生产环境运行时,将日志级别设为INFOWARNING,避免DEBUG日志产生的巨大IO开销。
  5. 编写单元测试:为你的Spider编写测试,确保解析逻辑正确。Scrapy提供了scrapy.utils.test模块来帮助测试。

这个基于Scrapy的豆瓣电影爬虫项目,从环境搭建、爬虫编写、反爬应对、数据清洗到数据库存储,覆盖了一个数据采集系统的主要环节。它提供的不仅仅是一段可运行的代码,更是一种工程化的思考和解决问题的方法。你可以以此为基础,尝试爬取豆瓣的其他板块(如书籍、音乐),或者将数据应用于更复杂的分析、推荐系统,甚至结合Django或Flask搭建一个简单的电影展示网站。爬虫技术的核心在于对目标网站结构和网络协议的理解,以及应对各种异常情况的耐心与技巧,希望这个项目能为你提供一个扎实的起点。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询