Python Django电商比价系统实战:从数据采集到可视化看板

发布时间:2026/8/14 1:31:38
Python Django电商比价系统实战:从数据采集到可视化看板 1. 先搞清楚这个“比价系统”到底要解决什么问题如果你正在找电商比价系统的实现方案或者想用 Python 把价格监控、数据分析和可视化串起来那这篇文章就是为你写的。我拆解过不少类似项目发现新手最容易卡住的不是代码本身而是整个流程的串联和生产环境下的稳定性。一个能跑通的 Demo 和一套能持续运行、方便维护的系统中间隔着好几个需要提前设计的环节。这个“Python电商比价系统”的核心是利用 Django 作为 Web 框架来管理任务和展示数据用 requests 库去电商网站抓取价格信息把结果存到 MySQL 数据库最后通过数据分析生成可视化图表。听起来步骤清晰但实际做的时候很多人会一头扎进爬虫代码里忽略了任务调度、数据去重、错误处理和前端展示的衔接。结果就是爬虫偶尔能跑但页面看不到数据或者数据库里一堆重复记录根本没法分析。所以我更建议你先别急着写爬虫。把思路理顺这是一个由“数据采集-存储-处理-展示”构成的流水线。Django 在这里不只是做个后台它更重要的角色是任务管理中心和数据门户。下面我们就按实际落地的顺序从环境搭建到核心环节一步步拆解。2. 环境与项目骨架搭建别在起点埋雷动手之前先把环境理清楚。很多“跑不起来”的问题都出在环境配置和项目结构上。2.1 基础环境准备与依赖安装你需要一个干净的 Python 环境。我强烈建议使用venv或conda创建虚拟环境避免包版本冲突。# 创建并激活虚拟环境 (以 venv 为例) python -m venv venv # Windows venv\Scripts\activate # Linux/macOS source venv/bin/activate激活环境后安装核心依赖。这里的关键是版本兼容性。Django、requests 和 MySQL 驱动如mysqlclient或pymysql的版本需要匹配你的 Python 版本。pip install django4.2 # 选择一个稳定的LTS版本 pip install requests pip install mysqlclient # 如果安装失败可以试试 pip install pymysql pip install pandas matplotlib # 用于数据分析和可视化为什么先定版本因为不同版本的 Django 对异步支持、数据库配置写法有差异mysqlclient对系统和 MySQL 版本也有要求。用固定版本能确保你复现的步骤和我后面讲的一致。2.2 初始化Django项目与应用结构接下来用 Django 的命令行工具创建项目和核心应用。这个项目的结构应该能清晰地区分不同模块的职责。django-admin startproject price_comparison cd price_comparison python manage.py startapp crawler # 爬虫应用 python manage.py startapp dashboard # 数据看板应用创建后的目录结构大致如下price_comparison/ ├── manage.py ├── price_comparison/ # 项目配置目录 │ ├── __init__.py │ ├── settings.py # 核心配置文件 │ ├── urls.py │ └── wsgi.py ├── crawler/ # 爬虫应用 │ ├── migrations/ │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── models.py # 定义商品、价格等数据模型 │ ├── tasks.py # 爬虫任务逻辑核心 │ ├── tests.py │ └── views.py └── dashboard/ # 数据看板应用 ├── __init__.py ├── apps.py ├── models.py # 可能存放分析结果的模型 ├── views.py # 处理图表数据请求 └── templates/ # 存放HTML模板关键点我把爬虫逻辑主要放在crawler/tasks.py里而不是views.py。这是因为爬虫通常是后台定时任务应该和处理 HTTP 请求的视图逻辑解耦。tasks.py里的函数可以被 Django 的自定义管理命令、Celery 任务队列或者简单的脚本调用。2.3 配置MySQL数据库连接在price_comparison/settings.py中配置数据库。不要用默认的 SQLite因为比价数据量增长很快且涉及简单的关联查询MySQL 更合适。# settings.py DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: price_comparison_db, # 数据库名需提前在MySQL中创建 USER: your_username, PASSWORD: your_password, HOST: localhost, PORT: 3306, OPTIONS: { charset: utf8mb4, # 支持存储Emoji等特殊字符商品描述可能用到 } } }配置好后运行python manage.py migrate来创建 Django 内置的管理表。在这之前请确保你的 MySQL 服务已启动并且已经通过命令行或工具如 MySQL Workbench创建了名为price_comparison_db的数据库。避坑提醒如果遇到mysqlclient安装错误特别是在 Windows 上通常是因为缺少 C 编译环境或 MySQL 客户端库。一个更简单的替代方案是使用pymysql。在settings.py同级的__init__.py文件中加入以下代码# price_comparison/__init__.py import pymysql pymysql.install_as_MySQLdb()然后将settings.py中的ENGINE仍然保持为django.db.backends.mysql即可。pymysql是纯 Python 实现兼容性更好但在极高并发下性能可能略逊于mysqlclient。对于学习和小型项目完全够用。3. 设计数据模型与爬虫任务核心数据库表结构设计是比价系统的基石。设计得好后续查询和分析就顺畅设计得随意后面改起来就头疼。3.1 定义核心数据模型在crawler/models.py中我们定义几个核心模型from django.db import models class Website(models.Model): 电商平台 name models.CharField(max_length100, uniqueTrue, verbose_name平台名称) base_url models.URLField(verbose_name基础URL) is_active models.BooleanField(defaultTrue, verbose_name是否启用) def __str__(self): return self.name class Product(models.Model): 商品唯一标识 name models.CharField(max_length255, verbose_name商品名称) # 商品唯一标识可以是型号、SKU或通过名称平台生成的哈希值 identifier models.CharField(max_length200, db_indexTrue, verbose_name商品标识) # 用于在前端展示的默认图片或描述 image_url models.URLField(blankTrue, nullTrue, verbose_name图片链接) description models.TextField(blankTrue, verbose_name描述) class Meta: unique_together (name, identifier) # 联合唯一约束 def __str__(self): return f{self.name} ({self.identifier}) class PriceRecord(models.Model): 价格记录核心表 product models.ForeignKey(Product, on_deletemodels.CASCADE, related_nameprices, verbose_name商品) website models.ForeignKey(Website, on_deletemodels.CASCADE, verbose_name平台) price models.DecimalField(max_digits10, decimal_places2, verbose_name价格) # 记录抓取时的原价、促销价等信息增加灵活性 original_price models.DecimalField(max_digits10, decimal_places2, nullTrue, blankTrue, verbose_name原价) timestamp models.DateTimeField(auto_now_addTrue, db_indexTrue, verbose_name抓取时间) # 可以添加其他字段如库存状态、促销文本等 in_stock models.BooleanField(defaultTrue, verbose_name有货) note models.CharField(max_length200, blankTrue, verbose_name备注如促销信息) class Meta: ordering [-timestamp] # 默认按时间倒序排列方便查最新价格 indexes [ models.Index(fields[product, website, -timestamp]), # 复合索引加速常用查询 ] def __str__(self): return f{self.product.name} {self.website.name}: {self.price} ({self.timestamp})设计思路解析Website 和 Product 分离Product代表一个抽象商品如“iPhone 15 128GB黑色”Website代表平台如“平台A”、“平台B”。这样一个商品可以在多个平台有价格记录关系清晰。PriceRecord 是事实表每条记录代表在特定时间点、特定平台抓取到的该商品的价格。timestamp非常重要是后续分析价格走势的依据。使用 DecimalField 存储价格绝对不要用FloatField存储金额会有精度问题。DecimalField是精确的数字类型。建立索引在timestamp和(product, website, timestamp)上建立索引能极大提升按商品查历史价格、按时间筛选等查询的速度。定义好模型后执行python manage.py makemigrations crawler和python manage.py migrate来创建表。3.2 编写稳健的爬虫任务函数这是系统的核心引擎写在crawler/tasks.py。重点不是爬虫能跑而是跑得稳、易维护、好扩展。# crawler/tasks.py import requests import logging from datetime import datetime from django.utils.timezone import now from django.db import transaction from .models import Website, Product, PriceRecord logger logging.getLogger(__name__) def fetch_product_price(product_identifier, website_name): 抓取单个商品在指定网站的价格。 返回一个字典包含价格、原价、库存状态等信息。 这是一个示例函数你需要根据目标网站的实际结构进行解析。 # 1. 获取网站配置 try: website Website.objects.get(namewebsite_name, is_activeTrue) except Website.DoesNotExist: logger.error(f网站未找到或未启用: {website_name}) return None # 2. 构造请求头模拟浏览器访问 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } # 3. 构造目标URL (这里需要你根据网站规则自己拼装) # 例如search_url f{website.base_url}/search?q{product_identifier} # 或者直接是商品详情页URL这需要你事先维护一个商品URL的映射表。 # 为简化示例我们假设有一个函数能根据商品标识和网站生成URL target_url construct_product_url(product_identifier, website) if not target_url: logger.warning(f无法为商品 {product_identifier} 构造 {website.name} 的URL) return None # 4. 发送请求并加入重试和超时机制 try: # 设置超时避免长时间等待 response requests.get(target_url, headersheaders, timeout15) response.raise_for_status() # 如果状态码不是200抛出HTTPError异常 except requests.exceptions.Timeout: logger.error(f请求超时: {target_url}) return None except requests.exceptions.HTTPError as e: logger.error(fHTTP错误 {e.response.status_code}: {target_url}) # 特别注意 429 Too Many Requests 错误 if e.response.status_code 429: logger.warning(f触发速率限制需要降低抓取频率或使用代理: {website_name}) return None except requests.exceptions.RequestException as e: logger.error(f请求异常: {e} - {target_url}) return None # 5. 解析页面提取价格信息 (这里需要你写具体的解析逻辑) # 可以使用 BeautifulSoup, lxml, 正则表达式等。 # 示例 # soup BeautifulSoup(response.text, html.parser) # price_element soup.select_one(.price-class) # price extract_price(price_element.text) if price_element else None # in_stock check_stock(soup) # 以下为模拟数据 import random mock_data { price: round(random.uniform(100, 5000), 2), original_price: round(random.uniform(120, 5500), 2), in_stock: random.choice([True, False]), note: random.choice([, 限时优惠, 秒杀]), } # 实际项目中将 mock_data 替换为你的解析结果 parsed_data mock_data if parsed_data[price] is None: logger.warning(f未能从页面解析出价格: {target_url}) return None return { website_id: website.id, price: parsed_data[price], original_price: parsed_data.get(original_price), in_stock: parsed_data[in_stock], note: parsed_data.get(note, ), fetched_at: now(), } def construct_product_url(identifier, website): 根据商品标识和网站规则构造URL。这是一个需要你大量定制填充的函数。 # 示例不同网站的URL模式不同 url_patterns { 平台A: fhttps://www.platform-a.com/product/{identifier}, 平台B: fhttps://www.platform-b.com/item?sku{identifier}, } return url_patterns.get(website.name) # 更复杂的场景可能需要查询一个 ProductURL 关联表。 def save_price_record(product_obj, price_info): 将抓取到的价格信息保存到数据库。 if not price_info: return False try: with transaction.atomic(): # 使用事务保证数据一致性 PriceRecord.objects.create( productproduct_obj, website_idprice_info[website_id], priceprice_info[price], original_priceprice_info.get(original_price), in_stockprice_info[in_stock], noteprice_info[note], timestampprice_info[fetched_at], # 使用抓取时间而非自动添加时间 ) logger.info(f成功保存价格记录: {product_obj.name} - {price_info[price]}) return True except Exception as e: logger.error(f保存价格记录失败: {e}) return False def run_crawler_for_product(product_identifier, product_nameNone): 为一个商品运行爬虫抓取所有启用网站的价格。 # 获取或创建商品对象 product_obj, created Product.objects.get_or_create( identifierproduct_identifier, defaults{name: product_name or product_identifier} ) if created: logger.info(f创建了新商品: {product_obj.name}) active_websites Website.objects.filter(is_activeTrue) for website in active_websites: logger.info(f开始抓取 {product_obj.name} 在 {website.name} 的价格...) price_info fetch_product_price(product_identifier, website.name) if price_info: save_price_record(product_obj, price_info) # 建议在请求间增加延时避免触发反爬 import time time.sleep(1) # 简单延时生产环境建议使用更智能的速率控制核心经验与避坑点错误处理要全面网络超时、HTTP错误特别是429状态码、解析失败、数据库保存失败都要有对应的日志记录和处理逻辑。不能让一个商品的失败导致整个任务崩溃。使用日志不要只用printlogging模块可以方便地控制日志级别将信息输出到文件便于后期排查。生产环境中print语句是看不到的。请求头模拟浏览器这是绕过简单反爬的基础。User-Agent是关键字段。设置超时timeout参数必须设置防止因某个网站响应慢而阻塞整个爬虫进程。注意速率限制在循环中抓取不同网站时一定要加延时如time.sleep。更高级的做法是使用随机延时、维护一个请求队列或者使用IP代理池。看到429 Too Many Requests错误就是明确的速率限制信号。解析逻辑独立fetch_product_price函数中的解析部分应该根据每个网站的HTML结构单独编写或配置。可以考虑将解析规则如CSS选择器存入数据库实现可配置化爬虫。使用事务保存transaction.atomic()确保价格记录和可能的其他关联数据同时成功或失败避免数据不一致。4. 构建数据管理与可视化看板数据抓取进来后需要通过 Django 的管理后台和自定义视图进行管理和展示。4.1 使用Django Admin进行数据管理Django Admin 能快速生成一个功能强大的后台非常适合管理商品、网站和价格记录。# crawler/admin.py from django.contrib import admin from .models import Website, Product, PriceRecord class PriceRecordInline(admin.TabularInline): # 在商品详情页内嵌显示价格记录 model PriceRecord extra 0 readonly_fields (timestamp,) # 时间戳设为只读 can_delete False fields (website, price, original_price, in_stock, note, timestamp) ordering (-timestamp,) admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display (name, identifier, latest_price_info) search_fields (name, identifier) inlines [PriceRecordInline] def latest_price_info(self, obj): 自定义列表列显示最新价格 latest obj.prices.first() # 因为Meta中设置了ordering为[-timestamp] if latest: return f{latest.website.name}: ¥{latest.price} return 无记录 latest_price_info.short_description 最新价格 admin.register(Website) class WebsiteAdmin(admin.ModelAdmin): list_display (name, base_url, is_active) list_editable (is_active,) # 可以直接在列表页启用/禁用网站 admin.register(PriceRecord) class PriceRecordAdmin(admin.ModelAdmin): list_display (product, website, price, original_price, in_stock, timestamp) list_filter (website, in_stock, timestamp) date_hierarchy timestamp # 按时间层级钻取 search_fields (product__name,) readonly_fields (timestamp,) # 抓取时间不应手动修改注册后访问/admin即可管理所有数据。对于比价系统Admin 后台足够完成初期的商品录入、网站配置和记录查看工作。4.2 创建数据可视化视图与API对于前端展示我们需要提供数据接口。在dashboard/views.py中创建视图处理前端的图表数据请求。# dashboard/views.py from django.http import JsonResponse from django.views.generic import TemplateView from django.db.models import Avg, Min, Max from django.utils.timezone import now, timedelta from crawler.models import Product, PriceRecord import json class DashboardView(TemplateView): 数据看板主页面 template_name dashboard/index.html def get_price_trend(request, product_id): 获取某个商品的价格走势数据API try: product Product.objects.get(idproduct_id) except Product.DoesNotExist: return JsonResponse({error: Product not found}, status404) # 获取时间范围默认最近30天 days int(request.GET.get(days, 30)) since_date now() - timedelta(daysdays) # 按天、按平台分组查询 # 使用数据库的日期函数TruncDate来按天分组这里用简化逻辑示意 # 实际生产环境对于MySQL可以使用DATE(timestamp)但Django ORM需要借助TruncDate from django.db.models.functions import TruncDate records PriceRecord.objects.filter( productproduct, timestamp__gtesince_date ).annotate(dateTruncDate(timestamp)).values(date, website__name).annotate( avg_priceAvg(price), min_priceMin(price), max_priceMax(price) ).order_by(date, website__name) # 将数据格式化为前端图表库如ECharts需要的格式 websites set(r[website__name] for r in records) trend_data {website: {dates: [], prices: []} for website in websites} for record in records: website record[website__name] trend_data[website][dates].append(record[date].strftime(%Y-%m-%d)) trend_data[website][prices].append(float(record[avg_price])) # 使用日均价 return JsonResponse({product_name: product.name, trend_data: trend_data}) def get_price_comparison(request): 获取多个商品在当前时刻的比价数据API product_ids request.GET.getlist(product_ids[]) if not product_ids: return JsonResponse({error: No product selected}, status400) products Product.objects.filter(id__inproduct_ids) comparison_data [] for product in products: # 获取每个商品在各个平台的最新价格 latest_records [] for website in Website.objects.filter(is_activeTrue): latest product.prices.filter(websitewebsite).order_by(-timestamp).first() latest_records.append({ website: website.name, price: float(latest.price) if latest else None, in_stock: latest.in_stock if latest else False, timestamp: latest.timestamp.isoformat() if latest else None, }) comparison_data.append({ product_id: product.id, product_name: product.name, prices: latest_records }) return JsonResponse({comparison: comparison_data})然后在dashboard/templates/dashboard/index.html中你可以使用 JavaScript配合 ECharts、Chart.js 等库来调用这些 API 并渲染图表。!-- 简化示例使用 Chart.js -- div h3价格走势图/h3 select idproductSelect !-- 选项通过Django模板或JS动态加载 -- /select input typenumber iddaysInput value30 min1 max365 button onclickloadTrendChart()加载/button canvas idtrendChart/canvas /div div h3实时比价/h3 div idcomparisonTable/div /div script srchttps://cdn.jsdelivr.net/npm/chart.js/script script let trendChart; async function loadTrendChart() { const productId document.getElementById(productSelect).value; const days document.getElementById(daysInput).value; const resp await fetch(/dashboard/api/price_trend/${productId}/?days${days}); const data await resp.json(); // ... 使用 Chart.js 绘制折线图 ... } // ... 类似地编写加载比价表格的函数 ... /script关键点前后端分离Django 视图提供清晰的 JSON API前端用 JavaScript 渲染。这样交互更灵活图表体验更好。数据聚合在数据库层完成像按天统计平均价这样的操作尽量使用 Django ORM 的annotate和aggregate在数据库里完成而不是把所有数据拉到 Python 中再处理效率天差地别。时间处理使用 Django 的timezone模块处理时间确保时区一致。TruncDate等数据库函数能高效地进行日期分组。4.3 配置URL与运行测试最后别忘了把应用的 URL 加入到项目的主路由中并运行服务器测试。# price_comparison/urls.py from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(dashboard/, include(dashboard.urls)), ] # dashboard/urls.py from django.urls import path from . import views urlpatterns [ path(, views.DashboardView.as_view(), namedashboard), path(api/price_trend/int:product_id/, views.get_price_trend, nameprice_trend), path(api/price_comparison/, views.get_price_comparison, nameprice_comparison), ]运行开发服务器python manage.py runserver。访问http://127.0.0.1:8000/admin/管理数据访问http://127.0.0.1:8000/dashboard/查看可视化看板。5. 任务调度、部署与进阶考量让系统从“手动运行”变成“自动运行”并考虑生产环境的要求。5.1 实现定时爬虫任务你不能总手动执行run_crawler_for_product函数。需要定时任务。对于轻量级应用可以使用 Django 的django-crontab或django-apscheduler库。以django-apscheduler为例安装pip install django-apscheduler添加到INSTALLED_APPS。在crawler/apps.py的ready方法中启动调度器# crawler/apps.py from django.apps import AppConfig class CrawlerConfig(AppConfig): default_auto_field django.db.models.BigAutoField name crawler def ready(self): if not os.environ.get(RUN_MAIN): # 防止开发服务器重复启动 return from apscheduler.schedulers.background import BackgroundScheduler from .tasks import run_crawler_for_product scheduler BackgroundScheduler() # 每天凌晨2点执行一次抓取所有商品 scheduler.add_job( run_crawler_for_all_products, # 你需要编写这个函数遍历所有商品 cron, hour2, minute0, iddaily_price_crawl ) scheduler.start()更稳健的方案是使用 Celery对于任务量大、需要重试、结果回调的复杂场景Celery Redis/RabbitMQ 是标准选择。它可以分布式执行任务有完善的重试和监控机制。但引入 Celery 会增加系统复杂度对于初期项目django-apscheduler更简单。5.2 生产环境部署要点关闭Debug模式在settings.py中设置DEBUG False并正确配置ALLOWED_HOSTS。收集静态文件运行python manage.py collectstatic。使用WSGI服务器用 Gunicorn 或 uWSGI 替代 Django 自带的开发服务器。搭配Nginx用 Nginx 做反向代理和静态文件服务。数据库优化定期清理过期数据如3个月前的价格记录避免表过大。可以考虑对PriceRecord表进行分区按时间。日志管理配置settings.py中的LOGGING将日志按级别写入文件并设置日志轮转。反爬应对User-Agent 池准备多个 User-Agent 随机使用。IP 代理如果抓取频率高需要考虑使用代理IP池。请求间隔随机化请求间隔模拟人工操作。处理验证码遇到验证码需要引入打码平台或机器学习识别库成本较高。遵守robots.txt尊重网站的爬虫协议。5.3 系统扩展思路当基本系统跑通后可以考虑以下扩展方向智能Agent呼应标题中的agent这里的“Agent”可以理解为自动化的决策单元。例如降价监控Agent当某个商品的价格低于你设置的目标价时自动发送邮件或短信通知。库存监控Agent监控缺货商品一旦补货立即提醒。实现方式在定时任务中不仅抓取价格还调用这些 Agent 的逻辑函数进行分析和触发动作。更复杂的分析使用pandas对历史价格数据进行更深入的分析比如计算价格波动率、寻找历史最低价、预测价格趋势等。可以将分析结果缓存或存入新表供前端展示。支持更多数据源除了网页爬虫是否可以接入一些公开的比价API将fetch_product_price函数抽象成插件模式不同的网站使用不同的“抓取插件”。用户系统让用户可以订阅自己关心的商品个性化推送比价信息。6. 常见问题排查清单在开发和运行过程中你大概率会遇到以下问题。按这个顺序排查能节省大量时间。Django 无法连接 MySQL症状python manage.py migrate或运行时报数据库连接错误。排查确认 MySQL 服务是否启动 (sudo systemctl status mysql或查看服务列表)。检查settings.py中的数据库名、用户名、密码、主机端口是否正确。检查 MySQL 用户是否有从本地主机 (localhost) 连接并操作该数据库的权限。GRANT ALL PRIVILEGES ON price_comparison_db.* TO your_usernamelocalhost; FLUSH PRIVILEGES;如果使用pymysql确认已在__init__.py中正确安装。爬虫抓不到数据或返回 403/429症状requests请求返回错误状态码或解析不到数据。排查检查请求头特别是User-Agent是否模拟了真实浏览器。检查网络能否在浏览器中正常访问目标URL。查看响应内容将response.text打印或日志记录一部分看是否是预期的HTML还是包含了反爬提示如“请输入验证码”。429 Too Many Requests立即停止大幅增加请求间隔或更换IP。这是最明确的流量限制信号。解析失败目标网站的HTML结构可能已更改。需要更新你的解析逻辑CSS选择器或XPath。管理后台或前端页面样式丢失症状Django Admin 或自定义页面没有CSS样式。排查开发环境下确保DEBUG True。运行python manage.py collectstatic生产环境必要步骤。检查 Nginx 或类似服务器是否正确配置了静态文件路径。定时任务不执行症状配置了django-apscheduler或 Celery但任务没有按计划运行。排查进程是否存活定时任务需要有一个常驻进程。对于django-apscheduler确保它在 Django 启动时被正确加载通常通过AppConfig.ready。对于 Celery需要单独运行celery -A your_project worker --beat。检查日志查看调度器和任务执行的日志是否有报错。时区问题确认settings.py中的TIME_ZONE设置正确并且调度器配置的时区与之匹配。页面图表数据加载慢症状前端请求价格走势API响应很慢。排查数据库查询是否优化使用 Django Debug Toolbar 查看查询次数和耗时。确保对timestamp等字段建立了索引。是否拉取了过多历史数据前端默认请求的天数是否过长可以在API中限制最大天数或实现分页加载。考虑缓存对于变化不频繁的数据如商品列表可以使用 Django 的缓存框架。这个比价系统的骨架已经搭起来了从数据建模、稳健爬虫、任务调度到可视化展示形成了一个闭环。真正落地时最耗时间的部分往往是针对不同电商网站编写和维护那个fetch_product_price函数里的解析逻辑。建议你先用一两个网站跑通全流程把异常处理、日志记录和任务调度的基础打牢然后再逐步扩展爬虫的覆盖范围。记住系统的核心价值不在于能爬多少网站而在于抓取的数据是否准确、稳定以及能否为你提供清晰、及时的决策依据。