ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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.pyDjango 启动时读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.pyimport 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_MODULExxx传入时不会被覆盖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_spiderDjango 项目名myproject。3.1 定义 Django 模型在 Django 侧创建一个 app假设叫articles模型定义如下# myproject/articles/models.py from django.db import models class Article(models.Model): url models.URLField(uniqueTrue) title models.CharField(max_length200) content models.TextField() created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return self.title逻辑说明url设置uniqueTrue是为了给后续的「按 URL 去重」提供数据库级约束这是去重策略的底线。即使 pipeline 写错了重复 URL 也无法插入数据库会抛IntegrityError。created_at使用auto_now_addTrue由 ORM 自动填充pipeline 里不需要手动赋值。参数说明CharField的max_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。这样无论在哪个目录启动 ScrapyPython 都能找到myproject.settings。这里的..是相对路径的字符串拼接最终会被os.path.join规范成绝对路径不依赖当前工作目录。3.3 定义 Item 和 PipelineScrapy 侧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( urlitem[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_LEVELINFO跑完后在 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_sizecrawler.settings.getint(DJANGO_BULK_SIZE, 200)) def process_item(self, item, spider): self.buffer.append( Article( urlitem[url], titleitem[title], contentitem[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_conflictsTrue) self.buffer.clear() def close_spider(self, spider): self.flush()逻辑说明process_item不再直接写库而是把数据组装成Article实例放进缓冲区flush()用bulk_create一次性插入。ignore_conflictsTrue让重复 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_create的ignore_conflictsTrue无法判断记录是插入成功还是被忽略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( urlitem[url], defaults{title: item[title], content: item[content]}, ) return item逻辑说明deferToThread把_write放到线程池执行process_item立即返回一个 Deferredreactor 不会等待数据库操作完成。这种方式适合数据库响应较慢比如远程 MySQL且并发量大的场景。注意线程池默认大小有限如果并发写入量极大_write会在池里排队并不会无限加速。提示对于 90% 的业务量直接在process_item里同步写库就够了。引入线程池等于引入新的并发控制问题——Django ORM 的数据库连接本身不是线程安全的deferToThread并发执行时连接会被多个线程共享需要把CONN_MAX_AGE设为 0每次请求新建连接以减少MySQL server has gone away的概率。5. 上线前必查的验证与排错步骤django 和 scrapy 结合的项目运行时报错往往集中在环境加载、连接管理和时区三个层面。把这几个点验证完基本就能稳定上线。5.1 环境类错误的排查线路第一个高频错误是AppRegistryNotReady: Apps arent 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__datetoday).count() print(f总数: {total}, 今日新增: {today_count}) 输出结果如果出现较大的新增量说明 pipeline 写入正常。如果 total 为 0 但爬虫日志有输出优先检查DJANGO_BULK_SIZE是否设置缓冲区没 flush以及close_spider有没有被调用。5.3 部署时的环境变量陷阱宝塔部署或 systemd 定时任务执行爬虫时常常遇到本地跑得好好的、定时任务里却找不到 Django 模块的问题。原因是 cron 和 systemd 的环境变量只保留了最小集合PYTHONPATH和DJANGO_SETTINGS_MODULE都没有继承。解决起来很简单在启动命令前显式写入cd /path/to/project \ DJANGO_SETTINGS_MODULEmyproject.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会被代码覆盖成默认值导致连错数据库。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进