
简介这是一套基于Python构建的全栈式数据采集分析与可视化平台源码包涵盖网络爬虫抓取、多源异构数据存储、交互式前端界面以及深度学习手写数字识别模型适合高校教学演示、毕业设计及个人实战开发。包内共2002个文件压缩后约89.64MB以Markdown文档和JavaScript文件为主另含Python脚本、JSON配置、CSS样式及少量SQL、HTML和说明文档分别对应数据说明、前端交互逻辑、爬虫与模型代码、界面样式及数据库结构等。平台技术栈覆盖Scrapy/BeautifulSoup、MySQL/MongoDB、React/Vue、ECharts、TensorFlow/PyTorch等主流工具可帮助开发者快速理解数据应用全流程。目前已有73人学习浏览。资源附赠说明文档目录结构清晰便于按模块查阅既能用于课堂讲解数据采集到可视化闭环也可为二次开发和工程落地提供可复用基础。1. 基于Python的全栈式数据采集分析与可视化平台教学演示项目为什么值得认真搭基于Python的全栈式数据采集分析与可视化平台说白了就是把网络爬虫数据抓取、多源异构数据存储管理、交互式前端展示界面、深度学习手写数字识别模型这四块硬骨头用一套代码编成一条完整的数据流水线。这类项目在课程设计、毕业设计和公司内训里出现频率极高大家单拎出来都能写难的是把五套技术串成能演示、能交差、能继续加功能的整体。这个标题解决的是“五套技术各做各的”这个典型问题。爬虫抓完数据扔进 CSV前端展示时再手工导一次模型训练和网站完全脱节演示的时候打开三个窗口来回切观众看不出这是一个平台。把数据从网页一路流到浏览器画布上才是这个标题真正的价值。适合的人群很明确要做课程设计或毕业设计的在校生、要给新人做全栈示范的团队导师、以及想快速搭一个数据采集分析 Demo 来验证想法的 Python 开发者。新手能照着把每一层跑通熟手可以拿来当脚手架替换自己的算法和数据源。下面按我实际搭这类平台的顺序来写先讲选型再讲数据采集与存储然后是模型和前端联调最后是把五个模块踩过的坑一一拆开。保证你照着做完不是只有截图能看而是真能跑、能改、能现场演示的东西。2. 技术栈选型与整体架构五层选型理由和一条数据管道2.1 五层技术选型与“为什么不选”教学演示和实战开发场景里选型的第一原则不是“哪个技术最潮”而是“环境成本低、模块可替换、代码能讲清楚”。基于这个原则这套平台我一般固定用下面这组组合。技术层本方案选型选型理由不选主流替代方案的理由爬虫抓取Scrapy内置并发调度、下载中间件、Item Pipeline采集链路完整教学讲起来清晰单用 Requests 要自己写重试、去重和管道代码量翻倍数据存储SQLite零配置、单文件数据库整个库拷走就能跑演示不怕现场没网MySQL/PostgreSQL 需要服务端演示机器上一旦装不上就翻车后端接口Flask路由直白、上手快一个文件能讲清前后端交互FastAPI 的异步优势在本地演示场景体现不出来反而增加理解成本数据可视化PlotlyPython 端一行生成图表 JSON前端直接渲染交互式缩放拖拽免费自带纯 ECharts 需要写较多前端 JS教学演示时容易把重点带到前端细节里深度学习PyTorchtorchvision 内置 MNIST 数据集动态图调试直观导出 TorchScript 后 Flask 加载极简TensorFlow 版本接口变动大新手装环境时容易被兼容性问题劝退这五层之间通过结构化数据解耦。爬虫不知道前端长什么样前端不关心模型内部结构替换任何一层都不影响其他层。比如想把 SQLite 换成 MySQL只需要改 WriteHub 的实现Scrapy 的 Pipeline 和 Flask 的路由都不用动。2.2 数据流设计与目录骨架整个平台有两条数据链路。第一条是采集分析链路Scrapy 多源抓取Item Pipeline 清洗后交给统一存储层 WriteHub写入 SQLite 和 CSVFlask 从库里读取数据交给 Plotly 生成图表 JSON前端渲染成交互面板。第二条是模型识别链路PyTorch 训练手写数字识别模型导出 TorchScript 文件Flask 挂载模型提供识别接口浏览器 Canvas 画板把手写图片转成 base64 传给后端返回预测结果。目录结构按模块划分不按技术类型划分这样每个模块都能独立演进。project_root/ ├── app/ # 前端展示与接口层 │ ├── app.py # Flask 入口页面路由与识别接口 │ ├── models/ # 训练好的模型文件 │ │ └── mnist_scripted.pt # TorchScript 导出格式加载不依赖类定义 │ └── templates/ │ └── index.html # 交互式前端画板与图表页面 ├── spiders/ # Scrapy 爬虫目录 │ ├── multi_source_spider.py # 多源采集爬虫 │ └── pipelines.py # 清洗与入库管道 ├── storage/ │ └── write_hub.py # 统一存储层SQLite / CSV / JSON 读写 ├── data/ # 运行时生成的数据目录 │ ├── demo.db # SQLite 主库 │ └── items.csv # CSV 导出副本方便人工抽查 ├── train_mnist.py # 手写数字模型训练脚本 ├── requirements.txt └── run.sh # 一键启动脚本Scrapy 自带的 scrapy.cfg、items.py 没有列进来它们是框架自动生成的不影响理解。核心思路是把业务逻辑放在 spiders、storage、app 这三层里框架文件保持默认。2.3 环境准备先让空壳跑起来动手写代码之前先把虚拟环境锁好。依赖版本冲突是这个项目最常出问题的地方后面避坑章节会专门讲这里先养成好习惯。python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install -r requirements.txt # 一次性安装全部依赖 python -c import flask, scrapy, torch, torchvision, plotly; print(依赖 OK)requirements.txt 只需要写五个核心包flask、scrapy、torch、torchvision、plotly。版本号我建议不要一开始就锁定先把最新稳定版装通跑完整个平台后再用pip freeze requirements.lock把精确版本记录下来。PyTorch 在教学演示场景不需要 GPU 版CPU 版就够跑 MNIST 训练和推理。环境准备好之后先不写任何业务代码直接建好目录结构确认 import 全部通过再用python train_mnist.py这种空脚本占位确保框架搭起来的工程能编译能导入。这一步能过滤掉绝大多数环境问题后面每层开发都建立在“环境是好的”这个前提下。3. 网络爬虫数据抓取与多源异构存储Scrapy 多源采集到 WriteHub 统一读写层3.1 Scrapy 工程化item、spider、pipeline 一条线爬虫层最常见的错误是只用 Requests 写脚本抓完一个页面就结束。Scrapy 的价值在于把采集过程拆成可维护的组件尤其是多源采集时两个站点的解析逻辑互不干扰清洗规则写在管道里统一下发。# spiders/multi_source_spider.py import scrapy class MultiSourceSpider(scrapy.Spider): name multi_source allowed_domains [quotes.toscrape.com, books.toscrape.com] start_urls [ https://quotes.toscrape.com/, # 名言站列表页结构 https://books.toscrape.com/, # 书籍站结构完全不同 ] def parse(self, response): # 两个站点交给各自的解析函数避免把不同结构揉在一个函数里 if quotes in response.url: yield from self.parse_quotes(response) else: yield from self.parse_books(response) # 统一的翻页处理两个站点都用这个规则 next_page response.css(li.next a::attr(href)).get() if next_page: yield response.follow(next_page, self.parse) def parse_quotes(self, response): for quote in response.css(div.quote): yield { source: quotes, title: quote.css(span.text::text).get(), author: quote.css(small.author::text).get(), content: quote.css(span.text::text).get(), url: response.url, } def parse_books(self, response): for book in response.css(article.product_pod): yield { source: books, title: book.css(h3 a::attr(title)).get(), author: book.css(p.price_color::text).get(), content: book.css(p::text).get(), url: response.url, }注意 parse 方法用yield from把两种解析函数串起来Scrapy 会把每个字典当成一个 Item 自动交给 Pipeline。这里故意把两个站点的字段统一成 source、title、author、content、url 五个键多源异构的第一步就是在采集端做字段映射而不是等入库后再处理。# spiders/pipelines.py class NormalizePipeline: 清洗字段去空白、补默认值避免脏数据入库 def process_item(self, item, spider): for key, value in item.items(): if value is None: item[key] # 缺失字段给空串SQLite 入库不抛错 elif isinstance(value, str): item[key] value.strip().replace(\n, ) return itemPipeline 是 Scrapy 里最容易被忽略的组件。很多人喜欢在 spider 里顺手做字符串处理但多源场景下每个站点的脏数据规则不一样集中到 Pipeline 里才能统一治理。这个 NormalizePipeline 只做最基础的清洗你可以在后面追加去重 Pipeline 或关键词过滤 Pipeline。# settings.py 关键参数 BOT_NAME demo_platform ROBOTSTXT_OBEY True # 教学演示务必遵守 robots.txt避免对目标站造成压力 DOWNLOAD_DELAY 1.0 # 多站共用下载器演示场景稳定性优先 CONCURRENT_REQUESTS 4 # 并发调小防止被目标站封 IP FEED_EXPORT_ENCODING utf-8 # 导出 CSV/JSON 时强制 UTF-8中文不乱码 RETRY_TIMES 2 # 网络抖动时自动重试两次DOWNLOAD_DELAY 和 CONCURRENT_REQUESTS 是一对需要平衡的参数。演示场景我一般把延时设在 1 到 2 秒并发控制在 4 到 8这样抓几百条数据不会太慢也不容易触发目标站的反爬机制。实战场景想提速优先加代理中间件而不是盲目调高并发。3.2 多源异构存储WriteHub 统一读写层实现多源异构的“异构”主要体现在三个层面字段名不同、字段数量不同、字段类型不同。有的站有作者字段有的站没有有的 content 是字符串有的嵌套着标签。统一存储层的职责就是把它们全部收拢成一张宽表。# storage/write_hub.py import sqlite3, json, csv class WriteHub: 统一写入层把不同源的 dict 数据落到 sqlite/csv/json def __init__(self, db_pathdata/demo.db): self.db_path db_path self._init_db() def _init_db(self): conn sqlite3.connect(self.db_path) conn.execute( CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL, title TEXT, author TEXT, content TEXT, url TEXT, extra TEXT, -- 不固定字段统一塞进 JSON created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ) conn.commit() conn.close() def to_sqlite(self, item): conn sqlite3.connect(self.db_path) # extra 字段用 JSON 兜底后续加新字段不用改表结构 common_keys {source, title, author, content, url} extra {k: v for k, v in item.items() if k not in common_keys} conn.execute( INSERT INTO items (source, title, author, content, url, extra) VALUES (?, ?, ?, ?, ?, ?), (item.get(source, ), item.get(title, ), item.get(author, ), item.get(content, ), item.get(url, ), json.dumps(extra, ensure_asciiFalse)) ) conn.commit() conn.close() def to_csv(self, item, pathdata/items.csv): # CSV 用于人工抽查字段固定为统一列 fieldnames [source, title, author, content, url] with open(path, a, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnamesfieldnames) if f.tell() 0: writer.writeheader() writer.writerow({k: item.get(k, ) for k in fieldnames})WriteHub 的设计要点是 extra 字段。很多教学项目会不断往表里加列加到后面表结构膨胀前端查数据还要写一堆条件判断。把不稳定字段塞进 JSON 后表结构稳定了查询也不受影响。需要按不固定字段筛选时SQLite 支持json_extract(extra, $.某个字段)照样能查。Scrapy 的 Pipeline 里调用 WriteHub 很简单在 spider 的引用里传一个全局实例或者直接在 pipeline 的open_spider里初始化连接。注意连接不要每次请求都新建量大的时候 SQLite 会频繁锁库。3.3 运行与验证怎么确认采集和入库都成功运行爬虫的命令很基础但我每次都会强调跑完之后看的是入库数量不是爬虫日志。scrapy crawl multi_source -O data/items.json python -c from storage.write_hub import WriteHub wh WriteHub() rows wh.query_count() # 需要自己在 WriteHub 里加一个 count 方法 print(fSQLite 共入库 {rows} 条) WriteHub 里可以补一个极简的查询方法def query_count(self): conn sqlite3.connect(self.db_path); return conn.execute(SELECT COUNT(*) FROM items).fetchone()[0]。验证时关注三个数字Scrapy 日志里的item_scraped_count、SQLite 的总行数、CSV 文件的行数。三者应该一致不一致说明有数据在某个环节丢了。翻车高发点是编码。爬虫抓回来的是 bytes如果站点没声明 charsetScrapy 默认按 ISO-8859-1 解码中文直接变乱码。遇到这种情况在 spider 的start_requests里给 Response 指定response.encoding utf-8或者直接在 settings 里设DEFAULT_REQUEST_HEADERS带Accept-Charset头。判断有没有解对直接看库里的 title 字段而不是看终端打印。4. 手写数字识别模型与交互式前端展示PyTorch 训练到浏览器画板识别4.1 训练与导出为什么手写数字识别选 CNN 而不是全连接手写数字识别是深度学习教学里最经典的案例MNIST 数据集 28x28 像素全连接网络也能跑到 94% 左右但 CNN 能到 99%。差距来自卷积的局部感受野和参数共享数字的笔画特征横、竖、弧线在哪个位置都成立卷积把这种平移不变性直接编码进网络结构里。# train_mnist.py import torch import torch.nn as nn from torchvision import datasets, transforms class DigitNet(nn.Module): def __init__(self): super().__init__() # 输入 1x28x28两层卷积池化后变成 64x7x7 self.features nn.Sequential( nn.Conv2d(1, 32, 3, padding1), # - 32x28x28 nn.ReLU(), nn.MaxPool2d(2), # - 32x14x14 nn.Conv2d(32, 64, 3, padding1), # - 64x14x14 nn.ReLU(), nn.MaxPool2d(2), # - 64x7x7 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 7 * 7, 128), nn.ReLU(), nn.Dropout(0.25), # 教学演示也值得加防过拟合 nn.Linear(128, 10), ) def forward(self, x): return self.classifier(self.features(x)) def main(): transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)), # MNIST 官方均值和标准差 ]) train_data datasets.MNIST(data/mnist, trainTrue, downloadTrue, transformtransform) train_loader torch.utils.data.DataLoader(train_data, batch_size64, shuffleTrue) model DigitNet() optimizer torch.optim.Adam(model.parameters(), lr0.001) criterion nn.CrossEntropyLoss() for epoch in range(3): # CPU 演示 3 轮足够几分钟跑完 model.train() for images, labels in train_loader: optimizer.zero_grad() loss criterion(model(images), labels) loss.backward() optimizer.step() print(fepoch {epoch 1} loss{loss.item():.4f}) scripted torch.jit.script(model) scripted.save(app/models/mnist_scripted.pt)参数选择上batch_size 64、学习率 0.001 的 Adam、3 个 epoch是 CPU 教学场景的甜点区。调大 batch_size 到 128 能跑更快但梯度更新次数变少3 轮学不充分准确率会掉 0.5 到 1 个百分点。epoch 加到 5 准确率还能提升但演示现场等待时间太长就没必要了。导出用torch.jit.script而不是直接torch.save(model.state_dict())这是我从跑通到演示踩过坑之后固定下来的做法。state_dict 加载时需要重新实例化模型类并手动 load脚本文件里类定义一改动就报错TorchScript 把模型结构和权重打包成一个文件Flask 端拿到文件直接加载不需要 import 训练脚本里的任何东西。4.2 Flask 挂载与 Plotly 交互面板模型变成能点的页面模型训练完下一步是让浏览器里画一个数字后端识别并返回结果。这里用 Flask 提供两个接口一个返回采集数据的 Plotly 图表 JSON一个接收手写图片 base64 返回预测结果。# app/app.py import os, base64, io, json import torch from flask import Flask, render_template, request, jsonify from PIL import Image import plotly.graph_objects as go app Flask(__name__) _model None MODEL_PATH os.path.join(os.path.dirname(__file__), models, mnist_scripted.pt) def get_model(): global _model if _model is None: _model torch.jit.load(MODEL_PATH, map_locationcpu) _model.eval() # 固定 dropout 和 batchnorm 行为 return _model app.route(/) def index(): return render_template(index.html) app.route(/api/recognize, methods[POST]) def recognize(): data request.get_json() image_b64 data.get(image, ) if not image_b64: return jsonify({error: empty image}), 400 # 前端传的是白底黑字的 PNG转换成 MNIST 训练样本的黑底白字 img Image.open(io.BytesIO(base64.b64decode(image_b64))).convert(L) img img.resize((28, 28), Image.Resampling.LANCZOS) tensor torch.tensor(list(img.getdata()), dtypetorch.float32).view(1, 1, 28, 28) / 255.0 tensor 1.0 - tensor with torch.inference_mode(): logits get_model()(tensor) pred int(logits.argmax(dim1).item()) prob float(torch.softmax(logits, dim1).max().item()) return jsonify({prediction: pred, confidence: round(prob, 4)}) app.route(/api/chart) def chart(): # 从 WriteHub 读取数据Plotly 生成图表 JSON前端直接渲染 import sqlite3 conn sqlite3.connect(os.path.join(os.path.dirname(__file__), .., data, demo.db)) rows conn.execute(SELECT source, COUNT(*) FROM items GROUP BY source).fetchall() conn.close() fig go.Figure(data[go.Bar(x[r[0] for r in rows], y[r[1] for r in rows])]) return jsonify(json.loads(fig.to_json()))代码里有个细节get_model()用全局变量缓存模型对象。Flask 默认多线程处理请求如果每次请求都torch.jit.load磁盘 IO 和模型初始化耗时会拖垮接口并发一上来甚至可能内存暴涨。全局变量加载一次后所有线程共享推理CPU 下单个请求稳定在 20 到 50 毫秒。前端页面核心是一个 Canvas 画板加一小段图表渲染逻辑。画板的处理是整个识别率的关键不能直接把 280x280 的画板缩到 28x28 就提交必须做包围盒裁剪和居中。// templates/index.html 内部 script function canvasToDigitData() { const src document.getElementById(board); const ctx src.getContext(2d); const imageData ctx.getImageData(0, 0, 280, 280); // 求手写笔迹的包围盒背景为白色笔迹为深色 let minX 280, maxX 0, minY 280, maxY 0; for (let y 0; y 280; y) { for (let x 0; x 280; x) { const gray imageData.data[(y * 280 x) * 4]; if (gray 128) { if (x minX) minX x; if (x maxX) maxX x; if (y minY) minY y; if (y maxY) maxY y; } } } if (maxX minX) return ; // 空画板直接返回不发请求 // 裁剪笔迹区域等比缩放后居中贴到 28x28 画布 const srcCanvas document.createElement(canvas); const w maxX - minX 1, h maxY - minY 1; srcCanvas.width w; srcCanvas.height h; srcCanvas.getContext(2d).drawImage(src, minX, minY, w, h, 0, 0, w, h); const dstCanvas document.createElement(canvas); dstCanvas.width 28; dstCanvas.height 28; const dstCtx dstCanvas.getContext(2d); dstCtx.fillStyle #ffffff; dstCtx.fillRect(0, 0, 28, 28); const scale 20 / Math.max(w, h); dstCtx.drawImage(srcCanvas, (28 - w * scale) / 2, (28 - h * scale) / 2, w * scale, h * scale); return dstCanvas.toDataURL(image/png).split(,)[1]; }这段 JS 做了三件事找笔迹边界、等比缩放、居中贴到 28x28。MNIST 的训练样本本身就是“数字大致在中间、占画布约八成”的分布前端预处理越接近这个分布识别率越高。这也是在没有做任何数据增强时让识别率从惨不忍睹到像样的最关键一步。4.3 联调验证画一个数字看整条链路启动命令和验证流程都不复杂但顺序有讲究。cd project_root python train_mnist.py # 训练并导出模型 python app/app.py # 启动 Flask默认端口 5000浏览器打开http://localhost:5000会看到两个区域上半部分是采集数据的柱状图下半部分是手写画板。先画一个 7点识别预测结果和置信度会显示在画板旁边。然后清空再画一个 3检查置信度是否正常波动。这一步如果识别失败先别怀疑模型用浏览器开发者工具看 Network 面板里/api/recognize的请求和响应。常见情况是 base64 数据没传全、画板在白色背景上画白色笔迹、或者 Canvas 转出来的是空图。模型本身在 MNIST 测试集上有 99% 准确率前端问题占九成。5. 避坑与排查数据管道、模型服务与前端联调的 5 个真实踩坑5.1 入库全是乱码SQLite 里查出来是“锟斤拷”现象爬虫抓取时终端打印中文正常进 SQLite 后查出来全是锟斤拷这类乱码CSV 导出更是没法看。原因这个坑通常不在 SQLite而在爬虫解码阶段。目标站返回的 HTTP 头没有声明 charset或者声明了ISO-8859-1Scrapy 按该编码解码中文 bytes 被错误解析。另一种情况是json.dumps写文件时没有加ensure_asciiFalse中文被转成\uXXXX转义序列写入数据库后看起来像乱码。解决在 spider 里显式声明编码。Scrapy 的 Response 对象有encoding属性直接设置为utf-8。WriteHub 的to_sqlite里存 JSON 时统一加ensure_asciiFalse。最关键的是验证手段入库后立刻用SELECT title FROM items LIMIT 5抽查不要只在终端看日志。# 在 spider 的 parse 开头加一行 response.encoding utf-8 # 强制按 UTF-8 解码覆盖 HTTP 头的错误声明5.2 多源字段漂移有的站有作者有的站没有入库直接报错现象单跑名言站一切正常加了书籍站之后 Pipeline 报KeyError或 SQLite 插入失败提示列数不匹配。原因两个站的字段天然不一致名言站有 author书籍站只有价格spider 里某个字段取不到None而 Pipeline 或 SQLite 插入语句写死了字段列表。这就是多源异构最典型的“schema 漂移”。解决在采集端统一映射五个核心字段用.get()加默认值兜底WriteHub 的建表语句里加extra字段接收所有非核心字段。这条原则要贯彻到每一层前端展示时也只读取核心字段需要看扩展字段再从 extra 里取。# NormalizePipeline 里加一段兜底逻辑 required [source, title, author, content, url] for field in required: item.setdefault(field, ) # 缺失字段统一给空串不抛异常5.3 模型接口一并发就崩单测好好的连续画两个数字就 500现象本地用测试脚本调/api/recognize正常浏览器里画完一个数字识别成功紧接着画第二个就报 500刷新后偶尔恢复。原因Flask 的 dev server 默认支持多线程而你的代码里如果在每个请求里都执行torch.jit.load频繁加载模型会导致资源竞争和内存碎片。更隐蔽的是有的新手把模型加载写在了请求函数内部每次推理都重新创建计算图。解决模型加载放到全局变量里进程启动时加载一次之后所有请求复用。推理外面套torch.inference_mode()不追踪梯度内存占用和耗时都会降下来。# 不要在每个请求里 load 模型 def recognize(): model torch.jit.load(MODEL_PATH) # 错误写法高并发下必崩 ...5.4 Canvas 手写体识别率不足 50%测试集 99%浏览器写个 7 识别成 2现象MNIST 测试集准确率 99%前端画板随便写一个数字识别率不到一半越想写端正越不对。原因模型学的是 28x28 黑底白字、数字居中占 70% 面积、笔画粗细均匀的 MNIST 风格。浏览器画板是白底黑字、笔画可能偏细、数字可能偏左偏小直接缩放提交等于给模型看了一类完全没训练过的样本。解决前端必须做完整预处理。按 4.2 节的方式先求包围盒、裁剪、等比缩放、居中贴画布后端再做一次反色。笔画粗细也很关键如果画板笔迹太细建议把lineWidth从 2 调到 5 到 8让笔迹宽度接近 MNIST 训练样本的笔画比例。# 后端反色一行搞定 tensor 1.0 - tensor # 白底黑字 - 黑底白字5.5 依赖版本冲突装完 Scrapy 后 PyTorch import 报错现象先装了 PyTorch 再装 Scrapy装完以后import torch报illegal instruction或numpy相关错误Flask 也起不来。原因pip 在安装新包时会自动升级依赖Scrapy 的依赖 lxml、cryptography 等会拉高 numpy 版本而 PyTorch 内置的 numpy 二进制接口不兼容新版本。Python 3.12 上 Scrapy 的某些依赖还会触发编译错误环境直接报废。解决所有依赖用同一个 requirements.txt 一次装完不用 pip 单独补装。PyTorch 安装显式指定 CPU 源。装完后用一条 import 命令全部验证再跑训练脚本和爬虫脚本。如果已经翻车最快的后悔药是删掉虚拟环境重建不要试图手动修。pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu python -c import flask, scrapy, torch, torchvision, plotly; print(ok)6. 演示现场不翻车的三个习惯自检脚本、数据快照与启动顺序教学演示和实战项目最大的区别是你只有一次展示机会没有时间在台上装环境。我的习惯是写一个自检脚本把所有模块的状态一次性检查完再决定要不要上场。# check_platform.py import os, sqlite3 errors [] # 1. 检查数据库和采集数据 db_path data/demo.db if not os.path.exists(db_path): errors.append(数据库不存在先跑爬虫采集) else: count sqlite3.connect(db_path).execute(SELECT COUNT(*) FROM items).fetchone()[0] if count 100: errors.append(f数据量偏少{count} 条演示可能不够丰满) # 2. 检查模型文件 if not os.path.exists(app/models/mnist_scripted.pt): errors.append(模型文件不存在先跑 train_mnist.py) else: import torch torch.jit.load(app/models/mnist_scripted.pt, map_locationcpu) # 3. 检查端口占用 import socket s socket.socket() try: s.bind((127.0.0.1, 5000)) except OSError: errors.append(5000 端口被占用关掉其他服务再启动) if errors: print(自检未通过) for e in errors: print( -, e) else: print(自检全部通过可以放心演示)自检脚本的价值不只是演示前跑一遍它本身就是项目质量的一部分。团队接手时跑一次自检就知道环境全不全比读 README 快得多。第二个习惯是数据快照。采集环节抓完一批数据后把demo.db复制一份存为data/demo_snapshot.db演示前用快照覆盖工作库。这样现场网络断了、目标站改版了、爬虫被封了都不影响你展示前端和模型。爬虫在演示中只是锦上添花不要让它成为风险的唯一来源。第三个习惯是固定启动顺序。先跑自检再跑训练脚本检查模型存在性最后启动 Flask。我在启动脚本里加了一句“模型缺失才训练”避免每次启动都等训练。#!/bin/bash set -e python check_platform.py [ -f app/models/mnist_scripted.pt ] || python train_mnist.py cd app flask run --host 0.0.0.0 --port 5000演示现场的排查顺序也要固定先看浏览器 Network 面板确认请求有没有发出去再看 Flask 日志确认路由有没有被命中最后看数据库有没有数据。我经历过一次模型文件忘拷到演示机器、现场发现画板识别一直报错的情况就是因为没跑自检脚本直接启动 Flask 导致。从那以后自检成了我交付任何平台类项目的第一道工序。这套全栈式数据采集分析与可视化平台每一层都是可以独立替换的模块。爬虫换数据源存储换数据库模型换更深的网络前端换 ECharts都不影响其他层工作。把它当成脚手架后面想加文本分类、情感分析、数据大屏都在这个骨架上长肉。希望帮到你。本文还有配套的精品资源点击获取