本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Scrapy是Python中高效、强大的网络爬虫框架,专为数据抓取设计,提供完整的组件体系以实现网页请求、数据解析与存储的全流程处理。本“Python Scrapy 爬虫框架demo”展示了一个结构清晰的Scrapy项目实例,涵盖Spiders、Item Pipeline、中间件配置及数据库集成等核心功能。通过该项目,开发者可学习如何定义数据结构、实现数据清洗与持久化存储,并掌握应对反爬机制、处理动态内容等实战技巧,适用于新闻、商品、评论等多类型数据采集场景,是掌握现代爬虫开发的优质实践资源。

Scrapy框架深度解析:从核心架构到反爬实战

你有没有遇到过这样的情况?明明写好了爬虫代码,跑起来却发现请求被频繁拦截,数据抓了一半就断了;或者好不容易提取出的数据,格式乱七八糟,存进数据库时报错一堆……别急,这几乎是每个爬虫开发者都踩过的坑。

而今天我们要聊的主角—— Scrapy ,正是为了解决这些问题而生的强大工具。它不只是一个“能发请求、提数据”的简单脚本集合,而是一套完整、高效、可扩展的网络采集系统。掌握它,就像拿到了一把通往海量网页世界的万能钥匙🔑。

但问题是:很多人用 Scrapy 时,只是照搬模板,知其然不知其所以然。结果一旦遇到动态加载、登录验证、IP封锁等复杂场景,立刻束手无策。究其原因,往往是忽略了对 底层机制的理解 工程化思维的构建

所以,我们不打算再走一遍“安装 → 创建项目 → 写Spider”的老路。相反,咱们要从“为什么这么设计”出发,深入 Scrapy 的心脏地带,看看它是如何以单线程之力,扛起数千并发的重任;又是如何通过模块化设计,让你轻松应对千变万化的反爬策略。

准备好了吗?🚀 让我们一起拆解这个 Python 爬虫界的“工业级引擎”。


引擎驱动的数据洪流:Scrapy 是怎么跑起来的?

想象一下,你在做一个电商比价项目,目标是抓取京东、天猫上十万款商品的价格变化。如果用传统的 requests + threading 方式,开几百个线程去请求,CPU 上下文切换直接飙高,内存占用爆炸,还没开始解析页面,机器已经卡死了 😵‍💫。

但 Scrapy 不一样。它靠的是 事件驱动 + 异步非阻塞 I/O ,背后站着一位重量级选手—— Twisted

🤔 等等,Twisted 是啥?

简单说,它是一个老牌的 Python 异步网络库,比 asyncio 还早很多年。虽然现在大家更熟悉 async/await ,但在 Scrapy 诞生的那个年代(2008年),Twisted 就是异步领域的王者👑。

那具体是怎么工作的呢?

当你运行 scrapy crawl myspider 时,整个流程就像一场精密编排的交响乐:

  1. 引擎(Engine) 启动,像个指挥家一样统筹全局;
  2. 它向 调度器(Scheduler) 要一个待处理的请求(Request);
  3. 调度器把 Request 交给引擎,引擎再转给 下载器(Downloader)
  4. 下载器不阻塞等待响应,而是告诉 Twisted:“我发出去了,请帮我监听回信”;
  5. 然后立即返回,继续处理下一个任务;
  6. 当服务器响应到达时,Twisted 触发回调,把 Response 送回给引擎;
  7. 指令传达到你的 Spider ,调用你写的 parse() 方法进行解析;
  8. 解析出的新 Request 又被放回调度器排队,数据则进入 Item Pipeline 流水线处理……

整个过程基于一个事件循环(Event Loop),所有操作都在同一个线程内完成。没有线程创建销毁的开销,也没有 GIL 锁的竞争问题,I/O 密集型任务的吞吐能力直接拉满💥!

你可以把它理解成一家快递分拣中心:

  • 引擎 = 中央控制系统
  • 调度器 = 待发货订单池
  • 下载器 = 快递员车队
  • Spider = 收件人 & 新订单发起者
  • Item Pipeline = 包裹质检+打包流水线

每一个包裹(请求)来了又走,系统始终高效运转,不会因为某个快递员在路上堵车就瘫痪。

这种“非阻塞+回调”的模式,使得 Scrapy 在普通机器上也能轻松支持上千并发连接,每秒处理数百甚至上千个请求——而这,仅仅是它的基本功罢了。

# Scrapy 基本项目结构示意
scrapy startproject tutorial

执行这条命令后,你会看到生成的目录结构中包含 spiders/ , pipelines.py , settings.py 等关键文件。这些不是随意安排的,而是整个框架职责分离的设计体现:

  • spiders/ :定义你要“抓什么”和“怎么抓”
  • pipelines.py :决定抓回来的数据“怎么处理”
  • settings.py :控制全局行为,比如并发数、延迟、中间件开关等

是不是已经开始感受到它的工程美感了?😎

特性 描述
并发模型 单线程异步(基于事件循环)
底层库 Twisted(Python异步网络框架)
请求效率 支持每秒数百至数千请求(视目标站点而定)

当然,光有骨架还不够。真正让 Scrapy 活起来的,是它的五大核心组件协同工作。下面我们来逐个拆解,看看它们是如何各司其职、默契配合的。


Spider:你的爬虫大脑🧠

如果说引擎是心脏,那 Spider 就是整个系统的“大脑”。它决定了:

  • 从哪里开始抓?
  • 遇到链接要不要跟进?
  • 怎么提取数据?
  • 提取出的数据长什么样?

换句话说, 你写的每一行爬虫逻辑,几乎都在 Spider 里实现

基础 Spider vs CrawlSpider:自由派与规则派之争

在 Scrapy 世界里,最常用的两种 Spider 类型是 scrapy.Spider CrawlSpider 。你可以把它们看作两种编程哲学的代表:

  • 基础 Spider :完全掌控一切,适合定制化需求
  • CrawlSpider :依赖规则自动导航,适合大规模站点遍历

举个例子🌰:你想抓取某论坛的精华帖列表页和详情页。

基础 Spider ,你可以这样写:

import scrapy

class BasicSpider(scrapy.Spider):
    name = 'basic'
    start_urls = ['https://example.com/list']

    def parse(self, response):
        # 手动提取详情页链接并生成新请求
        for href in response.css('.item a::attr(href)').getall():
            yield response.follow(href, callback=self.parse_detail)

    def parse_detail(self, response):
        yield {
            'title': response.css('h1::text').get(),
            'content': response.css('.article p::text').getall()
        }

💡 小贴士: response.follow() 是个好东西!它会自动处理相对 URL 补全,比手动拼接 urljoin 更安全。

这段代码非常直观:拿到响应 → 提取链接 → 构造新请求 → 回调解析详情页 → 输出字典数据。

但它也有缺点: 所有逻辑都要手动控制 。如果你要爬一个有几十个分类、上千个子页面的电商站,写起来就会很累。

这时候就可以考虑 CrawlSpider 出场了:

from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class CrawlingSpider(CrawlSpider):
    name = 'crawling'
    allowed_domains = ['example.com']
    start_urls = ['https://example.com']

    rules = (
        Rule(LinkExtractor(allow=r'/category/\d+'), callback='parse_item', follow=True),
        Rule(LinkExtractor(allow=r'/product/\d+'), callback='parse_product'),
    )

    def parse_item(self, response):
        self.logger.info(f"Parsing item page: {response.url}")
        # 提取分类页数据
        pass

    def parse_product(self, response):
        yield {
            'name': response.css('h1.product-name::text').get(),
            'price': response.css('.price::text').re_first(r'\d+\.\d+')
        }

看出来区别了吗?这里的关键在于 Rule 规则:

  • 第一条规则匹配 /category/数字 的链接,执行 parse_item 并继续跟进( follow=True
  • 第二条规则匹配 /product/数字 的链接,执行 parse_product 但不再往下爬

这就形成了一个清晰的两级爬取策略:先进目录页 → 自动跳转到商品页 → 抓取最终数据。

是不是感觉自动化程度一下子提高了?✨

不过别高兴太早——CrawlSpider 也不是万能的。它最大的限制是 灵活性不足 。一旦页面结构变了,正则表达式没匹配上,整个链条就断了。而且调试起来也不如基础 Spider 直观。

所以该怎么选?来看这张对比表👇:

特性 基础 Spider CrawlSpider
自动化程度 低,需手动构造 Request 高,基于规则自动提取链接
控制粒度 极高,完全自定义流程 中等,依赖 Rule 配置
适用场景 固定流程、小规模抓取 多层级导航、全站抓取
学习成本 中等
调试难度 易于追踪请求链 需理解 Rule 匹配顺序

我的建议是:

  • 初学者优先用 基础 Spider ,打好控制流基础;
  • 做大型站点镜像或全站抓取时,再考虑 CrawlSpider
  • 实在不行,也可以混合使用:主流程用 CrawlSpider,关键节点用自定义逻辑接管。

另外还有一个隐藏知识点: 默认情况下,Scrapy 是深度优先还是广度优先?

答案是: 深度优先(DFS)

因为它的调度器默认使用 LIFO(Last In First Out)策略,也就是后进先出。最新生成的请求会被优先处理,自然就越钻越深。

但如果你希望按层级一层层往外扩(比如先抓完所有一级分类,再抓二级商品),那就得改成 FIFO(先进先出):

# settings.py
SCHEDULER_DISK_QUEUE = 'scrapy.squeues.PickleFifoDiskQueue'
SCHEDULER_MEMORY_QUEUE = 'scrapy.squeues.FifoMemoryQueue'

加这两行配置,立马变身广度优先战士 🛡️!


动态参数与灵活启动:让爬虫变得更聪明🤖

现实中的爬虫很少是“一次性”的。你可能需要根据不同城市、关键词、时间段来运行同一个爬虫。

比如你想抓“北京”和“上海”的租房信息,难道要写两个 Spider?当然不用!

Scrapy 支持通过命令行动态传参:

scrapy crawl param_spider -a city=beijing -a keyword=三室一厅

对应的 Spider 可以这样接收参数:

import scrapy

class ParametrizedSpider(scrapy.Spider):
    name = 'param_spider'

    def __init__(self, city='beijing', keyword='租房', *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.city = city
        self.keyword = keyword
        self.start_urls = [f'https://rent.example.com/search?city={city}&q={keyword}']

    def parse(self, response):
        for house in response.css('.house-item'):
            yield {
                'title': house.css('.title::text').get(),
                'location': self.city,
                'keyword': self.keyword
            }

⚠️ 注意陷阱!如果不显式赋值 self.city ,即使传了 -a city=shanghai ,也会因为实例属性不存在而报错 AttributeError

还有一种更强大的方式:重写 start_requests() 方法。

这是干嘛的?当你需要发送 POST 请求、携带认证头、或者批量生成带分页的 URL 时,它就派上用场了:

def start_requests(self):
    headers = {
        'Authorization': 'Bearer your-jwt-token',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
    }
    base_url = 'https://api.example.com/v1/products'
    keywords = ['手机', '笔记本', '平板']

    for kw in keywords:
        yield scrapy.Request(
            url=f"{base_url}?q={kw}",
            headers=headers,
            callback=self.parse_api_result,
            meta={'search_keyword': kw}  # 重要!跨请求传递上下文
        )

这里的 meta 参数特别有用。它可以让你在多个回调之间传递数据,比如原始搜索词、用户ID、时间戳等,避免信息丢失。

下面这个 Mermaid 图,展示了从启动到第一个请求发出的全过程:

graph TD
    A[启动爬虫: scrapy crawl spider_name] --> B{解析命令行参数 -a}
    B --> C[调用 Spider.__init__(*args, **kwargs)]
    C --> D[设置 name, allowed_domains]
    D --> E[构建 start_urls 或重写 start_requests()]
    E --> F[Engine 发送第一个 Request 至 Scheduler]
    F --> G[Downloader 获取响应]
    G --> H[调用 parse() 或指定回调]
    H --> I[提取数据或生成新 Request]
    I --> J{是否结束?}
    J -- 否 --> F
    J -- 是 --> K[关闭爬虫]

是不是有种“原来如此”的感觉?😄 掌握了这个生命周期,以后遇到“为啥参数没生效?”、“为啥 start_urls 没请求?”这类问题,就能快速定位根源了。


数据建模的艺术:别再用裸字典了!

很多新手写爬虫时,喜欢直接 yield {'title': ..., 'price': ...} ,看起来方便,实则隐患重重。

问题在哪?

  • 字段名拼错怎么办?
  • 缺少字段会不会导致下游崩溃?
  • 多个 Spider 返回结构不一致怎么统一处理?

解决方案就是: 使用 Item 模型

经典方式:继承 scrapy.Item

import scrapy

class ProductItem(scrapy.Item):
    title = scrapy.Field()
    price = scrapy.Field()
    url = scrapy.Field()
    category = scrapy.Field()
    created_at = scrapy.Field(serializer=str)

虽然语法有点啰嗦(每个字段都要写 scrapy.Field() ),但它带来了几个关键优势:

  1. 结构清晰 :一眼看出这个数据项有哪些字段;
  2. 元数据支持 :可以通过 Field() 添加额外信息;
  3. 序列化控制 :比如 serializer=str 自动转换时间对象为字符串;
  4. 兼容性强 :原生支持各种 Exporter 导出格式。

尤其是元数据功能,很多人没意识到它的威力。比如你可以这样增强字段语义:

class NewsArticleItem(scrapy.Item):
    headline = scrapy.Field(
        required=True,
        source='h1.title',
        min_length=10
    )
    content = scrapy.Field(
        strip=True,
        filter_html=True
    )
    publish_time = scrapy.Field(
        format='%Y-%m-%d %H:%M:%S',
        timezone='Asia/Shanghai'
    )
    author = scrapy.Field(default='Unknown')

这些元数据可以在 Pipeline 中被读取,用来做自动化校验、清洗、格式化,真正做到“配置即代码”。

flowchart TD
    A[Spider Extract Data] --> B{Field Metadata Exists?}
    B -- Yes --> C[Apply Rules: Validate, Clean, Format]
    B -- No --> D[Use Default Processing]
    C --> E[Pass to Next Pipeline]
    D --> E

看到了吗?有了元数据,你的 Pipeline 就可以变得“智能”起来,根据不同标记执行不同逻辑,而不是写一堆 if-else。

现代化方案:dataclass 风格定义 Item

如果你觉得上面那种写法太冗长,好消息来了!社区有个超好用的扩展包叫 scrapy-dataclass ,让你可以用现代 Python 风格定义 Item:

pip install scrapy-dataclass

然后就可以这么写了:

from dataclasses import dataclass, field
from scrapy_dataclass import Field
from typing import List, Optional
from datetime import datetime

@dataclass
class BookItem:
    isbn: str = Field(metadata={'unique': True})
    title: str
    authors: List[str] = field(default_factory=list)
    price: float = None
    tags: Optional[List[str]] = None
    crawled_at: str = field(default_factory=lambda: datetime.now().isoformat())

爽不爽?🤩

相比传统方式,它有几个明显优势:

对比维度 scrapy.Item scrapy-dataclass
语法简洁度 较低,需重复写 Field() 高,符合 PEP 557 规范
类型提示支持 强,原生支持 typing 模块
默认值设置 需手动处理 支持 default , default_factory
IDE 自动补全 一般 优秀
序列化兼容性 原生支持 需适配导出器

更重要的是,它完全兼容 Scrapy 的序列化机制,可以直接用 -o output.json 导出,无需任何改造。

对于新项目,我强烈推荐使用这种方式。开发体验提升不止一点点!


数据流水线:Pipeline 的魔法之旅

当你的 Spider 提取出一个 Item 后,它并不会直接消失。相反,它会进入一条精心设计的 处理流水线(Item Pipeline)

你可以把它想象成工厂里的传送带:

  • 第一站:清洗脏数据(比如价格里的¥符号)
  • 第二站:验证完整性(标题不能为空)
  • 第三站:去重(防止重复入库)
  • 第四站:持久化(写入数据库)

每一站都是一个独立的 Pipeline 组件,彼此解耦,互不影响。

如何启用和排序 Pipeline?

settings.py 里注册就行:

ITEM_PIPELINES = {
    'myproject.pipelines.DataCleaningPipeline': 300,
    'myproject.pipelines.ValidationPipeline': 350,
    'myproject.pipelines.DeduplicationPipeline': 400,
    'myproject.pipelines.MongoDBPipeline': 500,
}

数字代表优先级,越小越早执行。建议间隔50,方便后续插入新组件。

比如某天你发现需要记录日志,可以直接插一个 LoggingPipeline 进来,而不影响其他逻辑。

实战一:通用数据验证

有效的验证能帮你提前发现问题,避免垃圾数据流入数据库。

from scrapy.exceptions import DropItem

class ValidationPipeline:
    REQUIRED_FIELDS = ['title', 'url']

    def process_item(self, item, spider):
        missing_fields = [
            field for field in self.REQUIRED_FIELDS
            if not item.get(field)
        ]
        if missing_fields:
            raise DropItem(f"Missing required fields: {missing_fields}")

        # URL 格式校验
        import re
        url = item.get('url')
        if url and not re.match(r'^https?://', url):
            raise DropItem(f"Invalid URL format: {url}")

        return item

DropItem 是 Scrapy 提供的特殊异常,抛出后该 Item 会被丢弃,并计入统计(可通过日志查看)。

实战二:智能去重

Scrapy 自带请求去重( dupefilter ),但 Item 层面还得自己动手。

一种高效做法是基于“内容指纹”:

import hashlib
from scrapy.utils.python import to_bytes

class DeduplicationPipeline:
    def __init__(self):
        self.seen_hashes = set()

    def process_item(self, item, spider):
        fingerprint_fields = [item.get('title'), item.get('price')]
        fingerprint_str = "|".join(str(f) for f in fingerprint_fields if f)
        fingerprint_hash = hashlib.sha256(to_bytes(fingerprint_str)).hexdigest()

        if fingerprint_hash in self.seen_hashes:
            raise DropItem(f"Duplicate item found: {item['title']}")

        self.seen_hashes.add(fingerprint_hash)
        item['fingerprint'] = fingerprint_hash
        return item

🔔 注意:内存去重只适用于单次运行。如果想跨批次去重,必须用 Redis 或数据库存储指纹。

graph LR
    A[Extract Item] --> B{Fingerprint Exists?}
    B -- Yes --> C[DropItem]
    B -- No --> D[Store Fingerprint]
    D --> E[Forward Item]

这个流程确保每个唯一商品只被处理一次,节省大量资源。


数据落地:从 JSON 到 MySQL,怎么存都行

最后一步,当然是把数据存下来啦!

内置导出器:一键保存

最简单的办法是用 -o 参数:

scrapy crawl myspider -o output.json
scrapy crawl myspider -o output.csv

支持格式包括: json , jsonlines , csv , xml , pickle 等。

也可以在 settings.py 中详细配置:

FEEDS = {
    'output.json': {
        'format': 'json',
        'encoding': 'utf8',
        'store_empty': False,
        'fields': ['title', 'price', 'url'],
        'indent': 2,
    },
}

存到 MongoDB:灵活 schema 的天堂

MongoDB 特别适合爬虫数据,毕竟网页结构千变万化,强约束的表结构反而碍事。

import pymongo

class MongoDBPipeline:
    collection_name = 'scrapy_items'

    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', 'items')
        )

    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[self.collection_name].insert_one(dict(item))
        return item

别忘了在 settings.py 加配置:

MONGO_URI = 'mongodb://localhost:27017'
MONGO_DATABASE = 'scraping_db'

写入 MySQL / PostgreSQL:用 SQLAlchemy 更优雅

对于需要事务支持、外键关联的业务系统,关系型数据库仍是首选。

借助 SQLAlchemy ORM,我们可以写出干净的代码:

from sqlalchemy.orm import sessionmaker
from mymodels import Product, create_engine

class SqlAlchemyPipeline:
    def __init__(self, db_url):
        self.db_url = db_url

    @classmethod
    def from_crawler(cls, crawler):
        return cls(db_url=crawler.settings.get('DATABASE_URL'))

    def open_spider(self, spider):
        self.engine = create_engine(self.db_url)
        Session = sessionmaker(bind=self.engine)
        self.session = Session()

    def close_spider(self, spider):
        self.session.close()
        self.engine.dispose()

    def process_item(self, item, spider):
        product = Product(**item)
        self.session.add(product)
        self.session.commit()
        return item

其中 Product 是预先定义好的 SQLAlchemy 模型类,负责映射数据库表结构。


反爬攻防战:让爬虫活下去💪

再厉害的爬虫,也架不住被封 IP 啊!所以,我们必须学会一些反反爬技巧。

Downloader Middleware:你的隐身斗篷

中间件允许你在请求发出前、响应收到后插入自定义逻辑。

随机 User-Agent:伪装成真实用户
class RandomUserAgentMiddleware:
    def __init__(self, agents):
        self.agents = agents

    @classmethod
    def from_crawler(cls, crawler):
        return cls(agents=crawler.settings.get('USER_AGENT_LIST'))

    def process_request(self, request, spider):
        if self.agents:
            ua = random.choice(self.agents)
            request.headers.setdefault('User-Agent', ua)

配合配置:

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/537.36',
    # ...更多UA
]
IP代理池:多马甲轮流上阵
class ProxyPoolMiddleware:
    def __init__(self, proxies):
        self.proxies = proxies

    @classmethod
    def from_crawler(cls, crawler):
        proxy_list = crawler.settings.get('PROXY_LIST', [])
        return cls(proxy_list)

    def process_request(self, request, spider):
        if self.proxies:
            proxy = random.choice(self.proxies)
            request.meta['proxy'] = proxy
        return None

配置代理列表:

PROXY_LIST = [
    'http://proxy1.example.com:8080',
    'http://proxy2.example.com:8080',
]

生产环境建议结合 Redis 动态维护代理质量,自动剔除失效节点。


结语:从工具使用者到系统设计者

看到这里,你应该已经意识到: Scrapy 不只是一个爬虫框架,更是一种工程思维的体现

它教会我们:

  • 如何用异步提升性能;
  • 如何用组件化解耦复杂性;
  • 如何用配置代替硬编码;
  • 如何用流水线管理数据生命周期。

当你不再只是“写个脚本抓点数据”,而是开始思考“这个爬虫怎么设计才够健壮、可维护、易扩展”时,你就已经迈入了高级开发者的行列。

而这,才是真正的成长🚀。

所以,下次再遇到反爬难题,别急着换工具。先问问自己:
👉 我的架构够合理吗?
👉 我的错误处理够完善吗?
👉 我的日志能帮我看清问题吗?

搞定了这些,你会发现—— 没有爬不了的网站,只有不够强的系统 。🔥

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Scrapy是Python中高效、强大的网络爬虫框架,专为数据抓取设计,提供完整的组件体系以实现网页请求、数据解析与存储的全流程处理。本“Python Scrapy 爬虫框架demo”展示了一个结构清晰的Scrapy项目实例,涵盖Spiders、Item Pipeline、中间件配置及数据库集成等核心功能。通过该项目,开发者可学习如何定义数据结构、实现数据清洗与持久化存储,并掌握应对反爬机制、处理动态内容等实战技巧,适用于新闻、商品、评论等多类型数据采集场景,是掌握现代爬虫开发的优质实践资源。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐