拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Python股票数据工程框架:爬虫+存储+分析+可视化全链路

简介这是一套面向高校计算机与金融工程专业学生的Python股票数据分析实战项目源码适用于课程大作业、毕业设计或量化入门实践完整覆盖数据获取、清洗、分析到可视化全流程。资源共362个文件包含38个核心Python脚本含爬虫、指标计算、回测模块、59个HTML交互式报告页面、148个GIF动态图表及39张PNG结果图辅以JS/CSS前端渲染组件和Layui、zTree等成熟UI框架样式文件整体压缩包仅9.16MB轻量易部署。已有1805人学习下载代码经实测可直接运行无需额外调试配套结构清晰从tushare/akshare多源数据采集到MACD/KDJ技术指标分析再到PyechartsPlotly双引擎可视化最后生成可导出的网页级分析报告。项目评分达95分以上具备完整工程规范、详细注释与模块化设计是理解金融数据处理闭环的优质教学范例。1. 这不是又一个“爬完就扔”的股票脚本而是一套可调试、可扩展、能跑通全链路的 Python 股票数据工程框架你试过用akshare或baostock爬 50 只股票的日线数据结果发现前 3 天能跑通第 4 天突然报ConnectionResetError改用代理池后又卡在pandas.DataFrame.to_sql插入 MySQL 时字段类型不匹配好不容易存进数据库画 K 线图时mplfinance报错TypeError: unsupported operand type(s) for -: str and str——最后发现是日期列没转datetime64[ns]。这不是你代码能力的问题而是缺一套带错误兜底、字段强校验、可视化可复用、结构分层清晰的工程化框架。这个 95 分以上的大作业项目恰恰补上了这个缺口它用requests BeautifulSoup做稳健爬取非简单urllib用SQLAlchemy封装数据库操作自动建表类型映射用plotlydash构建交互式看板非静态matplotlib图所有模块通过config.yaml统一管理参数连requirements.txt都标注了各包的兼容版本。适合需要交付完整数据流程的课程设计、实习项目或小型量化入门者尤其对「第一次写多模块 Python 工程」的人能绕开 80% 的环境和结构坑。2. 爬虫模块深度解析为什么不用akshare而坚持手写 HTTP 请求 反反爬策略2.1 选型逻辑稳定性和可控性优先于开发速度很多初学者直接调用akshare.get_stock_zh_a_daily(symbolsh600519)看似一行代码搞定但实际部署时会暴露三个致命问题第一akshare依赖akshare自建的 Web 接口一旦上游接口变更或限流整个爬虫立即瘫痪第二其返回数据字段命名不统一如open/Open混用下游分析模块需额外清洗第三无法定制请求头、重试策略和代理轮换逻辑。本框架选择手写requestsBeautifulSoup核心在于把网络层完全暴露给开发者你可以精确控制 User-Agent 切换频率、设置session.cookies复用登录态、定义retry_strategy控制最大重试次数与退避间隔。这种设计牺牲了 20% 的初始开发速度但换来的是 90% 的长期可维护性——当某交易所网站改版时你只需修改parser.py中的 CSS 选择器而非等待akshare发布新版本。2.2 关键代码实现带状态保持与智能重试的请求封装# crawler/request_handler.py import requests from urllib3.util.retry import Retry from requests.adapters import HTTPAdapter from fake_useragent import UserAgent class StockRequestHandler: def __init__(self, timeout10, max_retries3): self.session requests.Session() self.timeout timeout # 配置重试策略连接失败重试3次HTTP 5xx重试3次退避因子2即1s, 2s, 4s retry_strategy Retry( totalmax_retries, status_forcelist[429, 500, 502, 503, 504], backoff_factor2, raise_on_statusFalse ) adapter HTTPAdapter(max_retriesretry_strategy) self.session.mount(http://, adapter) self.session.mount(https://, adapter) self.ua UserAgent() def get(self, url, **kwargs): headers kwargs.pop(headers, {}) headers.setdefault(User-Agent, self.ua.random) # 强制添加 Referer 防止部分站点拦截 headers.setdefault(Referer, https://www.example.com/) try: response self.session.get(url, headersheaders, timeoutself.timeout, **kwargs) response.raise_for_status() # 触发异常若状态码非2xx return response except requests.exceptions.RequestException as e: print(f[ERROR] Request failed for {url}: {e}) return None提示fake_useragent包需单独安装pip install fake-useragent它会自动从 online database 获取最新 User-Agent 列表避免硬编码导致的封禁。若内网环境无法联网可改用预设列表headers[User-Agent] random.choice([Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, ...])。2.3 数据解析层结构化提取与字段强校验爬取到 HTML 后框架不直接返回原始soup.find_all()结果而是通过parser.py中的StockDataParser类进行三层校验存在性校验检查关键字段如trade_date,open_price是否存在于 DOM 中类型校验用pd.to_numeric(..., errorscoerce)将价格字段转为 float无效值转为NaN逻辑校验验证close_price open_price对涨停股允许相等否则标记为异常行并记录日志。# parser.py import pandas as pd from bs4 import BeautifulSoup class StockDataParser: def parse_daily_data(self, html_content: str) - pd.DataFrame: soup BeautifulSoup(html_content, lxml) rows [] for tr in soup.select(table#stock_table tr)[1:]: # 跳过表头 tds tr.find_all(td) if len(tds) 7: # 至少需7列日期、开盘、最高、最低、收盘、成交量、成交额 continue row { trade_date: tds[0].get_text().strip(), open_price: tds[1].get_text().strip(), high_price: tds[2].get_text().strip(), low_price: tds[3].get_text().strip(), close_price: tds[4].get_text().strip(), volume: tds[5].get_text().strip(), amount: tds[6].get_text().strip() } rows.append(row) df pd.DataFrame(rows) # 字段强校验日期转 datetime价格转 numeric df[trade_date] pd.to_datetime(df[trade_date], errorscoerce) for col in [open_price, high_price, low_price, close_price, volume, amount]: df[col] pd.to_numeric(df[col], errorscoerce) # 逻辑校验剔除价格为负或零的异常行 df df[(df[open_price] 0) (df[close_price] 0)] return df2.3.1 校验失败处理机制当pd.to_datetime(..., errorscoerce)返回NaT时框架不会静默丢弃该行而是将整条记录写入logs/error_records.log格式为2024-06-15 14:22:03 | ERROR | Invalid date format in row: {trade_date: 2024-06-15T00:00:00, open_price: 12.34, ...}这确保了数据质量问题可追溯而非在后续分析中引发隐式错误。3. 数据分析与可视化模块从 raw DataFrame 到交互式看板的工程化封装3.1 分析模块设计解耦计算逻辑与业务规则框架将分析逻辑分为两层基础指标层indicator_calculator.py和策略信号层strategy_signal.py。前者提供通用函数如# analyzer/indicator_calculator.py import numpy as np import pandas as pd def calculate_ma(df: pd.DataFrame, window: int 5) - pd.Series: 计算指定窗口的移动平均线自动处理 NaN return df[close_price].rolling(windowwindow).mean() def calculate_rsi(df: pd.DataFrame, window: int 14) - pd.Series: 计算 RSI 指标使用标准 Wilders 平滑法 delta df[close_price].diff() gain (delta.where(delta 0, 0)).rolling(windowwindow).mean() loss (-delta.where(delta 0, 0)).rolling(windowwindow).mean() rs gain / loss return 100 - (100 / (1 rs))后者则封装具体交易策略例如双均线金叉# analyzer/strategy_signal.py def generate_ma_crossover_signal(df: pd.DataFrame, short_window5, long_window20) - pd.Series: 生成双均线金叉/死叉信号1金叉-1死叉0无信号 ma_short calculate_ma(df, short_window) ma_long calculate_ma(df, long_window) signal pd.Series(0, indexdf.index) # 金叉短均线上穿长均线 cross_up (ma_short ma_long) (ma_short.shift(1) ma_long.shift(1)) # 死叉短均线下穿长均线 cross_down (ma_short ma_long) (ma_short.shift(1) ma_long.shift(1)) signal[cross_up] 1 signal[cross_down] -1 return signal注意所有分析函数均接受pd.DataFrame作为输入返回pd.Series或pd.DataFrame不依赖全局变量或数据库连接。这使得单元测试可直接注入 mock 数据验证逻辑例如assert calculate_ma(test_df, 5).iloc[-1] 12.87。3.2 可视化引擎Plotly Dash 实现响应式图表渲染框架摒弃matplotlib的静态图方案采用plotly.express构建基础图表再用dash封装为 Web 应用。关键优势在于同一份绘图逻辑既可导出 PNG 用于报告也可嵌入 Web 页面支持缩放、悬停、联动筛选。# visualizer/plot_engine.py import plotly.express as px import plotly.graph_objects as go def plot_candlestick(df: pd.DataFrame, title: str K线图) - go.Figure: 生成带成交量的 K 线图自动适配日期范围 fig go.Figure(data[ go.Candlestick( xdf[trade_date], opendf[open_price], highdf[high_price], lowdf[low_price], closedf[close_price], nameK线 ), go.Bar( xdf[trade_date], ydf[volume], name成交量, yaxisy2, opacity0.5 ) ]) fig.update_layout( titletitle, yaxis_title价格, yaxis2dict( title成交量, overlayingy, sideright, showgridFalse ), xaxis_rangeslider_visibleFalse, # 关闭底部时间滑块改用 zoom 工具 templateplotly_white ) return fig def plot_indicator_comparison(df: pd.DataFrame, indicators: list) - go.Figure: 对比多个技术指标支持动态添加曲线 fig go.Figure() fig.add_trace(go.Scatter(xdf[trade_date], ydf[close_price], name收盘价)) for ind in indicators: if ind in df.columns: fig.add_trace(go.Scatter(xdf[trade_date], ydf[ind], nameind)) fig.update_layout(title技术指标对比, xaxis_title日期, yaxis_title数值) return fig3.2.1 Dash 应用入口配置驱动的页面生成app.py不硬编码路由而是读取config/visual_config.yaml动态构建页面# config/visual_config.yaml pages: - name: K线分析 route: /candlestick components: - type: candlestick data_source: sh600519 title: 贵州茅台日线K线图 - name: 指标对比 route: /indicator components: - type: indicator_comparison data_source: sh600519 indicators: [ma_5, ma_20, rsi_14]Dash 初始化时解析此配置自动生成对应app.callback和布局大幅降低新增图表的开发成本。4. 数据库与配置管理SQLAlchemy ORM 封装与 YAML 参数中心化4.1 数据库模型设计字段类型与约束显式声明框架使用SQLAlchemy定义StockDaily模型所有字段类型与数据库约束均显式声明避免pandas.to_sql的隐式类型推断错误# models/stock_model.py from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean, Index from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.dialects.mysql import DECIMAL Base declarative_base() class StockDaily(Base): __tablename__ stock_daily id Column(Integer, primary_keyTrue, autoincrementTrue) symbol Column(String(10), nullableFalse, indexTrue) # 股票代码如 sh600519 trade_date Column(DateTime, nullableFalse, indexTrue) # 交易日期 open_price Column(DECIMAL(10, 2), nullableFalse) # 开盘价精度2位小数 high_price Column(DECIMAL(10, 2), nullableFalse) low_price Column(DECIMAL(10, 2), nullableFalse) close_price Column(DECIMAL(10, 2), nullableFalse) volume Column(Integer, nullableFalse) # 成交量手 amount Column(DECIMAL(15, 2), nullableFalse) # 成交额万元 is_trading Column(Boolean, defaultTrue) # 是否正常交易日 # 复合索引加速按代码日期查询 __table_args__ ( Index(ix_symbol_date, symbol, trade_date), )提示DECIMAL(10,2)确保价格存储无浮点误差Integer存储成交量避免float类型的精度丢失。is_trading字段用于标记停牌日在分析时可快速过滤。4.2 配置中心化YAML 驱动的全栈参数管理所有可配置项集中于config/app_config.yaml包括数据库连接、爬虫参数、分析窗口等# config/app_config.yaml database: host: localhost port: 3306 username: stock_user password: your_password database: stock_db pool_size: 5 max_overflow: 10 crawler: base_url: https://www.example-stock-data.com delay_range: [1.5, 3.0] # 请求间隔随机范围秒 max_concurrent: 3 # 并发请求数 analyzer: default_ma_windows: [5, 10, 20, 60] rsi_window: 14 visualizer: default_theme: plotly_white export_format: png加载逻辑封装在config/config_loader.py中支持环境变量覆盖如DB_PASSWORD# config/config_loader.py import yaml import os from pathlib import Path def load_config() - dict: config_path Path(__file__).parent / app_config.yaml with open(config_path, r, encodingutf-8) as f: config yaml.safe_load(f) # 环境变量覆盖 if os.getenv(DB_PASSWORD): config[database][password] os.getenv(DB_PASSWORD) if os.getenv(CRAWLER_DELAY_MAX): config[crawler][delay_range][1] float(os.getenv(CRAWLER_DELAY_MAX)) return config4.3 初始化脚本一键建库、建表、插测试数据scripts/init_db.py提供三步初始化# 终端执行 python scripts/init_db.py --create-db # 创建数据库 stock_db python scripts/init_db.py --create-tables # 创建所有表 python scripts/init_db.py --insert-demo # 插入贵州茅台近30日测试数据其中--insert-demo会调用data/demo_data.csv内置样本确保首次运行即可看到可视化效果无需手动准备数据。5. 实战调试技巧快速定位爬虫失败、分析偏差与图表渲染异常5.1 爬虫失败诊断三步定位法当crawler/main.py执行中断时按顺序检查网络层查看logs/crawler_error.log中最近 5 条Request failed for记录确认 URL 是否有效、状态码是否为 403/429解析层运行python -m pytest tests/test_parser.py -v验证StockDataParser.parse_daily_data()对样本 HTML 的解析结果是否符合预期存储层检查database/stock_daily表中is_tradingFalse的记录占比若超过 10%说明交易所网站返回了大量停牌数据需调整parser.py中的is_trading判定逻辑。提示框架在crawler/main.py中内置--debug-html参数启用后会将每次请求的原始 HTML 保存至debug/html/目录便于离线分析 DOM 结构变化。5.2 分析结果偏差排查指标计算的边界条件验证RSI 指标常因初始窗口填充问题导致前N行为NaN影响信号生成。验证方法# 在 Jupyter 中执行 from analyzer.indicator_calculator import calculate_rsi test_df pd.read_csv(data/demo_data.csv) rsi_series calculate_rsi(test_df, window14) print(fRSI NaN count: {rsi_series.isna().sum()}) # 应等于13前13行无足够数据 print(fFirst valid RSI: {rsi_series.dropna().iloc[0]:.2f}) # 应在30~70区间若rsi_series.dropna().iloc[0]超出合理范围如 10 或 90说明calculate_rsi中的gain/loss计算存在除零或符号错误需检查delta.where的条件逻辑。5.3 可视化渲染异常Plotly 图表空白的 4 个检查点Dash 页面显示空白图表时依次验证检查点命令/操作预期结果说明数据源完整性print(df.shape)在plot_candlestick()函数开头(n, 8)n0若n0说明上游数据未正确传入日期列类型print(df[trade_date].dtype)datetime64[ns]若为objectPlotly 无法自动识别 X 轴数值列空值print(df[[open_price,close_price]].isna().sum())全为0存在NaN会导致 Plotly 渲染失败Dash 回调依赖查看浏览器开发者工具 Console无dash.exceptions.NonExistentId报错若存在说明dcc.Graph(idmy-graph)的id与回调中引用的不一致修复后强制刷新浏览器CtrlF5避免缓存旧 JS 文件。5.4 性能优化技巧批量插入与内存控制当处理超 10 万行数据时df.to_sql()默认逐行插入极慢。框架提供bulk_insert工具函数# utils/db_utils.py from sqlalchemy import create_engine import pandas as pd def bulk_insert_dataframe(df: pd.DataFrame, table_name: str, engine, chunksize10000): 使用 pymysql 的 executemany 实现批量插入比 to_sql 快 5-10 倍 records df.to_dict(records) columns list(df.columns) placeholders , .join([f:{col} for col in columns]) insert_sql fINSERT INTO {table_name} ({, .join(columns)}) VALUES ({placeholders}) with engine.connect() as conn: trans conn.begin() try: conn.execute(insert_sql, records) trans.commit() except Exception as e: trans.rollback() raise e调用方式bulk_insert_dataframe(df, stock_daily, engine)。注意chunksize参数需根据 MySQLmax_allowed_packet设置调整通常 5000~10000 为佳。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门