用Python爬虫+ECharts搭建股票行情可视化分析系统

📅 发布时间:2026/10/8 3:04:30
用Python爬虫+ECharts搭建股票行情可视化分析系统
最近帮朋友做了一个股票行情可视化分析的小系统从爬虫抓数据、清洗落库、再到页面图表展示一条龙走下来踩了不少坑也攒了不少经验。正好有人问我这类项目应该怎么从零搭干脆把整个设计和实现思路整理出来。如果你正想找一个Python爬虫数据处理可视化相结合的项目练手或者需要给团队搭一套轻量级的行情监控工具这篇内容应该能帮你省不少时间。先说这个系统能干什么定时从公开财经数据源抓取股票实时行情和K线数据自动做数据清洗和均线、涨跌幅等指标计算最后通过Web页面以图表形式展示个股走势和自选股监控。整体只用Python生态完成不用写复杂的JavaScript前端图表全部由ECharts渲染。适合有一定Python基础、想往数据采集和可视化方向深入的开发者参考。1. 项目整体设计与技术选型1.1 为什么选Python做股票数据采集做股票行情采集这件事技术栈的选择其实很关键。你可以用Java、Go甚至Node.js写爬虫但我在实际对比之后还是选了Python。原因有三个一是生态成熟requests、BeautifulSoup、Scrapy这些库对网页抓取和信息提取的支持非常完善写一个针对性的爬虫代码量可以压缩到很小的范围二是数据分析链条是闭环的抓下来的数据直接用pandas清洗、计算画图用pyecharts或者把处理结果通过API喂给前端Python能一路管到底三是调试效率高特别是应对数据源返回格式变化的情况脚本语言改起来比编译型语言快得多。当然Python爬虫有性能短板遇到大规模分布式采集时会吃力但对股票信息这种单机、低频率、小数据量的场景完全够用。用官方一句话说就是先让事情跑起来再考虑优化。这个系统本身就是轻量级定位不需要上分布式爬虫集群单线程定时轮询就能满足绝大部分个人和小团队的需求。1.2 爬虫方案对比与选型市面上获取股票数据的方式大概分三类。第一类是直接用现成的Python库比如akshare、tushare、baostock这些库封装好了数据获取逻辑调用接口就能拿数据省事是真的但缺点是依赖第三方维护有些库的接口更新滞后自由度也不够。第二类是调用券商或财经网站开放的HTTP接口这类接口数据全、实时性好但需要自己处理请求参数和返回格式。第三类是写通用的HTML爬虫去解析页面这种方式最麻烦因为要面对页面结构变动和反爬机制。我最终选的是第二类路线即直接请求财经网站公开行情接口。以新浪财经为例它的股票行情接口hq.sinajs.cn是一个标准的HTTP GET接口传入股票代码即可返回一条包含股票名称、当前价、涨跌额、成交量等字段的逗号分隔字符串。这种接口本质上是为网页前端服务的相比解析整个HTML页面它传输的数据量小、字段结构相对固定解析起来非常简单稳定性也远高于抓取HTML页面。用这类接口还有一个好处请求成本低单次请求就能拿到一只股票的完整行情快照连续请求几十只股票做轮询也没有压力。而且只要控制好频率在正常使用强度下基本不会触发对方的风控逻辑。1.3 可视化工具选型ECharts是主力可视化部分我对比过两套方案。一套是Python生态内的Matplotlib和Pyecharts另一套是前端JavaScript的ECharts。Matplotlib在静态分析报告场景很好用画K线、均线图都能胜任但它生成的图片不支持交互鼠标悬停看不到具体数值也没有时间范围拖拽这类功能。Pyecharts本质是把ECharts的配置能力包装成Python接口它生成的HTML里还是运行着ECharts。对于股票查看场景交互性是刚需。用户需要看某个时间段的走势必须能缩放、拖拽、悬浮提示。ECharts在这方面的体验是所有方案里最好的而且它支持的数据格式非常灵活我只需要在后端通过Flask提供JSON接口把pandas处理好的数据按ECharts要求的格式返回前端完全不用写复杂的业务逻辑。这里我采用的架构是后端返回标准JSON、前端ECharts直接渲染。Flask负责提供数据查询接口数据从数据库读取后经过简单的字段映射和格式转换以JSON形式输出。前端页面用原生HTMLJavaScript构建通过fetch请求数据然后交给ECharts实例渲染。这个模式看起来是多了一层前后端分离但实际上代码简洁后期维护也很方便。2. 爬虫模块的实现细节2.1 数据源选择与请求参数构造股票数据源我同时接了两个新浪财经做实时行情腾讯财经做K线历史数据。新浪接口格式如下http://hq.sinajs.cn/listsh600000,sz000001这个是标准A股代码格式sh代表上海交易所、sz代表深圳交易所后跟六位数字代码。注意返回的数据编码是GBK请求时必须在响应上做解码处理否则中文会出现乱码。腾讯的K线接口格式是http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?paramsh600000,day,2023-01-01,2023-12-31,320,qfq参数依次是股票代码、K线周期、开始日期、结束日期、返回数量、复权方式。qfq表示前复权这样拉到的历史价格已经处理过除权除息的影响直接可以用来算均线和收益率。请求参数的构造有几个细节需要注意。一是必须在请求头中带上Referer字段否则新浪接口会拒绝响应。我用的是https://finance.sina.com.cn作为Referer。二是不需要伪装成浏览器和搜索类站点不同财经数据接口主要检查的是来源不是浏览器身份。三是返回的字段是固定的用逗号分隔但要注意部分字段可能为空比如停牌期间成交量字段可能是空字符串。2.2 数据解析与清洗的完整流程以新浪实时行情接口为例返回的原始数据格式大约长这样var hq_str_sh600000浦发银行,10.50,10.52,10.48,10.55,10.40,...;我的解析思路分三步走。第一步用正则表达式剥离前面的var hq_str_前缀和结尾的分号只保留引号里的内容。第二步按逗号分割成一个列表。第三步根据新浪字段顺序建立映射关系。这里贴一段核心解析代码import requests import re def fetch_stock_real_time(stock_code): url fhttp://hq.sinajs.cn/list{stock_code} headers { Referer: https://finance.sina.com.cn, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } resp requests.get(url, headersheaders, timeout5) resp.encoding gbk text resp.text.strip() # 从响应中提取引号内的数据 match re.search(r(.*?), text) if not match: return None fields match.group(1).split(,) if len(fields) 32: return None return { code: stock_code, name: fields[0], open: float(fields[1] or 0), pre_close: float(fields[2] or 0), current: float(fields[3] or 0), high: float(fields[4] or 0), low: float(fields[5] or 0), volume: int(fields[8] or 0), amount: float(fields[9] or 0), date: fields[30], time: fields[31] }清洗环节有几个容易出坑的地方。第一是字段的空值判断某只股票停牌时价格字段可能为空字符串直接float()会抛异常所以我在每个转换前都加了or 0兜底。第二是单位统一问题新浪接口的成交量单位是股成交额单位是人民币元入库和计算时必须统一转换成万手和亿元否则后续可视化会出现数据量级错乱。第三是去重策略同一股票在同一个交易日可能被轮询多次为了避免重复入库我在数据库表设计时给交易日期加唯一索引写入时使用INSERT OR REPLACE语义天然保证数据不会重复。2.3 动态频率控制与断线重试爬虫跑起来容易但要让它长期稳定地跑下去频率控制和异常处理必须做扎实。我遇到过的情况是开始写爬虫时用死循环每秒抓一次跑了十几分钟后开始被数据源限流返回的响应变成异常内容。这个问题后来通过两个手段解决。第一个手段是动态调整请求间隔。开盘时段每10秒轮询一次收盘后数据不再变化则切换到10分钟一次。通过判断当前时间是否落在交易时段内来决定请求频率既不浪费请求资源又能保证页面展示的实时性。核心逻辑不复杂import time from datetime import datetime def should_fetch_high_frequency(): now datetime.now() weekday now.weekday() hour_minute now.strftime(%H:%M) # 工作日且处于交易时段早盘9:30-11:30午盘13:00-15:00 if weekday 5: if 09:30 hour_minute 11:30: return True if 13:00 hour_minute 15:00: return True return False第二个手段是重试机制。网络请求不可能每次成功遇到超时、连接重置这类问题时我的做法是连续重试三次每次间隔指数递增分别是1秒、2秒、4秒。如果三次都失败本轮跳过该股票记录日志后继续处理下一只不阻塞整个采集循环。这里不建议无限重试因为被限流时重试越频繁越容易加重封禁风险。另外一个容易忽略的点是请求顺序。我实测发现用同一个session对象顺序请求多只股票比每次新建连接效率高出不少。requests库的Session会复用底层的TCP连接对同一host的请求可以节省握手开销。这个优化虽然看不出来质的飞跃但在请求几千只股票时会明显减少总耗时。3. 存储、分析计算与可视化实现3.1 数据存储方案SQLite适合轻量需求存储层我用了SQLite而不是MySQL。原因是这个系统不需要多用户并发读写SQLite单文件方式的部署成本最低不需要单独装数据库服务。实测几千只股票的日K线数据量也就在几十MB级别SQLite完全扛得住。建表时我分了两张表一张存日K线一张存实时行情快照。关键字段如下CREATE TABLE daily_kline ( code TEXT, date TEXT, open REAL, close REAL, high REAL, low REAL, volume INTEGER, amount REAL, PRIMARY KEY (code, date) ); CREATE TABLE realtime_quotes ( code TEXT PRIMARY KEY, name TEXT, current REAL, open REAL, high REAL, low REAL, pre_close REAL, change_rate REAL, volume INTEGER, amount REAL, update_time TEXT );实时行情的表加了update_time字段每次轮询都更新前端展示时通过这个字段判断数据是否过期。日K线表用codedate做联合主键天然可以防止同一交易日的数据重复插入。这里有一个实操心得要分享SQLite对大量写入操作有锁机制多线程同时写会报database is locked。我的方案是爬虫线程只负责写Web请求线程只负责读避免写冲突。3.2 分析指标计算移动平均线是核心可视化页面上光有裸的K线还不够我加了几条移动平均线指标。MA5、MA10、MA20、MA60这四条均线分别代表短、中、长周期的平均成本线。计算方式很简单就是滚动求平均import pandas as pd def calculate_ma(df, window): return df[close].rolling(windowwindow, min_periods1).mean() df[ma5] calculate_ma(df, 5) df[ma10] calculate_ma(df, 10) df[ma20] calculate_ma(df, 20) df[ma60] calculate_ma(df, 60)需要注意min_periods1这个参数。如果没有它数据量不够5条时前四天都是NaN而ECharts在前端渲染时遇到NaN会出现数据断点K线图中间缺一段观感非常差。加了这个参数之后前四天的均线值就是已有数据的平均值图形更平滑。除了均线我还计算了涨跌幅和换手率两个衍生指标。涨跌幅用今天的收盘价减去昨天的收盘价再除以昨天的收盘价这个计算有一个细节第一天的涨跌幅没有历史参照需要置零或者直接丢弃不能参与可视化。换手率指标需要额外的流通股本数据这个不是每个接口都提供我干脆跳过流通股本用相对成交量和成交额来替代展示。3.3 后端接口设计与ECharts渲染后端服务用Flask搭建只提供两个核心接口一个是查某只股票的历史K线数据一个是查自选股的实时行情。这里贴一段K线接口的代码from flask import Flask, jsonify, request import sqlite3 app Flask(__name__) def query_kline(code, limit120): conn sqlite3.connect(stock.db) cur conn.cursor() sql SELECT date, open, close, high, low, volume FROM daily_kline WHERE code? ORDER BY date DESC LIMIT ? rows cur.execute(sql, (code, limit)).fetchall() conn.close() # 逆序得到按日期升序的数据 rows rows[::-1] dates [r[0] for r in rows] kline [[r[1], r[2], r[3], r[4]] for r in rows] # [open, close, high, low] volumes [r[5] for r in rows] return { dates: dates, kline: kline, volumes: volumes } app.route(/api/kline) def api_kline(): code request.args.get(code, sh600000) return jsonify(query_kline(code))ECharts接收这个JSON结构后用dataZoom组件配合K线图渲染。前端关键配置如下const chart echarts.init(document.getElementById(chart)); chart.setOption({ tooltip: { trigger: axis }, dataZoom: [ { type: inside, start: 60, end: 100 }, { type: slider, start: 60, end: 100 } ], xAxis: { type: category, data: data.dates }, yAxis: { scale: true }, series: [{ type: candlestick, data: data.kline, itemStyle: { color: #ef232a, color0: #14b143, borderColor: #ef232a, borderColor0: #14b143 } }] });一个细节是ECharts的K线数据结构它要求每根K线是四元组顺序是[开盘价、收盘价、最低价、最高价]注意不是[开、高、低、收]我在这里搞反过一次结果画出来的K线影线全部反了。另一个细节是涨跌颜色A股习惯红涨绿跌ECharts默认是国际惯例绿涨红跌必须通过color和color0手动设置。3.4 可视化大屏布局与交互体验页面布局我设计了三个核心区域。左侧面板展示自选股列表和实时涨跌数据点击某只股票后中间区域刷新K线图右侧区域展示均线示意图和分析指标卡片。自选股列表使用定时器每10秒向后端拉取一次最新行情并把涨跌幅超过3%的股票用特殊颜色标出相当于一个简单的异动提醒功能。页面整体没有用前端框架就是原生HTMLCSS这样不需要引入Node.js构建工具链。CSS采用了深色背景主题因为K线图在深色背景下的视觉反馈明显更好分辨涨跌也更轻松。页面用flex布局实现三栏结构中间图表区域弹性缩放在小屏设备上也基本能用。视觉呈现上少用花哨的元素图表区域保持足够大信息层级清楚。4. 系统架构整合与定时调度4.1 分层架构设计思路整个系统按职责分成四层数据采集层、存储层、分析计算层、展示层。四层之间通过接口衔接每一层都可以独立替换。比如数据采集层目前用新浪的HTTP接口哪天想切到tushare只需要改采集层的代码上层存储和分析逻辑完全不用动。这个分层设计在处理需求变更时优势很明显。朋友后来提出要增加港股数据我只需要在采集层增加一个港股数据源的适配器把返回字段统一映射成现有结构的字典即可分析计算和展示层感知不到新增了数据源。如果你是给自己做工具用分层虽然多写几个接口函数但后续维护的幸福感会翻倍。数据采集层另外做了一个抽象处理把新浪接口、腾讯接口的返回结果统一转成一个标准的字典结构字段名固定为code、name、open、close、high、low、volume、amount。这样一来即使切换数据源也不用每次都去修改数据库层和展示层的字段映射关系。4.2 定时任务调度方案系统不可能一直靠手动运行脚本必须要有自动调度能力。我用了APScheduler这个Python库来做定时任务而不是依赖系统crontab。理由很简单APScheduler在进程内管理调度不依赖操作系统环境而且可以直接操作已经初始化的数据库连接和日志系统。调度策略如下交易时段内每10秒跑一次实时行情采集任务每天收盘后跑一次日K线更新任务每周做一次数据完整性检查。这三个任务用APScheduler配置成本地触发器服务启动时自动加载。核心写法不复杂from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.interval import IntervalTrigger from apscheduler.triggers.cron import CronTrigger scheduler BlockingScheduler() # 交易时段轮询实时行情通过job内逻辑判断是否真正执行 scheduler.add_job( fetch_realtime_job, triggerIntervalTrigger(seconds10), idrealtime_fetch, max_instances1, coalesceTrue ) # 每天15:10更新日K线 scheduler.add_job( update_daily_kline_job, triggerCronTrigger(hour15, minute10), iddaily_kline_update ) scheduler.start()max_instances1和coalesceTrue这两个参数是并发保护的关键。前者保证同一任务前一个实例没跑完时不会启动新实例后者处理任务积压场景比如程序挂了一小时后恢复之前的触发表会被合并成一次执行而不是补跑几百次。这对于爬虫任务非常重要避免重复请求导致数据源限流。4.3 自选股管理与配置化自选股列表不应该写死在代码里。我把它做成了一个配置文件加一个前端编辑入口用户可以在页面上增删自选股票配置保存后采集线程在下一轮自动感知变更。实现上采用一个简单的JSON文件存储自选股代码列表采集任务每次迭代前重新读取一次配置这样不需要重启服务。{ stocks: [ {code: sh600000, name: 浦发银行}, {code: sz000001, name: 平安银行}, {code: sh600519, name: 贵州茅台} ] }这里有一个坑要提醒股票代码的交易所前缀不能搞混。用sh开头的去请求深圳接口虽然不会报错但返回的代码段对不上实际股票数据全部错误。所以配置校验逻辑必须做两层一层检查代码格式是否合法另一层校验返回的数据名称是否和配置名称一致不一致直接告警。5. 实操中遇到的问题与排查实录5.1 数据源偶发请求超时运行一段时间后我注意到日志里偶尔出现requests.exceptions.Timeout。排查发现在午间休市前后数据源服务器会短暂调整部分请求响应变慢。解决办法是在请求中设置超时时间不设置read超时的话一个挂起的连接可能卡住线程好几分钟。正确的超时设置是连接超时和读取超时都设置resp requests.get(url, headersheaders, timeout(3.05, 5))连接超时3.05秒读取超时5秒。这里建议连接超时不要用整数3秒因为底层TCP重传机制可能导致刚好在3秒边缘的连接被提前误杀。3.05这个数值是Python官方文档推荐的相比3秒可以降低一些误判概率。5.2 中文乱码是编码转换坑第一次从新浪接口拿数据时打印出来的股票名称全是乱码。问题的根源在于新浪返回的是GBK编码而requests库默认按响应头猜测编码但该接口的响应头中没写清楚charset导致requests错误地按UTF-8解码。解决方案是手动指定编码resp.encoding gbk这里还想多说一句经验总结凡是处理国内股票数据源的响应不管对方声称什么编码最好都以手动方式指定编码不要依赖自动检测。自动检测在大部分场景可靠但遇到返回头缺失编码声明、数据本身又不是纯文本的时候就会掉链子。腾讯的行情接口也存在类似情况返回UTF-8但偶尔夹杂转义字符同样需要手动归一化处理。5.3 触发访问限制后的处理策略我在调试阶段因为频繁请求触发过一次数据源的限制特征是连续几个请求全部返回异常或空内容。排查下来发现原因是调试循环里遗漏了time.sleep导致每秒请求十几次。我的规避策略分三层。第一层是请求频率上限单只股票的间隔不低于10秒整个轮询循环结束后额外sleep 2秒。第二层是异常惩罚机制连续失败5次后自动进入冷却状态暂停该任务5分钟让数据源的风控策略冷却下来。第三层是观察响应内容如果返回内容明显不符合正常数据模式直接判定被限流此时立刻停止当前批次的请求而不是硬着头皮继续抓。下面是一个精简版的带冷却机制的采集循环fail_count 0 cooling False def fetch_with_guard(code): global fail_count, cooling if cooling: return None try: data fetch_stock_realtime(code) fail_count 0 return data except Exception as e: fail_count 1 if fail_count 5: cooling True # 启动冷却计时器5分钟后自动恢复 threading.Timer(300, reset_cooling).start() return None def reset_cooling(): global fail_count, cooling fail_count 0 cooling False5.4 前端图表卡顿与数据量优化全量展示上证几千只股票的数据时前端加载数据明显变慢页面操作也有卡顿。这个问题的根源不是ECharts渲染力不足而是我给前端的接口返回了全部股票的全字段数据每次传输几十万个数据点浏览器的JSON解析和图表重绘压力自然大。解决方案是分维度优化。第一K线接口默认只返回最近120个交易日的数据超过这个范围需要前端主动请求更多第二实时行情列表用分页方式第一屏只显示前50只自选股加载更多时再拉下一批第三指标卡片的计算放到后端完成前端只拿计算结果展示不在浏览器里跑大数据量计算。还有一个体验优化的小细节前端定时刷新时负责拉数据的setInterval请求如果上一次请求还没返回会造成请求堆积进而拖慢页面。我在方案中加入了请求锁let loading false; async function refreshQuotes() { if (loading) return; loading true; try { const data await fetchQuotes(); renderQuotes(data); } finally { loading false; } } setInterval(refreshQuotes, 10000);这样即使后端响应偶尔变慢前端也不会同时堆积多个请求。实测下来页面长时间运行也不会出现内存持续增长的现象。5.5 数据库偶发写入失败的处理SQLite在高频轮询场景下偶尔出现database is locked错误。排查后确认是写入事务和读取事务产生了锁竞争。解决办法是在连接SQLite时增加超时设置并开启WAL模式conn sqlite3.connect(stock.db, timeout10) conn.execute(PRAGMA journal_modeWAL)WAL模式让读写协调共存读操作不会阻塞写操作。配合单写多读的使用方式这个配置让数据库运行得非常稳定从加上这个PRAGMA之后没有再出现过锁错误。这个场景给我一个启发不要一上来就追求MySQL或PostgreSQL这样的重型数据库。SQLite搭配正确的配置在单机低并发场景下完全不输给网络数据库而且不占内存、不占额外进程作为爬虫项目的存储层非常理想。6. 项目扩展方向与个人心得做完了这套基础版本之后我还在盘算几个扩展方向。第一个是增加技术指标图形比如MACD指标、RSI相对强弱指标、布林带通道这些在ECharts里都有对应的渲染方式核心工作只是指标计算。第二个是增加指数汇总视图把沪深主要指数放在一个页面上统一监控以便看整体趋势。第三个是引入机器学习做简单预测用历史K线数据训练一个分类模型判断后续走势概率这是一个有趣的实验方向但实盘参考价值有限只能当技术实验。分享一下我在整个项目开发中的体会。爬虫项目最重要的不是花哨的抓取技巧而是稳定的数据治理能力。你把数据抓下来只是完成了第一步让数据始终保持干净、准确、可持续更新才是系统真正能跑起来的关键。写代码的时候给自己提一个问题这个模块如果运行三十天不出人工干预会不会出问题有时候变量名写得再漂亮不如把异常处理好来得实在。另外一个体会是数据源的选择直接影响开发效率。第一次做类似项目时总想抓那些需要复杂解析的页面觉得这样才有含金量。实际经验告诉我选择一个稳定的公开数据接口比写复杂的解析逻辑重要得多。这不是偷懒是把精力花在更有价值的地方。低耦合、高内聚的分层设计让我在后面每次新增功能时都省了很多事。最后再分享一个实用习惯给爬虫项目加上日志记录。我在采集函数入口和出口分别打了两条日志记录时间、股票代码、成功与否、耗时。这个日志平时看着没什么用但系统出现问题的时候它是唯一能帮你快速定位是网络原因、数据源异常还是自己代码bug的信号。没有日志的爬虫项目出问题基本靠猜有日志的项目排查起来是有手就行的活。