简介: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_APPS | Django settings | ORM 依赖这些配置完成模型注册和建连 |
| ITEM_PIPELINES、CONCURRENT_REQUESTS | Scrapy settings | 控制抓取请求和入库管道的执行顺序 |
| 数据库连接池、CONN_MAX_AGE | Django settings | Scrapy 不感知数据库连接生命周期 |
| 请求延迟、下载超时 | 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 也无法插入,数据库会抛IntegrityError。created_at使用auto_now_add=True,由 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。这样无论在哪个目录启动 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_create的ignore_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 的环境变量只保留了最小集合,PYTHONPATH和DJANGO_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会被代码覆盖成默认值,导致连错数据库。
本文还有配套的精品资源,点击获取