news 2026/9/16 8:51:44

Django与Scrapy整合实战:通过Item Pipeline实现数据入库与批量写入

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django与Scrapy整合实战:通过Item Pipeline实现数据入库与批量写入

简介:Django与Scrapy框架结合开发爬虫管理系统的完整示例代码包,适合熟悉Python基础、希望掌握Web框架与爬虫框架整合技能的开发者。资源共59个文件,以Python源码(.py)为主,附带编译缓存(.pyc)及XML配置、HTML模板、SQLite数据库等,包体仅41KB,目录结构清晰,便于快速定位Django应用、Scrapy项目及配置文件。已有1346人学习,内容涵盖Django视图与模板、Scrapy爬虫封装、Pipeline数据入库等核心环节,通过实际项目演示了从Web界面触发爬虫、监控状态到数据落库的完整流程。资源内包含Django核心应用、数据仓库模块以及Scrapy的items、pipelines、middlewares等组件,直观呈现两者整合时的目录划分与调用关系。尤其值得关注的是,示例中已集成可运行的新闻爬虫,并配有对应的项目配置,可直接作为模板改造复用,帮助开发者少走弯路,快速搭建属于自己的爬虫管理平台。

1. 把 django 和 scrapy 装进同一个项目:先解决两个框架打架的问题

如果团队里已经有一套 Django 服务在维护业务数据,而数据来源是 Scrapy 爬虫,最常见的做法是什么?把抓取结果导出 JSON,再写脚本导入;或者干脆把 Django 的模型文件复制一份到 Scrapy 项目里。后者初期确实跑得快,但两边模型一旦不同步,字段没改全、迁移忘执行,数据错乱只是时间问题。django 和 scrapy 结合的核心,不是「两个项目拼在一起」,而是让 Scrapy 的 Item Pipeline 直接使用 Django ORM,把items.py里定义的字段在入库前实时映射到 Django Model。这样迁移、admin 后台、ORM 查询全都复用同一套代码,Scrapy 只负责抓取和清洗。

这个方案能在哪里落地?凡是爬虫结果需要长期维护、需要后台管理、需要跟现有业务表做关联的场景都适用:资讯聚合、商品监控、报表采集。对新手来说,照着做能省掉「导出再导入」的脏活;对熟手来说,本文后半部分关于批量写入、去重键设计和异步边界的讨论,才是真正决定线上稳不稳的分水岭。

2. 让 scrapy 进程先加载 django 环境:settings_module 与 django.setup()

Scrapy 和 Django 都是「配置驱动」的框架,但两者的配置体系完全隔离。Scrapy 启动时读自己的settings.py,Django 启动时读DJANGO_SETTINGS_MODULE指向的文件。直接在一个 Scrapy 项目里from myproject.models import Article,十有八九会抛出django.core.exceptions.AppRegistryNotReady。原因很简单:Django 的模型注册机制要求先加载 settings、连接数据库、填充 app registry,才能 import 模型类。Scrapy 默认不会帮你做这一步。

2.1 初始化模块:把 django.setup() 放在所有模型 import 之前

常见的做法是在 Scrapy 项目根目录放一个独立的初始化模块,比如scrapy_django_boot.py

import os import django os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") django.setup()

然后在pipelines.py的第一行导入它:

import scrapy_django_boot # 必须先执行,Django 环境才可用 from myproject.models import Article

逻辑说明:setdefault只在环境变量不存在时写入,这样外部通过命令行export DJANGO_SETTINGS_MODULE=xxx传入时不会被覆盖;django.setup()会加载 settings 并填充 app registry,之后才能安全 import 模型。很多人把这两行直接写进settings.py或 spider 文件里,我一般建议单独放模块,因为 pipeline、middleware、spider 都可能需要它,集中导入一次比到处复制更可控。

参数说明:DJANGO_SETTINGS_MODULE的值必须指向一个在sys.path中可导入的模块,如果你的 Django 项目和 Scrapy 项目是平级目录,需要先在 Scrapy 的settings.py里把 Django 项目根目录加入sys.path,否则myproject.settings会导入失败。

2.2 两个 settings 各自的职责边界

很多人第一次结合时会困惑:数据库连接、中间件、app 列表到底该写在哪边?这里有一个可以参照的职责划分:

配置项归属方原因
DATABASES、INSTALLED_APPSDjango settingsORM 依赖这些配置完成模型注册和建连
ITEM_PIPELINES、CONCURRENT_REQUESTSScrapy settings控制抓取请求和入库管道的执行顺序
数据库连接池、CONN_MAX_AGEDjango settingsScrapy 不感知数据库连接生命周期
请求延迟、下载超时Scrapy settings属于抓取调度行为,与 ORM 无关

顺着这个边界往下走,还有一个容易被忽略的点:Scrapy 是 Twisted 异步框架,Django ORM 是同步阻塞调用。两者结合时,process_item里执行的 ORM 写操作会阻塞 reactor 事件循环,但多数业务场景下这是可以接受的,因为爬虫的瓶颈通常在下载环节而非入库环节。如果你在 pipeline 里做批量写入,阻塞时间会进一步压缩。后面第四章我会展开讲批量写入的边界。

2.3 为什么不要在 spider 里直接写 ORM

Article.objects.create(...)放进 spider 的parse方法里其实也能跑通,但等于把「抓什么」和「怎么存」耦合在一起。后续想加去重、加字段映射、加多爬虫复用,都要改 spider 代码。Item Pipeline 存在的意义就是让数据流变成「spider 产出 Item → pipeline 清洗/校验/入库」,每一段职责单一。

提示:如果你只是在本地调试,想快速验证 ORM 能不能通,可以在scrapy shell里先import scrapy_django_boot再查一条数据。如果这步通了而爬虫跑不通,问题基本出在管道注册或环境变量上,而不是 Django 配置。

3. 最小可运行实现:Item Pipeline 里写 Django ORM

理论清楚了,下面给一套完整的项目骨架。目录结构采用 Django 项目和 Scrapy 项目平级的方式,Scrapy 项目名暂定news_spider,Django 项目名myproject

3.1 定义 Django 模型

在 Django 侧创建一个 app,假设叫articles,模型定义如下:

# myproject/articles/models.py from django.db import models class Article(models.Model): url = models.URLField(unique=True) title = models.CharField(max_length=200) content = models.TextField() created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.title

逻辑说明:url设置unique=True是为了给后续的「按 URL 去重」提供数据库级约束,这是去重策略的底线。即使 pipeline 写错了,重复 URL 也无法插入,数据库会抛IntegrityErrorcreated_at使用auto_now_add=True,由 ORM 自动填充,pipeline 里不需要手动赋值。

参数说明:CharFieldmax_length要大于目标数据的最长可能值,如果抓取的标题有截断风险,可以在 pipeline 里先做item["title"][:200]预处理,而不是依赖模型报错。TextField没有长度限制,但注意 MySQL 下TEXT类型不能有默认值,迁移时不要给它加default

3.2 初始化代码调整

把上一章的scrapy_django_boot.py放在 Scrapy 项目根目录,内容不变,但有一点要注意:如果直接在命令行执行scrapy crawl news,当前工作目录就是 Scrapy 项目根目录,sys.path自然包含当前目录。但如果用了 PyCharm 的 Run Configuration 或者部署工具,工作目录可能变成其他位置,导致import myproject失败。规避办法是在 Scrapy 的settings.py顶部加一段路径处理:

# news_spider/settings.py import os import sys PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) DJANGO_PROJECT_ROOT = os.path.join(PROJECT_ROOT, "..", "myproject") sys.path.insert(0, DJANGO_PROJECT_ROOT)

逻辑说明:PROJECT_ROOT定位到settings.py所在目录,用..拼出 Django 项目的绝对路径并插入sys.path。这样无论在哪个目录启动 Scrapy,Python 都能找到myproject.settings。这里的..是相对路径的字符串拼接,最终会被os.path.join规范成绝对路径,不依赖当前工作目录。

3.3 定义 Item 和 Pipeline

Scrapy 侧items.py保持简单:

import scrapy class ArticleItem(scrapy.Item): url = scrapy.Field() title = scrapy.Field() content = scrapy.Field()

Field对象不声明类型,只是数据容器的键。真正的类型检查交给 Django 模型和数据库层去把关,Scrapy 这边不做重复校验。

Pipeline 写入库逻辑:

# news_spider/pipelines.py import scrapy_django_boot # noqa: F401 确保 Django 已初始化 from myproject.articles.models import Article from django.db import IntegrityError class DjangoWriterPipeline: def process_item(self, item, spider): try: article, created = Article.objects.update_or_create( url=item["url"], defaults={ "title": item["title"], "content": item["content"], }, ) except IntegrityError as e: spider.logger.warning(f"数据写入冲突: {e}") return item if created: spider.crawler.stats.inc_value("article/created") else: spider.crawler.stats.inc_value("article/updated") return item

逻辑说明:update_or_create先用url字段查记录,存在则更新defaults里的字段,不存在则新建。这一步天然实现了「按 URL 去重并更新」的需求,是 django 和 scrapy 结合里性价比最高的写法。返回值created是布尔值,用它给 Scrapy 的 stats 计数器分别累加,方便后面判断是全新增量还是大量更新。

参数说明:update_or_create的查询条件字段支持多个,例如url加上date做联合唯一;defaults里的字段不会参与查重条件。如果你想让某条记录在更新时保留原值(比如首次抓取时间),不要把该字段放进defaults,而是用auto_now_add或手动判断。

3.4 注册 Pipeline 并验证入库

在 Scrapy 的settings.py中启用管道:

ITEM_PIPELINES = { "news_spider.pipelines.DjangoWriterPipeline": 300, }

数字 300 是优先级,数值越小越靠前执行。如果你的管道链路里有清洗、去重、入库多级处理,按顺序分配 100、200、300 即可。

启动爬虫前先迁移 Django 数据库:

python manage.py makemigrations articles python manage.py migrate

迁移完成后,正常启动爬虫:

scrapy crawl article_spider -s LOG_LEVEL=INFO

跑完后在 Django 侧验证:

python manage.py shell -c "from articles.models import Article; print(Article.objects.count())"

如果输出了数字,说明数据已经通过 pipeline 进入 Django 的库。此时打开 Django admin 后台,在articles表里就能直接看到和管理刚入库的数据,这就是「爬虫入库 → admin 管理」闭环的直观反馈。

提示:如果你在 PyCharm 里直接运行scrapy crawl,建议把运行命令改成python -m scrapy crawl article_spider,因为模块方式运行时sys.path[0]是当前项目根目录,能避免很多诡异的路径导入问题。

4. 从能跑到跑稳:批量写入、去重与 django 与 scrapy 的异步边界

最小实现跑通后,紧接着会遇到三个生产问题:数据量大时逐条update_or_create太慢;重复 URL 的并发写入容易撞唯一约束;以及 reactor 阻塞导致的抓取吞吐量下降。逐个拆开说。

4.1 批量写入:缓存 + flush 而不是改循环

Scrapy 的process_item是逐条被调用的,如果每条都执行一次数据库写操作,在 MySQL 上大约消耗 1-3 毫秒连接往返,1 万条记录就要额外等几十秒。常见做法是在 pipeline 里维护一个缓冲列表,攒够一定数量再批量提交:

# news_spider/pipelines.py import scrapy_django_boot # noqa from myproject.articles.models import Article from django.db import transaction class BulkDjangoWriterPipeline: def __init__(self, flush_size): self.flush_size = flush_size self.buffer = [] @classmethod def from_crawler(cls, crawler): return cls(flush_size=crawler.settings.getint("DJANGO_BULK_SIZE", 200)) def process_item(self, item, spider): self.buffer.append( Article( url=item["url"], title=item["title"], content=item["content"], ) ) if len(self.buffer) >= self.flush_size: self.flush() return item def flush(self): if not self.buffer: return with transaction.atomic(): Article.objects.bulk_create(self.buffer, ignore_conflicts=True) self.buffer.clear() def close_spider(self, spider): self.flush()

逻辑说明:process_item不再直接写库,而是把数据组装成Article实例放进缓冲区;flush()bulk_create一次性插入。ignore_conflicts=True让重复 URL 的记录被数据库静默忽略,而不是抛异常中断整批。close_spider保证 spider 结束时缓冲区里的残留数据被清空,不会丢数据。

参数说明:DJANGO_BULK_SIZE可以从crawler.settings.getint读取,这样不同爬虫可以配置不同的批量大小。数值不宜太大,我一般设置在 200-500 之间。过大会导致单条INSERT语句过长,超过 MySQL 的max_allowed_packet时会报错;过小则退化成逐条插入,失去批量意义。

4.2 去重策略对比:别把 update_or_create 和 bulk_create 混用

很多人在尝试批量优化时,第一个想法是「能不能用 bulk_create 实现 upsert」。问题是bulk_createignore_conflicts=True无法判断记录是插入成功还是被忽略,update_or_create又没有批量版本。这两者的适用场景完全不同:

策略适用场景代价注意点
update_or_create数据量小、需要精准统计新增/更新每条至少一次 SELECT + 一次 UPDATE/INSERT并发写同一 URL 可能撞唯一约束
bulk_create + ignore_conflicts首次全量抓取,数据量大且不关心重复无法区分插入和冲突不会更新已有记录
先查询再批量更新已有大量旧数据,需要刷新字段内存占用高适合百万级以下的数据集

如果业务确实需要「批量且精准」,我一般会在flush()里加一步缓存键判断:用 URL 的md5作为内存中的集合,重复出现的 URL 在进 buffer 前就被滤掉。这个方法不查数据库,只依赖当前爬虫批次内的去重:

import hashlib class BulkDjangoWriterPipeline(BulkDjangoWriterPipeline): def __init__(self, flush_size): super().__init__(flush_size) self.seen = set() def _fingerprint(self, item): return hashlib.md5(item["url"].encode("utf-8")).hexdigest() def process_item(self, item, spider): fp = self._fingerprint(item) if fp in self.seen: return item self.seen.add(fp) return super().process_item(item, spider)

逻辑说明:_fingerprint把 URL 字符串转成定长 hash,存入self.seen。同一爬虫生命周期内遇到相同 URL 直接跳过,不再进入 buffer 和数据库。这个方案解决的是「进程内重复」,不是「历史库重复」——历史重复仍然靠数据库唯一约束兜底。

4.3 异步边界:ORM 调用阻塞 reactor 的取舍

Scrapy 的 reactor 是单线程事件循环,process_item里的同步 ORM 调用会让整个爬虫的下载调度短暂停顿。数据量不大时,这种停顿微乎其微;但如果你开了 32 个并发请求,每个响应都要等几百毫秒的数据库写入,吞吐量就会明显下降。

常见做法是把 ORM 写操作扔到 Twisted 的线程池里执行,让 reactor 不被阻塞。但 pipeline 的process_item是同步接口,要做线程化需要改成deferToThread,这里给出一个轻量写法:

from twisted.internet import threads from scrapy.exceptions import DropItem class AsyncDjangoWriterPipeline: def process_item(self, item, spider): return threads.deferToThread(self._write, item, spider) def _write(self, item, spider): # 这里放同步的 ORM 写逻辑 Article.objects.update_or_create( url=item["url"], defaults={"title": item["title"], "content": item["content"]}, ) return item

逻辑说明:deferToThread_write放到线程池执行,process_item立即返回一个 Deferred,reactor 不会等待数据库操作完成。这种方式适合数据库响应较慢(比如远程 MySQL)且并发量大的场景。注意线程池默认大小有限,如果并发写入量极大,_write会在池里排队,并不会无限加速。

提示:对于 90% 的业务量,直接在process_item里同步写库就够了。引入线程池等于引入新的并发控制问题——Django ORM 的数据库连接本身不是线程安全的,deferToThread并发执行时,连接会被多个线程共享,需要把CONN_MAX_AGE设为 0(每次请求新建连接)以减少MySQL server has gone away的概率。

5. 上线前必查的验证与排错步骤

django 和 scrapy 结合的项目,运行时报错往往集中在环境加载、连接管理和时区三个层面。把这几个点验证完,基本就能稳定上线。

5.1 环境类错误的排查线路

第一个高频错误是AppRegistryNotReady: Apps aren't loaded yet。出现这个错误时,按顺序检查:scrapy_django_boot是否在pipelines.py顶部被导入;DJANGO_SETTINGS_MODULE指向的模块是否在sys.path中;是否在任何import models之前调用了django.setup()。如果你是照着第三章结构写的还报这个错,把初始化模块的 import 移到pipelines.py的第一行,不要放在settings.py里——Scrapy 加载 settings 时会先执行里面的代码,但如果某个模块在 settings 加载前就 import 了模型,setup 时序就乱了。

第二个常见错误是ImproperlyConfigured: Requested setting INSTALLED_APPS, but settings are not configured。这个报错说明代码在django.setup()之前访问了 Django 配置,多半是某个模块顶部写了from django.conf import settings且被提前 import。定位方法:追溯 traceback 里第一个 import Django 的文件,把它改成延迟导入。

5.2 数据验证脚本

跑完爬虫后,不要只看item_scraped_count,那个数字只代表 Item 通过了 pipeline,不代表入库成功。推荐写一个 Django 侧的校验脚本:

python manage.py shell -c " from articles.models import Article from django.utils import timezone today = timezone.now().date() total = Article.objects.count() today_count = Article.objects.filter(created_at__date=today).count() print(f'总数: {total}, 今日新增: {today_count}') "

输出结果如果出现较大的新增量,说明 pipeline 写入正常。如果 total 为 0 但爬虫日志有输出,优先检查DJANGO_BULK_SIZE是否设置(缓冲区没 flush)以及close_spider有没有被调用。

5.3 部署时的环境变量陷阱

宝塔部署或 systemd 定时任务执行爬虫时,常常遇到本地跑得好好的、定时任务里却找不到 Django 模块的问题。原因是 cron 和 systemd 的环境变量只保留了最小集合,PYTHONPATHDJANGO_SETTINGS_MODULE都没有继承。解决起来很简单,在启动命令前显式写入:

cd /path/to/project && \ DJANGO_SETTINGS_MODULE=myproject.settings PYTHONPATH=/path/to/project \ /usr/bin/python3 -m scrapy crawl article_spider

这样一来,不依赖 shell 的环境变量继承关系,scrapy 进程能稳定拿到 Django 项目的配置。定时任务跑完后再对比 5.2 节的对账脚本输出,确认抓取结果已入库。

最后一件事:确保scrapy_django_boot.py里的os.environ.setdefault用的是setdefault而不是直接赋值,否则上面命令行传入的DJANGO_SETTINGS_MODULE会被代码覆盖成默认值,导致连错数据库。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 8:50:58

企业级一体化平台架构解析:ERP/CRM/HRM/ATS协同落地实践

1. 项目概述:一个被误读的命名,背后藏着企业级系统架构的底层逻辑“ever-gauzy”——这个词第一次出现在我面前时,我也愣了三秒。它不像“SAP”那样有明确指向,也不像“Salesforce”自带行业标签,更不像“钉钉”“飞书…

作者头像 李华
网站建设 2026/9/16 8:50:43

CMSIS-NN源码深度尽调:构建链、宏开关与量化数据流解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 8:50:23

PCIe在机器人控制器中的工程落地:带宽、可靠性与EMC实战

1. 为什么机器人控制器正在悄悄换“心脏”:PCIe 不再是电脑专属的高速通道你拆过一台工业机器人控制器吗?打开机箱盖,里面不是密密麻麻的接线端子,就是几块堆叠的电路板,上面插着各种功能模块——运动控制卡、视觉采集…

作者头像 李华
网站建设 2026/9/16 8:48:50

uniApp iOS打包Code Signing Error解决方案

1. 问题现象与初步定位 最近在将uniApp项目打包成iOS应用时,遇到了一个典型的报错场景:Xcode编译过程中突然中断,控制台抛出 Code Signing Error 相关提示。这种问题在跨平台开发中相当常见,尤其是当项目涉及原生模块或第三方S…

作者头像 李华
网站建设 2026/9/16 8:48:25

为什么你永远无法替换JDK的java.lang.String?深入JVM类加载机制

1. 我也曾天真地以为:敲一个 java.lang.String 出来就能替换JDK那个先说说这个问题的起源。前几天在技术群里看到一个小伙伴提问:他为了给字符串加一个“统计单词数”的便捷方法,直接把 JDK 里的java.lang.String源码复制到了自己的项目里&am…

作者头像 李华
网站建设 2026/9/16 8:48:05

SkyWalking vs Zipkin:微服务链路追踪选型与性能调优实战

1. 三个必须上链路追踪的典型场景:别再等故障发生了才后悔微服务架构走到第六个年头,我最大的一个醒悟是:链路追踪不是给领导看的监控大屏,也不是技术博客里用来炫耀的架构图,而是你凌晨三点被叫起来排查故障时唯一能救…

作者头像 李华