资讯动态

Python全栈数据采集分析与可视化平台:从爬虫到模型推理的教学实践

发布时间:2026/10/20 11:50:29 来源:尧图企业网站定制
简介一份基于Python的全栈式数据采集分析与可视化平台源码包面向高校教学、课程设计与个人实战开发。平台集成网络爬虫抓取、多源异构数据存储、交互式前端展示与深度学习手写数字识别四大模块技术栈涉及Scrapy/BeautifulSoup、MySQL/PostgreSQL、React/Vue与TensorFlow/PyTorch演示了从数据采集、清洗入库到可视化呈现的完整闭环适合作为毕业设计或项目实训的参考蓝本。包内共2002个文件以md说明文档、js前端逻辑、json配置和py脚本为主体另有css样式、sql表结构、html页面及docx附赠资料结构清晰便于按模块查阅。整体大小89.64MB目前已有73人学习下载。借助这些代码与文档可快速理解爬虫规则编写、多源数据管理、可视化图表搭建及模型推理接入等项目落地细节。1. 这个标题拆开看一台笔记本就能跑完的“数据全链路”教学平台基于Python的全栈式数据采集分析与可视化平台拆开就是四件事网络爬虫抓数据多源异构方式存进库Flask 接口喂给交互式前端展示界面再加一个深度学习手写数字识别模型。见过不少团队做这类平台把最多时间花在调模型精度上真正卡住进度的却是接口约定、编码和路径问题——模型反而是最省心的一块。它典型的落地场景是教学演示与实战开发教师需要一条每一步都能停下来讲的完整链路从抓取、入库、统计到页面展示层层可见开发者需要一套换数据源、换存储、换模型都不动主结构的骨架。本质上是台数据小篷车四个轮子各管一段一起跑起来才算数。下文按数据入口到交互出口的顺序逐段搭建最后给一份参数化配置方案和一串踩坑记录。一台没有 GPU 的普通笔记本就能跑完全程。2. 架构设计与技术选型把四块功能拼成一条可复现的数据流水线2.1 四个模块的职责边界与数据流这类项目最大的隐性要求不是“每个模块多厉害”而是“模块之间能互相替换”。我一般会立一条规矩各层之间只认表结构和接口签名不认对方的内部实现。爬虫不知道数据存在哪个库Flask 不知道页面长什么样模型不知道数据是从哪张表来的。按这个规矩四个模块的边界很清楚。采集层只负责把网页解析成结构化的 dict 列表写库动作交给存储层存储层只负责建表、写入、查询不关心数据来源服务层用 Flask 只读库并输出 JSON展示层用原生 HTML 加图表库只调接口。模型单独成层训练脚本和推理接口分离推理接口挂在 Flask 下面和普通数据接口共用一套路由。数据流只有一条主线但每个环节都能独立拆下来测试。爬虫抓到一条新闻后逐条写入 SQLiteFlask 聚合统计后吐给前端前端 fetch 渲染图表。手写识别走另一条支线画板把图转成 base64POST 给 /api/predict返回的 label 和置信度直接显示在页面上。教学演示时把爬虫停掉手动往库里插几行数据前端照样能跑——这是验证链路是否健康的最快办法。建议项目目录按层分开一上来就铺好省得后面改路径改到怀疑人生。project/ ├── scraper/ # 采集层爬虫与解析器 │ ├── config.json # 目标站点、选择器、抓取间隔 │ └── scraper.py ├── storage/ # 存储层建表、插入、查询 │ └── storage.py ├── server/ # 服务层Flask 接口 │ ├── app.py │ ├── static/ │ │ └── echarts.min.js │ └── templates/ │ └── dashboard.html ├── model/ # 算法层训练与推理 │ ├── train.py │ └── predictor.py └── data/ # 数据库文件和模型权重storage 和 model 两个目录不互相 import谁要数据都走 SQLite 或 HTTP 接口。课程演示时学生可以只改 scraper 里的解析函数就换一个数据源进阶练习时把 SQLite 换成 MySQL 或者把 sqlite3 换成 SQLAlchemy只动 storage 层。这样分层的好处是平台的价值不在单点技术而在链路本身链路清晰了每一步都能展开成独立作业。2.2 技术栈选型requests、SQLite、Flask、ECharts、PyTorch 各自的位置选型这件事最忌讳一上来就上重型框架。教学演示场景里工具链越短越容易让人把注意力放在数据流上。模块备选方案本平台选用选型理由爬虫Scrapy / requestsBeautifulSoup / httpxrequests BeautifulSoup请求与解析分离直观重试、编码、频率控制都在视野内存储MySQL / SQLite / MongoDB / JSON 文件SQLite raw_json 字段零部署、单文件分发演示环境不会出连接问题后端Flask / FastAPI / DjangoFlask路由写法最贴近教学模板渲染与 JSON 接口并存前端Vue / React / 原生ECharts原生 HTML ECharts不引入 node 工具链双击模板就能看效果深度学习PyTorch / TensorFlow / KerasPyTorch网络定义直观CPU 可训加载与推理代码量最少两个容易忽略的点。一是爬虫标题里既然点到技术栈涵盖 Scrapy 方向我把 Scrapy 放进备选。它适合大规模站点抓取、自带调度与去重但配置文件和中间件对初学者是个黑匣子。教学演示阶段用 requests把请求、解析、入库三个动作摊开在屏幕上讲得清也改得动等要抓上千个页面、需要断点续爬时再切 Scrapy模块边界不变替换成本很低。二是存储。SQLite 够用是因为这个平台的演示数据量级通常在一万条以内单文件拷贝即走学生机不用装数据库服务。MySQL 留给实战扩展把 storage.py 里的 sqlite3 调用换成 pymysql接口签名保持一致上层完全无感。后悔药我已经替你们吃过了——不要在第一个版本里同时引入 ORM、消息队列和容器编排那会让教学演示变成基础设施排错现场。3. 网络爬虫与多源异构数据存储从抓取到落库的完整链路3.1 一个可配置的通用爬虫请求层与解析层分离爬虫是整个平台的数据入口也是初学者最容易写成一坨的地方。常见翻车姿势是把请求、解析、存储写进同一个函数换一个目标站点就要重写整个脚本。正确做法是让请求层和解析层各干各的请求层管超时、编码、重试解析层只负责把 HTML 变成 dict 列表。# scraper/scraper.py import requests from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept-Language: zh-CN,zh;q0.9, } def fetch_html(url, timeout10): 统一的请求入口超时、编码、异常集中处理 try: resp requests.get(url, headersHEADERS, timeouttimeout) resp.raise_for_status() resp.encoding resp.apparent_encoding # 按页面内容推断编码避免中文乱码 return resp.text except requests.RequestException as e: print(f[抓取失败] {url} - {e}) return None def parse_news(html): 解析层与请求层分离换站点只需改这里的 select 规则 soup BeautifulSoup(html, lxml) items [] for li in soup.select(ul.news-list li): a li.find(a) if not a: continue items.append({ title: a.get_text(stripTrue), link: a.get(href, ), source: demo_site, }) return itemsfetch_html 里的 timeout 参数决定单次请求最久等多久教学演示建议 10 秒太大容易让演示卡在等待上。resp.encoding resp.apparent_encoding 是处理中文站点的关键一行很多教学站点的响应头不带 charset 或写错requests 默认按响应头解码就会乱。解析层的 select 规则和具体平台绑定换数据源只改这里请求层一行不动。接着在主入口里把抓取和解析串起来先只打印结果确认解析规则没问题再谈入库。if __name__ __main__: TARGET_URL https://example.com/news/ html fetch_html(TARGET_URL) if html: items parse_news(html) for item in items: print(item)这一步的价值是先把“能不能抓到、能不能解析出来”确认掉避免后面把网络问题和代码问题混在一起查。教学演示时我会把抓取频率控制在 3060 秒一轮太频繁容易被目标站点限流而且演示现场根本不需要高并发。提示抓取频率别贪快教学场景一轮 30 秒完全够用。反爬检测有时很玄学但把 UA、频率、超时这三样做好绝大多数演示站点都能正常抓。3.2 异构数据统一入库公共列加 raw_json 兜底多源异构的意思是来源 A 的每条数据有标题和链接来源 B 多了作者字段来源 C 还有评论数字段。如果每来一个新源就加一张表表结构会失控。教学项目里性价比最高的方案是“公共列抽出来 raw_json 兜底”所有源共有的字段建列独有的字段塞进 JSON 字符串查询时再按需抽列。# storage/storage.py import sqlite3 import json def get_conn(db_pathdata/platform.db, timeout30): timeout 设为 30 秒避免并发读写时直接抛 database is locked conn sqlite3.connect(db_path, timeouttimeout) conn.row_factory sqlite3.Row # 查询结果按列名访问 return conn def init_db(conn): conn.execute( CREATE TABLE IF NOT EXISTS scraped_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL, title TEXT, link TEXT, raw_json TEXT, created_at TEXT DEFAULT (datetime(now, localtime)) ) ) conn.execute( CREATE UNIQUE INDEX IF NOT EXISTS idx_source_link ON scraped_data(source, link) ) conn.commit() def insert_item(conn, source, item): 重复链接用 INSERT OR IGNORE 跳过实现简单的按源去重 conn.execute( INSERT OR IGNORE INTO scraped_data (source, title, link, raw_json) VALUES (?, ?, ?, ?), (source, item.get(title, ), item.get(link, ), json.dumps(item, ensure_asciiFalse)), ) conn.commit()source 字段标记数据来自哪个爬虫去重索引按“来源 链接”建同一篇文章被反复抓只会入库一次。raw_json 存完整原始条目ensure_asciiFalse 保证中文在库里可读。created_at 用 SQLite 内置时间函数自动生成省掉应用层传时间戳的代码。入库这一步有个常见误解不是所有字段都要抽成列。列越多新数据源接入越麻烦。只把“所有源都会用到、且经常作为查询条件”的字段抽出来其余全部塞进 raw_json。等演示时需要按作者筛选再从 JSON 里把 author 抽列这就是后面要讲的数据清洗动作。3.3 从库到 DataFrame清洗与统计的标准动作数据落库后平台需要一个对外输出统计结果的入口。教学场景最自然的做法是让 SQLite 和 pandas 接上把查询结果直接变 DataFrame清洗逻辑写在明面上。import sqlite3 import pandas as pd import json conn sqlite3.connect(data/platform.db) rows conn.execute(SELECT source, title, link, raw_json FROM scraped_data).fetchall() df pd.DataFrame(rows, columns[source, title, link, raw_json]) df[author] df[raw_json].apply( lambda s: json.loads(s).get(author, 未知) ) df[content_len] df[title].str.len() source_stats df.groupby(source).size().reset_index(namecnt) print(source_stats)这里的关键是把 raw_json 当“延迟解析字段”用入库时不拆分析时再通过 apply 抽列。这样无论未来接多少异构数据源清洗逻辑都只集中在这一个环节不会散落在爬虫代码里。source_stats 这张小表就是前端统计接口的数据基础Flask 端跑同一条 SQL 也能拿到同样的结果。4. 交互式前端与手写数字识别模型从接口联调到推理上线的最后一公里4.1 Flask 数据接口统计接口和列表接口怎么设计Flask 这层只做一件事把存储层的数据变成前端能直接消费的 JSON。接口命名统一走 /api 前缀返回结构固定成 {code, data, msg} 三件套前端判断 code 为 0 再渲染数据。这样错误信息也能走同一条通道而不是让前端去猜状态码。# server/app.py from flask import Flask, jsonify, request import sqlite3 app Flask(__name__) DB_PATH data/platform.db def query_db(sql, args()): 统一查询入口避免每个接口重复写连接逻辑 conn sqlite3.connect(DB_PATH, timeout30) conn.row_factory sqlite3.Row rows conn.execute(sql, args).fetchall() conn.close() return [dict(r) for r in rows] app.route(/api/stats) def api_stats(): 统计接口按来源分组计数大屏初始化时调用 rows query_db( SELECT source, COUNT(*) AS cnt FROM scraped_data GROUP BY source ) return jsonify({code: 0, data: rows}) app.route(/api/items) def api_items(): 列表接口来源筛选 分页两个参数都有默认值 source request.args.get(source, ) page int(request.args.get(page, 1)) limit int(request.args.get(limit, 20)) offset (page - 1) * limit sql SELECT id, source, title, link, created_at FROM scraped_data args [] if source: sql WHERE source ? args.append(source) sql LIMIT ? OFFSET ? args [limit, offset] rows query_db(sql, args) return jsonify({code: 0, data: rows, page: page})query_db 把连接、查询、关闭统一收敛接口函数里只写 SQL。分页用 LIMIT 和 OFFSET演示数据量小性能完全够。source 筛选参数是可选的空字符串表示不过滤这样前端可以同时支持“看全部”和“按来源看”。注意每个请求都新建连接是 SQLite 在多线程下的安全写法但一定要记得关闭连接。忘了 close 会积累句柄演示到一半接口就变慢重启才恢复。4.2 前端展示层ECharts 图表与手写画板前端不做复杂框架一个 dashboard.html 页面搞定。两块核心区域一块是数据可视化大屏用 ECharts 画各来源的饼图另一块是手写数字画板用 canvas 接收鼠标输入识别结果显示在旁边。两块都只和 Flask 接口打交道。!-- server/templates/dashboard.html -- !DOCTYPE html html langzh head meta charsetUTF-8 title数据采集分析可视化平台/title script src/static/echarts.min.js/script /head body div idsourceChart stylewidth:100%;height:420px;/div canvas idboard width280 height280 styleborder:1px solid #999;/canvas button idclearBtn清空画板/button div idresult识别结果/div script // 数据大屏拉取统计接口后渲染饼图 fetch(/api/stats) .then(r r.json()) .then(res { if (res.code ! 0) return; const chart echarts.init(document.getElementById(sourceChart)); chart.setOption({ series: [{ type: pie, radius: 60%, data: res.data.map(d ({ name: d.source, value: d.cnt })) }] }); }); // 手写画板黑底白字与 MNIST 训练集保持一致 const canvas document.getElementById(board); const ctx canvas.getContext(2d); ctx.fillStyle #000; ctx.fillRect(0, 0, 280, 280); ctx.strokeStyle #fff; ctx.lineWidth 18; ctx.lineCap round; let drawing false; canvas.addEventListener(mousedown, e { drawing true; }); canvas.addEventListener(mouseup, () { drawing false; }); canvas.addEventListener(mousemove, e { if (!drawing) return; const rect canvas.getBoundingClientRect(); ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top); ctx.stroke(); }); document.getElementById(clearBtn).onclick () { ctx.fillStyle #000; ctx.fillRect(0, 0, 280, 280); }; // 点击识别canvas 转 base64 后 POST 给模型接口 async function recognize() { const image canvas.toDataURL(image/png); // data:image/png;base64,... const res await fetch(/api/predict, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ image }) }); const json await res.json(); document.getElementById(result).textContent 识别结果${json.data.label}置信度${json.data.confidence}; } /script /body /htmlECharts 容器必须给固定高度否则 init 时宽度高度为 0图表会白屏。canvas 直接用 280x280 的画板后端再缩放到 28x28黑底白字是刻意对齐 MNIST 训练集的风格这一步不做后面模型预测全是乱的。前端不直接 import 模型只转 base64 发请求这是前后端分离在教学项目里的最小落地形式。4.3 手写数字识别模型训练、导出与推理封装手写数字识别用 MNIST 数据集模型写一个轻量 CNN 就够了参数量约两万CPU 上跑三五个 epoch 就能到 97% 以上的准确率。教学演示的关键不是追精度而是把“训练 — 导出 — 加载 — 推理”这条线走通。# model/train.py import torch import torch.nn as nn from torchvision import datasets, transforms class Net(nn.Module): 轻量 CNN两层卷积 一层全连接适合 CPU 训练 def __init__(self): super().__init__() self.conv nn.Sequential( nn.Conv2d(1, 16, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(16, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), ) self.fc nn.Linear(32 * 7 * 7, 10) def forward(self, x): x self.conv(x) x torch.flatten(x, 1) return self.fc(x) def main(): transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) # MNIST 全局均值与标准差 ]) train_set datasets.MNIST(./data, trainTrue, downloadTrue, transformtransform) loader torch.utils.data.DataLoader(train_set, batch_size64, shuffleTrue) model Net() optimizer torch.optim.Adam(model.parameters(), lr0.001) loss_fn nn.CrossEntropyLoss() for epoch in range(3): model.train() for x, y in loader: optimizer.zero_grad() out model(x) loss loss_fn(out, y) loss.backward() optimizer.step() print(fepoch{epoch 1} loss{loss.item():.4f}) torch.save(model.state_dict(), data/mnist_cnn.pt) if __name__ __main__: main()训练代码里最不能改错的就是 Normalize 的均值和标准差。0.1307 和 0.3081 是 MNIST 数据集全量统计出来的常数训练用它们推理时必须用同一组。模型只保存 state_dict不保存整个模型对象这样换个 Python 或 PyTorch 小版本也能加载。推理端单独放一个预测模块加载权重后立即切到 eval 模式并在推理时包住 torch.no_grad省内存也避免误更新参数。# model/predictor.py import base64 import io import torch import torchvision.transforms as T from PIL import Image from train import Net device torch.device(cpu) model Net() model.load_state_dict(torch.load(data/mnist_cnn.pt, map_locationdevice)) model.eval() # 必须调用否则 BatchNorm/Dropout 行为不一致 transform T.Compose([ T.Resize((28, 28)), T.ToTensor(), T.Normalize((0.1307,), (0.3081,)), ]) def predict_from_base64(b64_str): 前端传回的 base64 - PIL 灰度图 - 模型推理 img Image.open(io.BytesIO(base64.b64decode(b64_str.split(,)[-1]))).convert(L) x transform(img).unsqueeze(0) with torch.no_grad(): logits model(x) prob torch.softmax(logits, dim1)[0] label int(prob.argmax()) conf float(prob[label].item()) return {label: label, confidence: round(conf, 4)}map_locationcpu 这行是后悔药级别的关键训练机器如果用了 GPU 保存的权重在只有 CPU 的演示机器上加载会直接报错加上它就能把权重强制加载到 CPU。前端画板传过来的 base64 带 data:image/png;base64, 前缀split(,)[-1] 只取纯数据段。置信度取 softmax 后最大值四舍五入到四位小数前端直接展示。最后在 Flask 里挂上预测接口整个链路就通了。app.route(/api/predict, methods[POST]) def api_predict(): data request.get_json() if not data or image not in data: return jsonify({code: 1, msg: 缺少 image 字段}) result predict_from_base64(data[image]) return jsonify({code: 0, data: result})预测接口的输入是画板图片输出是标签和置信度数据接口和模型接口并存于同一个 Flask 进程演示时不需要开第二个服务这是教学场景最省事的部署形态。5. 避坑四模块拼成一个平台时的 5 个高频问题与排查四块功能单独跑都没问题拼起来就翻车问题基本集中在编码、路径、状态切换和并发这四类。下面是我在这个方向上攒下的血泪经验。5.1 爬虫入库后中文乱码现象抓回来的标题在终端里是正常的写进 SQLite 后变成æ–°é—»这类乱码或者读出来一半中文一半问号。原因requests 默认按响应头里的 charset 解码很多教学站点响应头不带 charset 或写错了requests 就会解错另一种情况是插入时把 str 和 bytes 混用Python 3 下 str 走 UTF-8 没问题一旦混入 bytes 就会出乱码。解决请求层统一在拿到响应后立刻执行resp.encoding resp.apparent_encoding让 requests 根据页面内容去推断编码插入时把 title、link 所有字段统一转成 strraw_json 用json.dumps(item, ensure_asciiFalse)序列化。排查时可以先用 sqlite3 命令行手工查一条记录确认是写入前的问题还是读取展示的问题别一上来就改前端。5.2 ECharts 图表空白或报 echarts is not defined现象页面打开后图表区域一片空白F12 控制台报echarts is not defined或者图表容器有高度但画不出来。原因两种情况最常见。一是 echarts.min.js 没加载成功引用了外网 CDN 但演示现场没外网或者路径写错。二是图表容器的父元素初始是隐藏的或者容器本身没给高度echarts.init 时拿到的宽度是 0图表自然画不出来。解决教学演示环境一律把 echarts.min.js 下载到本地 static 目录用相对路径/static/echarts.min.js引入彻底摆脱外网依赖容器给固定高度比如stylewidth:100%;height:420px;并保证 init 时元素可见。排查时先看 Network 面板里 js 文件是不是灰的再看容器尺寸两步能定位九成问题。5.3 模型预测结果全是同一个数字现象训练时准确率 97%前端画什么数字都返回同一个标签或者把 3 识别成 5、把 8 识别成 0清空画板还会预测出数字。原因这是最典型的“训练和推理数据分布不一致”。画板背景是白的、笔画是黑的而 MNIST 训练集是黑底白字或者前端 canvas 转了灰度但没反色模型看到的是黑白颠倒的图片。另一个常见原因是推理时没切 eval 模式但这个小模型没有 Dropout所以主要矛盾几乎都在颜色分布上。解决画板初始化时直接铺黑底、白色笔画和后端 Resize、ToTensor 的顺序保持一致。如果不确定颜色方向在 predict 前把图片存一份到本地看一眼。加载权重后立刻model.eval()推理包在torch.no_grad()里这两行养成习惯就不会再翻车。5.4 SQLite 报 database is locked现象爬虫一边写库Flask 一边读库运行一段时间后页面接口报database is locked重启服务又正常。原因SQLite 同一时刻只允许一个写事务Flask 每个请求新开一个连接两个写请求碰上就会锁读请求在写事务未提交时也会阻塞。教学演示里爬虫的 insert 和统计接口的 select 经常同时发生锁就暴露了。解决连接时加timeout30让 SQLite 在锁冲突时多等一会而不是立刻报错写操作统一用 try/commit/except rollback 包住提交失败就回滚。如果演示现场还有偶发锁最粗暴的办法是让爬虫和 Flask 共用同一个连接对象或者把写库间隔拉长但前者只适合演示环境实战还是要切 MySQL。5.5 模型权重加载路径与设备不匹配现象predictor 一启动就报FileNotFoundError或者报Missing key(s) in state_dict再或者 CUDA 相关的 device 错误。原因路径写的是相对路径data/mnist_cnn.pt而 Flask 的启动目录在 server 下相对路径就找不到文件保存权重时用了 GPU加载机器没有 GPU还有人是torch.save(model, ...)保存整个模型对象换环境后类定义对不上。解决路径统一用os.path.join拼绝对路径或者项目根目录放一个启动脚本保证所有模块的工作目录一致保存只存state_dict()不存模型对象加载一律带map_locationcpu一劳永逸。排查时先打印一个os.path.exists(weight_path)确认文件在不在再谈别的。6. 进阶技巧用一份 config.json 把平台参数化换数据源不动主代码教学演示最常做的事是换数据源、换端口、换模型权重。把路径和参数写死在代码里是最后悔的设计换个演示场地就要翻三个文件改五处地方。我的习惯是根目录放一份 config.json所有模块启动时统一加载改配置不改代码。{ crawler: { target_url: https://example.com/news/, list_selector: ul.news-list li, fetch_interval: 30 }, storage: { db_path: data/platform.db }, model: { weight_path: data/mnist_cnn.pt, input_size: 28 }, server: { host: 127.0.0.1, port: 8000 } }加一个简单的配置加载函数其他模块 import 它就能拿到全局配置。import json def load_config(pathconfig.json): with open(path, r, encodingutf-8) as f: return json.load(f) config load_config()scraper 里读config[crawler][target_url]和list_selector换站点只改这两行Flask 启动时读server.host和server.port换演示机器不冲突端口predictor 加载权重时读model.weight_path换训练好的新权重只改路径。这样一份配置管住整条链路参数化的收益在课程现场尤其明显——学生每人一份代码改配置就能跑出自己的数据。验证平台是否真正跑通按下面的顺序打三个请求。先看统计接口有没有数据再看列表接口的分页和筛选最后用手写画板验证模型链路。curl http://127.0.0.1:8000/api/stats curl http://127.0.0.1:8000/api/items?sourcedemo_sitepage1 curl -X POST http://127.0.0.1:8000/api/predict \ -H Content-Type: application/json \ -d {image:data:image/png;base64,iVBORw0KGgo...}前两个接口返回 code 0 和 JSON 数据说明爬虫、存储、服务三层是通的第三个接口在手写画板上画一个数字返回正确的 label 和置信度说明模型链路也是通的。全链路验证完这个平台才算真正能拿出来演示。某次带教学项目时A同学把站点路径写死在三个文件里换演示数据源改了十分钟才对齐加上 config.json 之后换数据源就是改两行的事。这个参数化的习惯我一直保留着也建议你从第一版就把配置和代码分开。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑