Scrapy爬虫实战:正则高效解析豆瓣电影

Scrapy 是 Python 生态中功能强大的爬虫框架,能高效抓取网页数据并进行结构化处理。本文将从基础用法出发,结合可落地的实战案例,重点讲解正则表达式在 Scrapy 中的应用,并拓展至高级功能,帮助你快速掌握并灵活运用。

一、Scrapy 基础入门

1. 安装 Scrapy

bash 复制代码
pip install scrapy

核心概念速览

  • 项目(Project):爬虫的整体工程目录,包含所有配置和代码。
  • 爬虫(Spider):自定义的爬虫类,定义抓取规则(起始 URL、解析逻辑等)。
  • Item:用于结构化存储爬取的数据(类似字典,但更规范)。
  • 管道(Pipeline):处理爬取到的 Item(如去重、存储到文件 / 数据库)。
  • 中间件(Middleware):介于 Scrapy 引擎和爬虫 / 下载器之间的钩子,可处理请求 / 响应(如添加代理、修改请求头)。

二、实战案例:爬取豆瓣电影 Top250

以爬取豆瓣电影 Top250(https://movie.douban.com/top250)为例,完整演示 Scrapy 工作流程,重点融入正则表达式的应用。

步骤 1:创建项目和爬虫

  1. 创建项目
bash 复制代码
scrapy startproject douban_movie  # 项目名:douban_movie
cd douban_movie

2**.创建爬虫**:

bash 复制代码
scrapy genspider top250 movie.douban.com  # 爬虫名:top250,域名:movie.douban.com

此时项目结构如下:

bash 复制代码
douban_movie/
├── douban_movie/          # 项目核心目录
│   ├── spiders/           # 爬虫目录
│   │   ├── __init__.py
│   │   └── top250.py      # 刚创建的爬虫
│   ├── __init__.py
│   ├── items.py           # 定义数据结构
│   ├── middlewares.py     # 中间件
│   ├── pipelines.py       # 数据处理管道
│   └── settings.py        # 配置文件
└── scrapy.cfg             # 项目配置

步骤 2:定义 Item(数据结构)

items.py 中定义需要爬取的数据字段(如电影名、评分、简介等):

python 复制代码
import scrapy

class DoubanMovieItem(scrapy.Item):
    # 电影名称
    title = scrapy.Field()
    # 评分
    rating = scrapy.Field()
    # 评价人数
    comment_count = scrapy.Field()
    # 电影简介(可能包含年份、国家、类型等信息)
    info = scrapy.Field()
    # 从简介中提取的年份(用正则处理)
    year = scrapy.Field()
    # 从简介中提取的国家(用正则处理)
    country = scrapy.Field()
    # 从简介中提取的类型(用正则处理)
    genre = scrapy.Field()

步骤 3:编写爬虫逻辑(核心,含正则案例)

打开 spiders/top250.py,实现爬取和解析逻辑。豆瓣电影列表页的 HTML 结构中,每部电影信息包含在 div.item 标签内,我们需要从中提取数据,并通过正则从复杂文本中提取结构化信息。

完整爬虫代码:
python 复制代码
import scrapy
import re  # 引入正则模块
from douban_movie.items import DoubanMovieItem

class Top250Spider(scrapy.Spider):
    name = "top250"  # 爬虫名(唯一)
    allowed_domains = ["movie.douban.com"]
    start_urls = ["https://movie.douban.com/top250"]  # 起始 URL

    def parse(self, response):
        """解析列表页,提取每部电影的信息,并翻页"""
        # 1. 提取当前页所有电影节点(div.item)
        movies = response.xpath('//div[@class="item"]')
        
        for movie in movies:
            item = DoubanMovieItem()  # 实例化 Item
            
            # 2. 提取基础信息(用 XPath)
            # 电影名(可能包含中文名和英文名,取第一个)
            item['title'] = movie.xpath('.//span[@class="title"][1]/text()').get()
            # 评分
            item['rating'] = movie.xpath('.//span[@class="rating_num"]/text()').get()
            # 评价人数(格式如"150.0万人评价",用正则提取数字)
            comment_text = movie.xpath('.//div[@class="star"]/span[4]/text()').get()
            # 正则案例1:提取评价人数中的数字(如"150.0万人评价"→"150.0万")
            item['comment_count'] = re.findall(r'(\d+\.\d+万|\d+万)', comment_text)[0] if comment_text else None
            
            # 3. 提取简介(包含年份、国家、类型,格式如"1994 / 美国 / 剧情 犯罪")
            info_text = movie.xpath('.//p[@class=""]/text()').getall()
            # 清洗简介文本(去除换行和空格)
            info_text = ''.join(info_text).strip() if info_text else ''
            item['info'] = info_text
            
            # 4. 用正则从简介中提取年份、国家、类型(核心正则案例)
            # 正则案例2:提取年份(如"1994 / 美国 / 剧情 犯罪"→"1994")
            year_match = re.search(r'(\d{4})', info_text)  # \d{4} 匹配4位数字(年份)
            item['year'] = year_match.group(1) if year_match else None
            
            # 正则案例3:提取国家(如"1994 / 美国 / 剧情 犯罪"→"美国")
            # 逻辑:年份后用"/"分隔,国家在年份和类型之间
            country_match = re.search(r'\d{4}\s*/\s*([^/]+)\s*/', info_text)
            item['country'] = country_match.group(1).strip() if country_match else None
            
            # 正则案例4:提取类型(如"1994 / 美国 / 剧情 犯罪"→"剧情 犯罪")
            # 逻辑:最后一个"/"后的内容为类型
            genre_match = re.search(r'/.*/\s*(.*)$', info_text)
            item['genre'] = genre_match.group(1).strip() if genre_match else None
            
            yield item  # 提交 Item 到管道处理

        # 5. 翻页逻辑(获取下一页 URL 并继续爬取)
        next_page = response.xpath('//span[@class="next"]/a/@href').get()
        if next_page:
            # 拼接完整 URL(豆瓣分页 URL 是相对路径,如"?start=25&filter=")
            next_url = response.urljoin(next_page)
            yield scrapy.Request(next_url, callback=self.parse)  # 递归调用 parse 解析下一页
正则表达式详解(核心):
  1. 提取评价人数 :原始文本:"150.0万人评价",正则 r'(\d+\.\d+万|\d+万)' 匹配带小数点或不带小数点的 "万" 单位数字,findall 返回匹配列表,取第一个结果。

  2. 提取年份 :原始文本:"1994 / 美国 / 剧情 犯罪",正则 r'(\d{4})' 匹配 4 位数字(年份特征),search 找到第一个匹配,group(1) 提取分组内容。

  3. 提取国家 :正则 r'\d{4}\s*/\s*([^/]+)\s*/' 中:

    • \d{4}\s*/\s*:匹配年份(4 位数字)+ 可能的空格 + "/" + 可能的空格;
    • ([^/]+):匹配非 "/" 的任意字符(国家名称),用分组捕获;
    • \s*/:匹配后续的空格和 "/"。
  4. 提取类型 :正则 r'/.*/\s*(.*)$' 中:

    • /.*/:匹配到最后一个 "/"(因为 .* 是贪婪匹配);
    • \s*(.*)$:匹配剩余内容(类型),$ 表示字符串结尾。

步骤 4:配置管道(存储数据)

pipelines.py 中定义数据处理逻辑,这里演示保存到 CSV 文件:

python 复制代码
import csv
from itemadapter import ItemAdapter

class DoubanMoviePipeline:
    def __init__(self):
        # 打开文件并写入表头
        self.file = open('douban_top250.csv', 'w', newline='', encoding='utf-8')
        self.writer = csv.DictWriter(self.file, fieldnames=[
            'title', 'rating', 'comment_count', 'info', 'year', 'country', 'genre'
        ])
        self.writer.writeheader()

    def process_item(self, item, spider):
        # 写入每一行数据
        self.writer.writerow(ItemAdapter(item).asdict())
        return item  # 传递给下一个管道(如果有的话)

    def close_spider(self, spider):
        # 关闭文件
        self.file.close()

settings.py 中启用管道(取消注释并修改):

python 复制代码
ITEM_PIPELINES = {
    "douban_movie.pipelines.DoubanMoviePipeline": 300,  # 300 是优先级(0-1000,越小越先执行)
}

步骤 5:设置请求头(避免被反爬)

settings.py 中添加 User-Agent(模拟浏览器请求):

python 复制代码
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"

# 可选:禁用 Cookie(部分网站反爬会检测 Cookie)
# COOKIES_ENABLED = False

步骤 6:运行爬虫

python 复制代码
scrapy crawl top250  # 爬虫名是 top250

运行后,项目根目录会生成 douban_top250.csv 文件,包含所有爬取的电影数据。

三、扩展性:高级功能与实战技巧

1. 处理动态加载数据(AJAX)

如果目标网站数据通过 AJAX 动态加载(如滚动加载、点击加载),可通过以下方式处理:

  • 打开浏览器开发者工具(F12),在 "Network" 页找到 AJAX 请求的 API 接口(通常是 JSON 格式);
  • 在 Scrapy 中直接请求 API 接口,解析 JSON 数据(无需解析 HTML)。

示例 :爬取某网站动态加载的列表,API 接口为 https://example.com/api/list?page=1

python 复制代码
def parse(self, response):
    data = response.json()  # 解析 JSON
    for item in data['results']:
        # 提取数据
        yield {'title': item['title'], 'url': item['url']}
    
    # 翻页
    next_page = data.get('next_page')
    if next_page:
        yield scrapy.Request(f'https://example.com/api/list?page={next_page}', callback=self.parse)

2. 使用代理 IP 突破反爬

middlewares.py 中添加代理中间件:

python 复制代码
class ProxyMiddleware:
    def process_request(self, request, spider):
        # 代理 IP 列表(可从代理服务商获取)
        proxies = [
            'http://123.45.67.89:8080',
            'http://98.76.54.32:8888'
        ]
        # 随机选择一个代理
        request.meta['proxy'] = random.choice(proxies)

settings.py 中启用:

python 复制代码
DOWNLOADER_MIDDLEWARES = {
    "douban_movie.middlewares.ProxyMiddleware": 543,
}

3. 分布式爬取(大规模数据)

使用 scrapy-redis 实现分布式爬取(多台机器共享任务队列):

bash 复制代码
pip install scrapy-redis

修改 settings.py

python 复制代码
# 启用 scrapy-redis 调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 启用 scrapy-redis 去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# Redis 连接配置
REDIS_URL = "redis://localhost:6379/0"

四、学习建议与总结

  1. 多练正则 :正则是 Scrapy 解析复杂文本的核心工具,推荐通过 Regex101 在线练习,重点掌握:

    • 元字符(\d 数字、\w 单词字符、. 任意字符);
    • 量词(* 0 次或多次、+ 1 次或多次、? 0 次或 1 次、{n} 恰好 n 次);
    • 分组(() 捕获分组、(?:) 非捕获分组);
    • 贪婪与非贪婪匹配(.* 贪婪、.*? 非贪婪)。
  2. 调试技巧

    使用 scrapy shell 调试 XPath 和正则:

bash 复制代码
scrapy shell "https://movie.douban.com/top250"  # 在终端交互式调试

在爬虫中添加 self.log(item) 打印数据,查看解析是否正确。

3.反爬应对 :除了代理和 User-Agent,还可设置请求间隔(DOWNLOAD_DELAY = 2settings.py 中)、随机请求头、模拟登录(通过 FormRequest)等。

通过豆瓣电影案例,你已掌握 Scrapy 的核心流程和正则应用,结合扩展性技巧,可应对大部分爬虫场景。实际项目中,需根据目标网站结构灵活调整解析逻辑,不断优化反爬策略。

四、正则表达式高阶技巧与实战场景

1. 非贪婪匹配与分组捕获的碰撞

在爬取电影简介时,常遇到类似"导演: 弗兰克·德拉邦特 / 主演: 蒂姆·罗宾斯 / 摩根·弗里曼 / 鲍勃·冈顿"的文本,需要分别提取导演和主演。若用贪婪匹配.*会一口气匹配到末尾,此时非贪婪模式.*?+ 分组捕获就能精准拆分:

python 复制代码
info_text = "导演: 弗兰克·德拉邦特 / 主演: 蒂姆·罗宾斯 / 摩根·弗里曼 / 鲍勃·冈顿"

# 提取导演
director_match = re.search(r'导演:\s*(.*?)\s*/', info_text)
director = director_match.group(1) if director_match else None
# 结果:'弗兰克·德拉邦特'

# 提取主演(所有主演用逗号拼接)
cast_match = re.search(r'主演:\s*(.*)$', info_text)
cast = ','.join(re.split(r'\s*/\s*', cast_match.group(1))) if cast_match else None
# 结果:'蒂姆·罗宾斯,摩根·弗里曼,鲍勃·冈顿'
  • 解析:\s*匹配空格,(.*?)非贪婪捕获导演名,遇到/就停止;主演部分用split/分割后拼接,处理多主演场景。

2. 零宽断言:不捕获文本却能定位

爬取网页中隐藏的邮箱时,常遇到"联系我们:xxx[AT]example.com"(用[AT]替代@防爬),需匹配[AT]前后的字符并替换:

python 复制代码
hidden_email = "联系我们:xxx[AT]example.com"
# 零宽断言定位[AT]前后内容,替换为@
email = re.sub(r'(?<=\w)\[AT\](?=\w)', '@', hidden_email)
# 结果:'联系我们:xxx@example.com'
  • 零宽断言(?<=\w)表示前面必须是单词字符(字母 / 数字 / 下划线),(?=\w)表示后面必须是单词字符,只做位置判断不捕获内容,完美解决替换边界问题。

五、动态渲染页面爬取(应对 JavaScript 加载数据)

部分网站用 JavaScript 动态生成内容(如滚动加载的商品列表),此时直接爬取 HTML 只能拿到空数据。可通过以下两种方式解决:

1. 分析 AJAX 接口

打开浏览器开发者工具(F12)→ Network → XHR,找到动态加载的 API 接口(通常返回 JSON)。例如某电商网站的商品列表接口为https://api.example.com/goods?page=1,直接请求接口更高效:

python 复制代码
def start_requests(self):
    for page in range(1, 11):  # 爬1-10页
        url = f'https://api.example.com/goods?page={page}'
        yield scrapy.Request(url, callback=self.parse_api)

def parse_api(self, response):
    data = response.json()
    for item in data['goods_list']:
        yield {
            'name': item['name'],
            'price': item['price'],
            'sales': item['sales']
        }

2. 集成 Selenium 处理渲染

若接口加密难以解析,可用 Selenium 驱动浏览器加载完整页面。需先安装scrapy-selenium和浏览器驱动:

bash 复制代码
pip install scrapy-selenium

settings.py配置:

python 复制代码
DOWNLOADER_MIDDLEWARES = {
    'scrapy_selenium.SeleniumMiddleware': 800
}
SELENIUM_DRIVER_NAME = 'chrome'
SELENIUM_DRIVER_EXECUTABLE_PATH = '/path/to/chromedriver'

爬虫中使用:

python 复制代码
from scrapy_selenium import SeleniumRequest

def start_requests(self):
    yield SeleniumRequest(
        url='https://example.com/dynamic-page',
        callback=self.parse_dynamic
    )

def parse_dynamic(self, response):
    # 此时response已包含JS渲染后的内容
    for goods in response.xpath('//div[@class="goods-item"]'):
        yield {
            'name': goods.xpath('.//h3/text()').get(),
            'price': goods.xpath('.//span[@class="price"]/text()').get()
        }

六、分布式爬取与去重优化(基于 Scrapy-Redis)

当数据量达到百万级,单台机器爬取效率极低,分布式爬取可让多台机器共享任务队列。

1. 环境配置

bash 复制代码
pip install scrapy-redis

修改settings.py

python 复制代码
# 替换调度器和去重器为Scrapy-Redis实现
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# Redis连接地址(多台机器需指向同一Redis服务器)
REDIS_URL = "redis://192.168.1.100:6379/0"
# 允许暂停后重启继续爬取
SCHEDULER_PERSIST = True

2. 爬虫代码调整

python 复制代码
from scrapy_redis.spiders import RedisSpider

class DistributedSpider(RedisSpider):
    name = 'distributed'
    redis_key = 'start_urls'  # 从Redis的start_urls列表获取起始URL

    def parse(self, response):
        # 解析逻辑与普通爬虫一致
        for item in response.xpath('//div[@class="item"]'):
            yield {
                'title': item.xpath('.//h2/text()').get(),
                'url': item.xpath('.//a/@href').get()
            }
        # 翻页链接
        next_page = response.xpath('//a[@class="next"]/@href').get()
        if next_page:
            yield response.follow(next_page, callback=self.parse)

启动方式:先在 Redis 中添加起始 URL:

bash 复制代码
redis-cli lpush start_urls https://example.com/page1

然后多台机器分别运行爬虫:

bash 复制代码
scrapy crawl distributed

机器会自动从 Redis 获取任务,避免重复爬取,极大提升效率。

七、反爬策略进阶:从伪装到动态调整

1. 随机请求头池

middlewares.py添加随机 User-Agent 中间件:

python 复制代码
import random

class RandomUserAgentMiddleware:
    def __init__(self):
        self.user_agents = [
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15",
            "Mozilla/5.0 (X11; Linux x86_64) Firefox/113.0"
        ]

    def process_request(self, request, spider):
        request.headers['User-Agent'] = random.choice(self.user_agents)

settings.py启用:

python 复制代码
DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.RandomUserAgentMiddleware': 400,
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None  # 禁用默认中间件
}

2. 动态调整请求间隔

根据网站响应速度自动调整爬取频率,在settings.py设置:

python 复制代码
DOWNLOAD_DELAY = 1  # 基础间隔1秒
RANDOMIZE_DOWNLOAD_DELAY = True  # 随机加减0.5-1.5倍,避免固定间隔被识别

八、数据存储多样化:从 CSV 到数据库

1. 存储到 MySQL

pipelines.py编写数据库管道:

python 复制代码
import pymysql
from itemadapter import ItemAdapter

class MySQLPipeline:
    def __init__(self):
        self.conn = None
        self.cursor = None

    def open_spider(self, spider):
        self.conn = pymysql.connect(
            host='localhost',
            user='root',
            password='123456',
            database='scrapy_data',
            charset='utf8mb4'
        )
        self.cursor = self.conn.cursor()
        # 创建表
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS goods (
                id INT AUTO_INCREMENT PRIMARY KEY,
                name VARCHAR(255) NOT NULL,
                price DECIMAL(10,2) NOT NULL,
                sales INT NOT NULL
            )
        ''')

    def process_item(self, item, spider):
        adapter = ItemAdapter(item)
        self.cursor.execute('''
            INSERT INTO goods (name, price, sales) VALUES (%s, %s, %s)
        ''', (adapter['name'], adapter['price'], adapter['sales']))
        self.conn.commit()
        return item

    def close_spider(self, spider):
        self.cursor.close()
        self.conn.close()

settings.py启用:

python 复制代码
ITEM_PIPELINES = {
    'myproject.pipelines.MySQLPipeline': 300
}

2. 存储到 MongoDB

安装pymongo后编写管道:

python 复制代码
import pymongo
from itemadapter import ItemAdapter

class MongoPipeline:
    def __init__(self):
        self.client = pymongo.MongoClient('mongodb://localhost:27017/')
        self.db = self.client['scrapy_db']
        self.collection = self.db['goods']

    def process_item(self, item, spider):
        self.collection.insert_one(ItemAdapter(item).asdict())
        return item

九、总结与学习路径

Scrapy 的核心魅力在于其模块化设计 ------ 从请求发送、数据解析到存储,每个环节都可灵活扩展。入门时可从单页静态网站练手,熟练掌握 XPath 和正则表达式(重点练分组、非贪婪匹配、断言);进阶阶段攻克动态渲染和反爬,尝试用 Selenium 或分析 AJAX 接口;最后通过 Scrapy-Redis 实现分布式,应对大规模数据爬取。

推荐资源:

  • 官方文档:Scrapy Documentation(最权威的参考资料)
  • 实战项目:爬取豆瓣图书、知乎话题、电商商品列表,用不同存储方式落地数据
  • 正则练习:Regex101(实时验证正则表达式,支持 Python 语法)

记住,爬虫的核心是 "尊重网站规则,合理控制频率",避免对服务器造成过度压力。随着技术迭代,反爬与绕过的博弈会持续升级,但扎实的基础和灵活的思路,永远是应对变化的底气。

相关推荐
李小白662 小时前
Python文件操作
开发语言·python
weixin_525936333 小时前
金融大数据处理与分析
hadoop·python·hdfs·金融·数据分析·spark·matplotlib
Zwb2997923 小时前
Day 30 - 错误、异常与 JSON 数据 - Python学习笔记
笔记·python·学习·json
码界筑梦坊4 小时前
206-基于深度学习的胸部CT肺癌诊断项目的设计与实现
人工智能·python·深度学习·flask·毕业设计
flashlight_hi4 小时前
LeetCode 分类刷题:74. 搜索二维矩阵
python·算法·leetcode·矩阵
通往曙光的路上5 小时前
国庆回来的css
人工智能·python·tensorflow
不语n5 小时前
Windows+Docker+AI开发板打造智能终端助手
python·docker·树莓派·香橙派·dify·ollama·ai开发板
蓑笠翁0015 小时前
从零开始学习Python Django:从环境搭建到第一个 Web 应用
python·学习·django
计算机毕业设计指导6 小时前
从零开始构建HIDS主机入侵检测系统:Python Flask全栈开发实战
开发语言·python·flask