Python Scrapy爬虫框架实战项目演示与数据库集成
简介: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 时,整个流程就像一场精密编排的交响乐:
- 引擎(Engine) 启动,像个指挥家一样统筹全局;
- 它向 调度器(Scheduler) 要一个待处理的请求(Request);
- 调度器把 Request 交给引擎,引擎再转给 下载器(Downloader) ;
- 下载器不阻塞等待响应,而是告诉 Twisted:“我发出去了,请帮我监听回信”;
- 然后立即返回,继续处理下一个任务;
- 当服务器响应到达时,Twisted 触发回调,把 Response 送回给引擎;
- 指令传达到你的 Spider ,调用你写的
parse()方法进行解析; - 解析出的新 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() ),但它带来了几个关键优势:
- 结构清晰 :一眼看出这个数据项有哪些字段;
- 元数据支持 :可以通过
Field()添加额外信息; - 序列化控制 :比如
serializer=str自动转换时间对象为字符串; - 兼容性强 :原生支持各种 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 不只是一个爬虫框架,更是一种工程思维的体现 。
它教会我们:
- 如何用异步提升性能;
- 如何用组件化解耦复杂性;
- 如何用配置代替硬编码;
- 如何用流水线管理数据生命周期。
当你不再只是“写个脚本抓点数据”,而是开始思考“这个爬虫怎么设计才够健壮、可维护、易扩展”时,你就已经迈入了高级开发者的行列。
而这,才是真正的成长🚀。
所以,下次再遇到反爬难题,别急着换工具。先问问自己:
👉 我的架构够合理吗?
👉 我的错误处理够完善吗?
👉 我的日志能帮我看清问题吗?
搞定了这些,你会发现—— 没有爬不了的网站,只有不够强的系统 。🔥
简介:Scrapy是Python中高效、强大的网络爬虫框架,专为数据抓取设计,提供完整的组件体系以实现网页请求、数据解析与存储的全流程处理。本“Python Scrapy 爬虫框架demo”展示了一个结构清晰的Scrapy项目实例,涵盖Spiders、Item Pipeline、中间件配置及数据库集成等核心功能。通过该项目,开发者可学习如何定义数据结构、实现数据清洗与持久化存储,并掌握应对反爬机制、处理动态内容等实战技巧,适用于新闻、商品、评论等多类型数据采集场景,是掌握现代爬虫开发的优质实践资源。
更多推荐



所有评论(0)