资讯动态

Flask股票数据采集可视化系统:毕设首选,从接口爬虫到ECharts K线图

发布时间:2026/10/22 3:05:59 来源:尧图企业网站定制
简介这是一套面向高校计算机专业毕业设计的Flask股票数据采集分析可视化系统完整资料包适合具备一定Python与Web基础、需要完成金融数据类课题或想了解爬虫与可视化全流程的学生。程序基于Flask框架搭建后端利用爬虫采集股票行情数据并借助matplotlib、seaborn等工具完成分析展示整体形成了“数据获取—清洗处理—可视化输出”的完整链路。压缩包共95个文件包含源码、模板、静态样式、项目说明、演示视频及数据库文件等文件类型以js、css、html、txt、jpg和mp4为主包大小约25.14MB目录结构清晰便于按模块查阅。已有136人学习下载适合用于毕业设计参考、技术学习与二次开发实践。1. 为什么说Flask股票数据采集可视化系统是毕业设计里性价比最高的选择每年毕业设计选股票方向的同学特别多但真正能把题目做扎实的人很少。多数人卡在同一个地方以为核心难点是爬虫结果写完爬虫发现数据没地方放以为核心难点是图表结果图表做完了发现后端接口根本撑不起前端页面。Flask股票数据采集分析可视化系统解决的就是这条完整链路——用Python爬虫或公开行情接口把历史K线数据拿下来存进本地数据库再用Flask提供查询接口最后在网页上用图表把数据展示出来。这套系统最值钱的地方在于一个题目同时覆盖了网络请求、数据处理、后端开发、前端可视化四个考核点论文工作量好写答辩也有东西可问。适合自动化、计算机、金融工程等方向的本科生前期只需要熟悉Python基础语法即可上手。我见过不少同学在两个框架之间反复比较实际上对毕业设计来说选什么框架不重要重要的是能独立跑通一条完整的数据流。这篇文章按照我实际做过的方案从数据源选型、库表设计、采集代码、避坑记录到Flask接口和ECharts图表完整拆给你。2. 系统架构与技术选型行情数据接口优先于网页解析2.1 为什么金融数据要走接口而不是硬解析网页不少同学一听“爬虫”就想到用BeautifulSoup去解析HTML页面对股票数据来说这是最吃力不讨好的做法。财经网站的页面结构是前端工程师决定的改版一次你的解析代码就废一次更麻烦的是页面里的表格经常经过动态渲染直接用requests拿到的HTML里根本没有数据。我做过一次完整的HTML解析方案光是在定位表格节点和调试动态加载上就花了两天时间后来彻底放弃。更可靠的思路是找公开的行情HTTP接口。这类接口返回的是固定格式的文本——通常是GBK编码的逗号分隔字符串字段顺序是文档固定的不像HTML那样层层嵌套。你只需要用requests请求一次拿到文本后做split拆分就完成了最核心的采集动作。严格来说这依然属于爬虫范畴只是把解析对象从HTML换成了结构化文本仍然符合毕设题目里“爬虫”的要求。围绕这个选型我整理了一张对比表建议在做开题报告时直接参照。对比项接口型采集网页HTML解析数据完整性字段完整包含开高低收、成交量、成交额页面可能只展示部分字段隐藏列拿不到字段可解析性逗号分隔顺序固定一次拆分到位需要先解析DOM结构再逐行提取单元格反爬应对常规限频加间隔即可可能有字体反爬、JS动态渲染、验证码日常维护成本接口文档基本不变很少需要改代码页面结构一改解析脚本立刻失效对于毕业设计来说时间成本是最大的成本。HTML方案的每一步都在消耗你写代码和改论文的时间而它带来的“看起来很辛苦”的观感并不会在答辩时加分。我的建议是先做接口方案快速跑通剩下的时间用来优化功能和丰富分析维度。2.2 Flask与Django的取舍我的选择理由Django确实能力更强自带Admin后台和ORM但正因为它太完整对毕业设计反而是负担。你必须先去理解它的路由配置、应用结构、中间件机制才能写出一个小功能。Flask则恰好相反你只需要写一个入口文件定义几个路由就能把后端跑起来。对股票采集系统这种业务逻辑集中于“请求数据、存库、查数据”的功能Flask的轻量完全够用。我一般会建议选Flask还有一个原因答辩现场修改代码的体验完全不同。评委对你的某个参数或某个逻辑提出疑问时你可以当场打开代码文件改一行重启进程刷新页面就能看到效果。Django的工程结构比较复杂评委的问题可能要跨好几个文件才能解释清楚。开发体验上Flask的调试模式会在代码变更后自动重载配合SQLite的零配置文件型整个项目在任何一台电脑上都能直接运行不需要评委额外配置任何环境。从数据流的角度看Flask只担任服务层角色。采集器把数据写进SQLite后Flask负责读取数据库并转成JSON返回给前端。这种各个进程独立的分工本身就是答辩时可以展开讲架构的地方。你不需要让Flask直接参与爬虫逻辑也不需要在路由函数里处理复杂的计算保持职责单一即可。2.3 整体数据流设计采集、存储、服务、展示四层整个系统我习惯拆成四条线采集层负责定时请求外部接口拿数据存储层用SQLite承接原始数据和复权因子服务层由Flask提供REST风格查询接口展示层在浏览器中用ECharts渲染K线图和成交量图。四层之间通过数据库表和JSON数据连接层与层不互相调用内部实现这样可以保证采集进程挂掉时页面仍然能正常查库展示最近一次采集的结果。数据库选SQLite的理由很实在零配置、单文件、答辩时可以直接把数据库文件拷贝到任意环境打开。很多人担心SQLite并发写入能力对本系统来说完全不是问题——采集器单进程请求外部接口本身就受限于网络速度写库的频率很低。后续如果你想让系统看起来更“专业”可以预留一个切换MySQL的配置项在论文里写清两种数据库的对比即可不必真的换库。3. 数据采集层实现写一个能连跑一个月的日K采集器3.1 表结构设计把复权因子和行情数据分开存数据表结构是最容易被忽略但事后最难改的部分。我见过有人把所有字段塞进一张表涨跌幅、换手率、振幅全算好再存结果换了一个数据源后字段对不上整套代码推倒重来。正确做法是只存基础行情字段和复权因子所有指标计算都在查询时实时算。下面是我在这套系统里实际使用的建表SQL。CREATE TABLE stock_basic ( code TEXT PRIMARY KEY, -- 股票代码取纯数字部分 name TEXT NOT NULL, -- 证券简称 industry TEXT, -- 所属行业用于后续分组统计 list_date TEXT, -- 上市日期格式YYYY-MM-DD updated_at TEXT -- 基础信息最后更新时间 ); CREATE TABLE daily_bar ( code TEXT NOT NULL, -- 股票代码与stock_basic关联 trade_date TEXT NOT NULL, -- 交易日期格式YYYY-MM-DD open REAL, -- 开盘价 high REAL, -- 最高价 low REAL, -- 最低价 close REAL, -- 收盘价 volume INTEGER, -- 成交量单位手 amount REAL, -- 成交额单位元 adj_factor REAL DEFAULT 1.0, -- 复权因子无复权时默认为1 PRIMARY KEY (code, trade_date) );字段类型上有几个有意为之的设计。第一主键采用(code, trade_date)联合主键同一只股票同一天的数据天然不重复后续增量更新时可以直接用INSERT OR REPLACE做幂等写入不需要先查一遍再决定是插入还是更新。第二trade_date用TEXT而不是DATE类型不是我不知道有DATE类型而是当作字符串存时可以直接用max()函数拿到最近日期排序和比较也都符合时间顺序反而少了一层类型转换的麻烦。第三adj_factor单独成列而不是直接把价格改成复权价是因为前复权价格会随着每一次新的除权除息而整体变化如果你存了前复权价历史数据第二天就“变”了到答辩时你无法向评委解释为什么昨天算的收益率今天对不上。3.2 采集器核心代码请求、解析、校验、入库一气呵成采集器是整个系统最核心的代码只需要一个Python脚本就能跑通。下面的代码实现了一个最小的日K线采集流程请求数据源、解析文本、校验字段、写入SQLite。关键点都加了注释你可以直接照着抄。import requests import sqlite3 import time from datetime import datetime, timedelta def fetch_daily(code: str, start: str, end: str) - list: 从公开行情接口拉取日K线返回清洗后的记录列表。 接口返回的是GBK编码的逗号分隔文本字段顺序需要对照 你实际使用的数据源文档确认。 params { code: code, start: start, end: end, fields: date,open,high,low,close,volume,amount, } # 这里换成你实际使用的公开行情接口地址 url https://your-data-source.example.com/hq/kline resp requests.get(url, paramsparams, timeout10) resp.encoding gbk # 行情接口普遍使用GBK编码 rows [] for line in resp.text.strip().split(\n): parts line.split(,) # 至少需要7列字段列数不对直接跳过 if len(parts) 7: continue try: trade_date parts[0].strip() open_p float(parts[1]) high_p float(parts[2]) low_p float(parts[3]) close_p float(parts[4]) volume int(float(parts[5])) amount float(parts[6]) except ValueError: # 字段解析失败说明这一行数据不合法丢弃 continue # 最基本的区间校验最低价不可能大于最高价 if not (low_p open_p high_p and low_p close_p high_p): continue rows.append((code, trade_date, open_p, high_p, low_p, close_p, volume, amount)) return rows def save_daily(rows: list, db_path: str) - int: 将清洗后的K线记录批量写入SQLite自动处理重复数据。 conn sqlite3.connect(db_path) sql INSERT INTO daily_bar(code, trade_date, open, high, low, close, volume, amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(code, trade_date) DO UPDATE SET openexcluded.open, highexcluded.high, lowexcluded.low, closeexcluded.close, volumeexcluded.volume, amountexcluded.amount conn.executemany(sql, rows) conn.commit() total len(rows) conn.close() # 每次入库都打印行数方便对账 print(f{code} 入库 {total} 条日期范围 {rows[0][1]} ~ {rows[-1][1]}) return total if __name__ __main__: code 600000 start 20250101 end datetime.now().strftime(%Y%m%d) rows fetch_daily(code, start, end) save_daily(rows, stock.db)代码里几个参数值得单独解释。timeout10 是给HTTP请求设置的上限超过10秒直接抛异常避免数据源无响应时程序卡死。resp.encodinggbk 是必须的如果不手动指定requests会按HTTP头里的charset猜测编码部分接口的响应头不包含编码信息猜错就会出现中文乱码或直接解析出错。字段解析前做一边校验解析失败就丢弃这一行这是数据质量的最后一道防线。区间校验本质上是利用行情本身的约束——开盘价和收盘价必须落在最高价和最低价之间——这个规则能拦截大部分脏数据。ON CONFLICT子句是SQLite的UPSERT语法遇到重复的主键就更新已有行保证脚本重复执行时数据库不会堆积重复记录。注意ON CONFLICT语法要求SQLite 3.24及以上版本。Python自带的sqlite3模块通常都能满足如果你发现执行报错先检查SQLite版本而不是改SQL。3.3 增量更新与每日调度别做重复的全量采集如果每天全量采集一次全部股票的K线请求量会非常夸张而且大部分数据是重复的。正确做法是增量更新先查数据库里每只股票最近的交易日只采集这个日期之后的数据。对这个系统来说增量更新不仅仅是省流量的问题更关键的是减少了对数据源的请求频率请求越少越不容易触发限流。以下是核心增量逻辑我习惯单独封装成一个函数和全量初始化逻辑分开。def incremental_update(db_path: str, code: str): 根据库内已有数据的最大日期增量拉取新数据。 conn sqlite3.connect(db_path) cur conn.execute( SELECT max(trade_date) FROM daily_bar WHERE code ?, (code,) ) latest cur.fetchone()[0] if latest is None: # 库里没有数据走全量初始化 start 20200101 else: # 从已有数据次日开始拉取 dt datetime.strptime(latest, %Y-%m-%d) timedelta(days1) start dt.strftime(%Y%m%d) conn.close() end datetime.now().strftime(%Y%m%d) rows fetch_daily(code, start, end) if rows: # 只持久化新查到的行不重复写已有数据 save_daily(rows, db_path)增量参数的精髓在start的计算上。strptime解析出库内最大日期加一天再格式化回字符串这样无论库里的日期是哪一天拉取范围永远从缺口处开始。如果数据源支持按日期区间过滤这个方法能保证每个交易日只新增一条记录。调度层面可以用APScheduler的CronTrigger每天下午收盘后执行一次也可以退一步直接用while循环加sleep对毕设来说完全足够。我见过有人用time.sleep(86400)实现每日周期简单粗暴但如果程序在夜间被手动中断当天数据就丢了所以建议至少加一个开机启动的守护方式。4. 踩坑记录金融数据采集与存储的五个典型翻车现场4.1 停牌日数据静默消失K线图出现假缺口现象某只股票连续停牌一周后图表上缺少那几天的数据格子看起来像价格垂直穿越K线图出现异常的“缺口”。有人误以为是采集代码漏了数据反复重跑采集脚本却发现数据源本来就没有这几天的记录。原因停牌期间股票不交易K线数据源自然不生成当天的bar。你查询“最近7天数据”得到的结果可能只有3天这是交易所规则决定的不是接口问题。如果不理解这一点就会花大量时间排查一个根本不是Bug的问题。解决明确区分“交易日”和“自然日”。在表设计时增加一个trade_calendar表存放全年交易日历。展示层拿到K线数据后根据日期在交易日历里做映射停牌日自动跳过。答辩被问到“为什么这里少了一段”时能直接回答停牌因素会显得你理解业务规则。我当时就是没做日历表被评委追问后现场补的。4.2 某天抓到的数据全是0字段校验缺失的代价现象某一次采集完成后入库正常但当天的行数据open、high、low、close全是0.0前端的K线图出现一条平躺的线。打印原始响应发现数据源在异常时返回全0占位并不是网络抖动导致的半截数据。原因接口在数据源异常时不会返回空字符串而是返回一组占位0。我当时只做了字符串拆分和类型转换float(0)能正常通过就没有拦住这一行脏数据。到现在我仍然认为数值校验时“字段能转成float”只是及格线真正要关注的是数值本身是否符合业务约束。解决在fetch_daily里增加两个条件——检查open、high、low、close是否同时为0如果同时为0直接丢弃再检查low high这个恒定约束。这两个条件组合起来能拦截绝大多数异常响应。不要只用if not row之类的判断因为0在Python里是False但0.0在这个业务场景里恰好是非法值用布尔判断反而会把非法数据漏进来。4.3 请求频率过高被限流连接被重置现象采集器连续运行第3天requests开始报ConnectionError或返回状态码异常明明网络是通的浏览器也能打开行情页面但程序就是拿不到数据重试多次后偶尔成功一次。原因高频连续请求触发了服务端的频率限制机制。我之前为了提高采集速度把请求间隔从1秒降到了0.2秒结果第二天就被封了。这里有个经验金融行情接口对请求频率的敏感度远高于普通网站因为数据接口背后连着实时行情库高频请求会消耗大量资源。解决把每次请求之间的sleep改回1秒以上并且每次请求都带上浏览器的User-Agent头。如果还是触发限频就放弃“一只股票一次性拉完”的做法拆分请求区间每次只请求10天数据请求间隔固定2秒。另外加一个指数退避重试逻辑——第一次失败等1秒再试第二次等2秒第三次等4秒最多重试5次。这套组合拳之后我的采集器连续跑了一个月没有中断。4.4 金额字段用FLOAT存储累加后误差越滚越大现象某天计算一只股票近30日总成交额发现手算结果和程序算出来的结果差了0.01元以为是四舍五入的问题但差到0.01明显不是正常浮点误差。原因SQLite的REAL类型对应C语言的double十进制小数在二进制浮点里本身就有误差。单看一位没差别但成交额动辄千万级累加几百行后误差就会被放大。更隐蔽的是如果后续再做收益率指标浮点误差会传播到复权计算里。解决价格和成交额字段全部改成NUMERIC或干脆用整数存“分”。修改方式是建表时用NUMERIC(12, 2)写入前把元转成分存整数也行。我现在用的是把成交额存成整数分的方案——SELECT之后除以100转回元。整个系统里除法和乘法都是可控的整数运算不会再出现误差累积。这个点写进论文里是一个合格的“系统设计考量”。4.5 数据库里的日期变成“2025-7-1”排序错乱现象查询某只股票的价格列表按trade_date排序后2025-7-1排在了2025-6-30前面而且7月的所有日期都排在6月之后看起来就像月份错位。原因数据源传过来的日期格式不统一有的是2025-07-01有的是2025-7-1。如果你不做格式化直接存库TEXT类型的字段按字典序排序时字符串2025-7-1和2025-07-01的排序结果完全不同。解决在fetch_daily的解析阶段统一格式化日期用datetime.strptime(trade_date, %Y-%m-%d)先解析再重新格式化成YYYY-MM-DD标准形式。如果你拿到的原数据带有斜杠或小数点分隔先把分隔符统一替换成-再解析。这个坑很容易被忽视因为它只在排序时暴露数据量少的时候肉眼根本看不出来。5. Flask后端与可视化把K线图真正做到浏览器里5.1 用蓝图组织后端采集与查询接口分离Flask项目不建议把所有路由写在单一的app.py里。做毕设时当然怎么写都能跑但答辩现场评委一定会问“你这个项目结构是怎么组织的”如果你的答案是“全在app.py里”会显得缺乏工程意识。用蓝图拆分后代码结构一目了然。我的方案是把采集相关的启动逻辑和查询相关的API拆成两个蓝图。# app.py 入口文件 from flask import Flask from api.route import api_bp from collect.route import collect_bp def create_app(): app Flask(__name__) # 查询接口蓝图负责给前端页面提供JSON数据 app.register_blueprint(api_bp, url_prefix/api) # 采集控制蓝图手动触发增量更新方便演示 app.register_blueprint(collect_bp, url_prefix/collect) return app if __name__ __main__: app create_app() app.run(host0.0.0.0, port5000, debugTrue)把采集接口做成手动触发的方式有个实际好处——答辩时你可以现场演示“点按钮触发增量采集”这个动作评委能看到日志里打印出“入库 N 条”的输出过程。这比空口讲解爬虫逻辑有说服力得多。host0.0.0.0可以让局域网内其他设备访问方便你在手机上测试页面效果。debugTrue用于开发调试不要在生产环境开启但答辩演示无所谓。5.2 查询接口设计日期范围、复权方式、成交量过滤查询接口是整个系统的门面前端图表的所有数据都从这个接口拿。在设计上我要确保这一个接口能覆盖K线图、成交量图、收益率计算三种场景避免为每个图表单独写接口。下面的代码实现了最核心的查询逻辑。# api/route.py from flask import Blueprint, request, jsonify import sqlite3 api_bp Blueprint(api, __name__) api_bp.route(/kline) def kline(): code request.args.get(code, ).strip() start request.args.get(start, 20200101).strip() end request.args.get(end, 20251231).strip() # 是否返回前复权数据默认开启 adj request.args.get(adj, 1) 1 conn sqlite3.connect(stock.db) conn.row_factory sqlite3.Row sql SELECT trade_date, open, high, low, close, volume, amount, adj_factor FROM daily_bar WHERE code ? AND trade_date ? AND trade_date ? ORDER BY trade_date ASC rows conn.execute(sql, (code, start, end)).fetchall() conn.close() result [] for r in rows: date r[trade_date] # 前复权计算原始价格乘复权因子 if adj: factor r[adj_factor] open_p round(r[open] * factor, 2) high_p round(r[high] * factor, 2) low_p round(r[low] * factor, 2) close_p round(r[close] * factor, 2) else: open_p, high_p, low_p, close_p r[open], r[high], r[low], r[close] result.append({ date: date, open: open_p, high: high_p, low: low_p, close: close_p, volume: r[volume], }) return jsonify({code: code, adj: adj, items: result, count: len(result)})后端接口的参数设计有几个统一原则。日期参数统一接收YYYYMMDD格式前端传什么后端就收什么不额外做格式转换减少前后端之间的格式约定成本。adj参数默认开启前复权因为K线图通常展示复权后的价格走势否则除权日会出现价格跳空影响观感。需要注意的是我这里是按“原始价×复权因子”计算的近似前复权严格的前复权需要以最新因子为基准做等比缩放对毕设来说这个近似完全够用。参数含义默认值说明code股票代码无必填6位纯数字start起始日期20200101格式YYYYMMDDend结束日期20251231格式YYYYMMDDadj是否前复权1传0返回原始价格5.3 ECharts前端接入K线图与成交量柱状图联动前端可视化直接使用ECharts不用pyecharts生成的静态图表页面。原因很简单pyecharts做出来的图本质是一张静态HTML换股票、改时间范围都要重新生成页面交互性太弱。直接引ECharts的JS库用fetch请求自己的接口图表就能实时响应参数变化。下面是我实现K线图和成交量图联动的最小代码。// static/kline.js async function loadKline(code) { const resp await fetch(/api/kline?code${code}start20250101end20251231adj1); const data await resp.json(); // ECharts的K线数据格式是 [开盘, 收盘, 最低, 最高]顺序不能错 const klineData data.items.map(item [ item.open, item.close, item.low, item.high ]); const dates data.items.map(item item.date); const volumes data.items.map(item item.volume); chart.setOption({ xAxis: { data: dates }, dataZoom: [ { type: inside, start: 0, end: 100 }, { type: slider, start: 0, end: 100 } ], series: [ { name: 日K, type: candlestick, data: klineData, itemStyle: { color: #ef232a, // 阳线红色 color0: #14b143, // 阴线绿色 borderColor: #ef232a, borderColor0: #14b143 } }, { name: 成交量, type: bar, xAxisIndex: 0, yAxisIndex: 1, data: volumes } ] }); }这段代码里最容易被忽略的是ECharts的candlestick数据顺序。很多人上来按照直觉传成[开盘, 最高, 最低, 收盘]图表画出来完全错乱。ECharts官方定义的顺序是[开盘, 收盘, 最低, 最高]这是我在实际开发中踩过并验证过的。dataZoom配置了inside和slider两种模式前者支持鼠标滚轮缩放后者提供底部的滑块拖拽两者配合用户就能自由查看任意时间区间。两个series共用一个xAxis日期轴保持一致缩放时K线和成交量同步进退。6. 进阶打磨收益率计算、数据核对与答辩演示技巧有了原始行情数据和复权因子你可以进一步做“分析”层面的功能这是标题里“分析”二字的落点。最常用的一个指标是区间收益率计算方式很简单取区间最后一个交易日的复权收盘价除以区间第一个交易日前一日的复权收盘价减1。注意这里一定要用复权价否则遇到区间内除权收益率会被严重低估。我把这个计算做成了一个接口前端页面加了一个时间段选择器用户选定起止日期后前端计算并展示涨跌幅。这个功能实现了“采集→存储→分析→展示”的闭环论文里的系统功能设计部分也更好写。答辩前一定要做数据核对用行情软件抽查3只不同股票的K线数据。我当时的核对方法是每只股票抽20个交易日逐个交易日比对收盘价误差容忍在0.01元以内。出现较大偏差时重点检查复权因子是否更新——如果数据源的复权因子是每日更新的而你采集时用了旧因子价格偏差会随时间累积。再检查一下最高价是否小于最低价这类异常如果出现说明采集校验逻辑还有漏洞需要回炉。答辩演示环节有一个技巧分享给大家不要展示静态图表。演示要覆盖“数据采集—接口查询—图表更新”的完整过程先在页面输入股票代码图表加载出K线接着调用一次采集接口让页面刷新出现新数据最后切到另一只股票对比两只股票的走势差异。整个流程控制在三分钟内全程有真实数据输出比你对着PPT讲五分钟架构图有说服力得多。准备演示时把数据库清空一次现场从头跑一遍全量初始化虽然多等几十秒但评委看到从空库到图表完整展示的全过程会觉得系统是真的能用。做这套系统的过程里我最大的教训是数据结构设计永远在爬虫代码之前。我先跑通了采集器才回头建表结果字段类型改了三遍前两版数据全部作废重来。现在我的习惯是先写出CREATE TABLE再开始写请求代码每次采集完先打印入库行数和行情软件的记录数对得上才继续。这个习惯让我后来做其他数据类项目都少走了很多弯路。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑