☰
股票大数据分析Python实战:爬虫抓取、数据清洗与可视化
2026/10/12 18:20:38 网站建设 项目流程

简介:面向金融数据分析与Python爬虫学习者的实战资料包,围绕股票大数据全流程展开:从爬虫采集历史行情与新闻公告,到Pandas清洗缺失值与异常值,再到Matplotlib、Seaborn绘制K线图和波动性图表,并延伸至Scikit-learn线性回归、随机森林等模型的股价预测,以及Tushare/Wind接口实时数据获取与Flask/Django可视化部署。资源包为7z压缩格式,大小约4.47MB,当前文件总数及类型明细未单独标注,适合已掌握Python基础、希望切入量化分析或金融数据挖掘场景的读者。已有3645人学习下载。通过随包资料可以系统梳理爬虫反爬处理、数据预处理、统计分析和机器学习建模的关键代码思路,为独立开展股票数据分析项目提供可直接参考的实践框架与排错经验。

1. 基于股票大数据分析Python实战:爬虫只是入口,别在第一步就耗尽耐心

接手一个基于股票大数据分析的Python实战项目,头一件事不是选模型,而是把行情数据从公开接口搬回本地。很多朋友以为python爬虫是重头戏,真动手才发现爬虫只占半小时,后面的清洗、对齐、落盘才是无底洞。这套实战资源就是围绕“行情抓取—清洗—存储—指标计算—可视化”这条主线组织的,适合刚开始做量化分析、又不想从零摸索代码的人。

说句实话,股票行情抓取本身不难,难在数据可信、字段对齐、图表可读。下文讲的都是我在真实复现中踩过又填平的细节,你可以照着跑,也可以在避坑清单里找后悔药。

2. 数据抓取层:Python爬虫选型与股票行情接口实战

2.1 三种数据获取方式,为什么选爬虫

股票日线数据至少有三种拿法:财经网站手动导出CSV、免费公开接口、付费数据终端。手动导出适合一次性小样本,但批量拉几十只股票时,鼠标点几百次不现实。付费终端数据全,但很多数据在客户端里,想交给python处理还得再导一道。爬虫方式最灵活,直接请求接口拿JSON,在内存里转成DataFrame,后面写Excel还是入库都顺。

选型之后要明确一个边界:爬虫抓的是“公开接口能返回的数据”,不是绕过登录权限拿非公开数据。做量化分析用日线级别的开高低收、成交量已经够用。请求频率也要克制,我一般会设置0.5秒以上的间隔,减少对数据源的打扰。

方式优点缺点适合场景
手动导出CSV简单直接无法批量、耗时一次性分析
公开接口爬虫可批量、可定时字段不稳定、需清洗日线数据、量化初阶
付费终端数据质量高环境隔离、成本高生产级策略

相比Java或C++的工程化路线,Python做数据探查的优势是生态,pandas和matplotlib这类库把结构化数据操作和绘图都收进内存,几行代码就能完成。有人主张直接上Selenium渲染页面,但日线行情是标准接口数据,requests足够。Selenium慢且容易被检测,适合做页面型数据源,不适合高频率更新。选型时先看数据源有没有JSON接口,有就优先requests。

2.2 日K线抓取的最小可跑脚本

确定用爬虫后,第一步是把“一个请求”跑通。我一般先把接口返回打印出来,确认字段结构,再写解析。下面是抓日K的最小脚本框架,具体接口地址放在配置里,换数据源时只改一处。

import requests def fetch_daily_kline(symbol, start_date, end_date, api_conf): """ 抓取日K线 symbol: 股票代码,如 "600000" api_conf: 接口配置字典,包含 url / headers / params """ params = { "symbol": symbol, "start": start_date, "end": end_date, "freq": "daily" } params.update(api_conf.get("extra_params", {})) resp = requests.get( api_conf["url"], params=params, headers=api_conf["headers"], timeout=10 ) resp.raise_for_status() # 有的接口把数据放在 list 字段里,有的放在 data 里,这里做兼容 payload = resp.json() rows = payload.get("list", payload.get("data", [])) return rows

逻辑说明:params负责查询条件,symbol传股票代码,start和end控制区间,freq表示K线频率。requests.get发出请求,timeout=10限制最长等待时间,避免某次请求卡死整个循环。resp.raise_for_status()在状态码非2xx时直接抛异常,防止后端返回错误页时还被当数据解析。最后用payload.get("list", payload.get("data", []))做两层兼容,很多行情接口数据节点名不一样,这一行能省掉不少调试时间。

参数说明:

  • api_conf["headers"] 至少要包含User-Agent和Referer,建议直接从浏览器开发者工具复制完整请求头,而不是手写精简版。
  • start_date和end_date用"YYYY-MM-DD"格式,多数接口是闭区间,多传一两天问题不大,少传会缺数据。
  • extra_params是留给接口特殊参数的兜底字典,比如有些接口需要传market或source字段。

强调一点:不要在这个脚本里做循环批量请求,先单独跑一只股票,确认返回结构没问题,再套循环。批量抓取的坑往往不是接口不通,而是字段取值和频率限制。

2.3 把接口返回整理成DataFrame:字段映射与类型转换

接口返回一般是JSON数组,直接转成DataFrame后,列名、类型、顺序都是乱的。我的习惯是三步走:重命名列、固定列顺序、统一类型。

import pandas as pd def to_frame(rows, field_map): # field_map 示例:{"trade_date": "date", "open_price": "open"} df = pd.DataFrame(rows) df = df.rename(columns=field_map) # 固定列顺序,防止接口字段顺序调整导致后续计算错位 need = ["date", "open", "high", "low", "close", "volume"] df = df[need].copy() for col in ["open", "high", "low", "close", "volume"]: df[col] = pd.to_numeric(df[col], errors="coerce") df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) return df

逻辑说明:rename把接口原始字段名映射为统一命名,之后代码里只认date、open、high、low、close、volume。df[need]这一步本质是数组切片,把列顺序固定下来,同时丢掉多余字段。pd.to_numeric配合errors="coerce"会把空字符串、非数字字符转成NaN,类型统一为float。最后to_datetime把日期统一成时间戳类型,sort_values按日期升序,reset_index让行号从0开始连续,避免画图时索引断层。

参数说明:

  • field_map必须按实际接口字段填写,如果字段没映射到,df[need]里会直接报KeyError,这个错误比后续数据错位好排查。
  • errors="coerce"会把脏值变成NaN,后面清洗时统一处理,不要改成errors="raise",否则一条脏数据会让整个抓取任务崩溃。
  • reset_index(drop=True)里的drop=True很关键,不加的话旧索引会变成新列,后续绘图和计算都可能出现错行。

这份资源里,fetch_daily_kline和to_frame是配套使用的,先抓后转,后面所有清洗和画图都基于这个统一结构。下载后建议先跑通这两步,再进入指标计算。如果接口返回的是CSV格式而不是JSON,解析逻辑会简单一些,直接用pd.read_csv读取字符串即可,但字段重命名的步骤不能省。

3. 数据清洗与落盘:DataFrame到Excel的五个关键点

3.1 复权处理:为什么不能直接把原始价格拿去做指标

股市里有分红送股,价格会在除权日出现跳空。比如一只股票10元,每10股派5元现金,除权后价格会下调,但这个下调不代表真实亏损。如果直接用原始价格算移动平均或涨跌幅,指标会在除权日出现虚假信号。最常见做法是前复权:以最新价格为基准,调整历史价格。

def adjust_price(df, factor_col="adj_factor"): if factor_col not in df.columns: # 没有复权因子时,至少提醒一次,避免静默用原始价 print("warning: no adj_factor found") return df base = df[factor_col].iloc[-1] df["adj_ratio"] = df[factor_col] / base for col in ["open", "high", "low", "close"]: df[col + "_adj"] = df[col] * df["adj_ratio"] return df

逻辑说明:base取最后一个交易日的复权因子,作为前复权基准。adj_ratio是当日因子和基准因子的比值,历史价格乘以这个比值后,最新交易日价格保持不变,历史价格按比例缩放,这样除权跳空就被抹平了。打印warning是为了避免你忘了带复权因子列,数据看起来没问题但实际上没有复权。

参数说明:

  • factor_col默认叫adj_factor,如果数据源叫factor或adj_factor_1,要显式传入。
  • 复权后的列名加了_adj后缀,不覆盖原始列。好处是可以并排对比,坏处是后续计算前要记得统一用哪个收盘价列,否则容易混。
  • 如果数据源直接给adj_close,就不需要这个函数,但要做一次校验:adj_close和原始close在最新一行应该相等。

另一个细节:前复权后,历史价格理论上不会变成负数,但遇到极端除权会导致历史价格很低。如果出现奇怪的值,优先检查因子序列是否倒序,而不是怀疑算法。

3.2 去重与时间线检查:停牌和缺失是两回事

清洗的第二步是处理重复行和缺失日期。接口偶尔会返回重复数据,尤其做增量更新时,旧数据和新数据衔接容易产生重复行。去重很简单,但日期连续性问题要多说一句:股票天然有休市日,不能把“非交易日”当缺失。

def clean_ts(df): df = df.drop_duplicates(subset=["date"], keep="last") df = df.sort_values("date") df = df.reset_index(drop=True) diff = df["date"].diff().dropna() gaps = diff[diff > pd.Timedelta(days=2)] if len(gaps) > 0: print("candidate gap rows:", len(gaps)) return df

逻辑说明:drop_duplicates按日期去重,keep="last"表示保留最后一条,适合增量更新里新数据覆盖旧数据。diff计算相邻交易日的间隔,间隔超过两天意味着这里可能有停牌或数据缺失。打印数量而不是直接抛错,是因为停牌本身合法,需要人工判断是补数据还是保留停牌。

参数说明:

  • keep="last"在回测场景里要慎用,如果你拼接的是“旧数据在前、新数据在后”,重复行保留后者是对的;如果顺序反了,结果就反了。
  • pd.Timedelta(days=2)是判断阈值,正常周五到下周一间隔是3天自然日,会在gaps里出现,这不算异常。所以只看数量、不做自动补值,避免误判断。

对于真实缺口,画图时用日期索引,pandas会自动跳过缺失交易日,不要手动插值。手动插值等于给回测数据加了不存在的交易信息,属于常见误用。

3.3 写入Excel:多sheet输出和日期格式处理

清洗完的数据最终要落盘,Excel是最轻量的交付格式。使用to_excel时,三个细节值得注意:需要多sheet时用ExcelWriter;日期列转成date后再写,避免Excel里看到的是一串时间戳;写完顺手检查行数和原始行数是否一致。

def save_excel(df_dict, filepath): from pandas import ExcelWriter with ExcelWriter(filepath, engine="openpyxl") as writer: for sheet_name, df in df_dict.items(): df_out = df.copy() if "date" in df_out.columns: df_out["date"] = df_out["date"].dt.date df_out.to_excel(writer, sheet_name=sheet_name, index=False) print("saved:", filepath)

逻辑说明:ExcelWriter是上下文管理器,with块结束时会把所有sheet一次性写入文件。df_dict是字典,key是sheet名,value是DataFrame。日期转成dt.date是为了让Excel识别成日期格式,而不是一串毫秒数。index=False表示不写DataFrame的行号,否则Excel里会多出一列0、1、2。

参数说明:

  • engine="openpyxl"专门处理xlsx文件,如果环境里没装,需要先pip install openpyxl。
  • sheet_name不能重复,重复会覆盖,中文sheet名在部分老版本Excel里兼容性一般,建议用英文或拼音。
  • 数据量超过几十万行时,to_excel会明显变慢,此时应该转用to_sql写库。资源里附带了一个sqlite落地模块,接口和这个函数保持一致的调用方式,换行连接串就能切到MySQL或Oracle。用python连接oracle查询数据本质上也是这个套路:先建连接,再执行SQL或to_sql,区别只在驱动配置上。

清洗和落盘做到这里,数据已经能交付给Excel或数据库了。再往后是计算指标和画图,也是这套资源里最容易看到效果的部分。

4. 指标计算与可视化:定义函数、数组切片和坐标轴密集的解法

4.1 把指标封装成def函数:调参只改一处

量化分析里最常用的就是移动平均、涨跌幅、交叉信号。如果不封装函数,每个脚本都重复写rolling和shift,一旦需求变化,到处改。封装后,参数暴露在函数签名里,调参只改一处。

def sma(series, window): # 简单移动平均,窗口为 window 个交易日 return series.rolling(window=window, min_periods=window).mean() def calc_change_pct(df): df["change_pct"] = df["close"].pct_change() * 100 return df

逻辑说明:sma用rolling滑窗计算均值,min_periods=window保证前window-1行结果是NaN,避免用不完整窗口算出虚值。calc_change_pct用pct_change计算涨跌幅,再乘100转成百分比。

参数说明:

  • window在两条均线配合时,通常给5、10、20、60这类常见周期。不同搭配会产生完全不同的交叉信号。
  • pct_change默认是用当前值除以上一行再减1,也就是相对前一交易日的变化,不是相对开盘价。
  • 对涨跌幅列做聚合时,算术平均没有意义,应该用乘积或log收益,这点在回测里容易被忽略。

计算指标时不要用for循环逐行算均线,DataFrame的rolling和shift都是矢量化实现,速度差一个量级,代码也短。我第一次写交叉信号时用了循环,跑几百行数据没问题,换成几千只股票时直接卡死,改成矢量化操作后才顺畅。资源里的指标模块全部按矢量化写法组织,不建议为了“看得懂”退回循环版本。

4.2 数组切片与数据对齐:信号算错通常是索引没对齐

DataFrame里做运算时,pandas会自动按索引对齐。一旦之前做过筛选、拼接或遗漏reset_index,两个Series的索引就会错位,轻则NaN,重则整个交叉信号偏移一根K线。解决思路是:提前固定索引顺序,关键运算后用数组切片做验证。

def add_ma_cross(df, short=5, long=20): df = df.sort_values("date").reset_index(drop=True) df["ma_short"] = sma(df["close"], short) df["ma_long"] = sma(df["close"], long) # 上穿信号:短均线今天大于长均线,且昨天不大于 df["cross_up"] = (df["ma_short"] > df["ma_long"]) & \ (df["ma_short"].shift(1) <= df["ma_long"].shift(1)) return df

逻辑说明:先做reset_index(drop=True)强制索引从0开始连续,这是对齐的前提。cross_up列由两个布尔条件做与运算:今天短均线在长均线上方,且昨天不在,严格捕获上穿的那一行。shift(1)取的是上一交易日的值,如果索引错位,shift结果就不对应了。

参数说明:

  • short和long是窗口参数,5和20是短中期常用组合,修改这两个数字会直接改变信号数量。
  • 布尔与运算里的括号不能省,&优先级比比较运算符低,不写括号会报错或结果全False。
  • 如果df做过按股票代码分组,务必先groupby再对组内排序,否则不同股票的数据混在一起算出的均线是错的。

4.3 画图横坐标太密集:matplotlib三个参数解决

一直有朋友问“python画图横坐标太密集”。其实不是数据问题,是刻度数量问题。当数据跨度几百个交易日,绘图库默认每个日期都标刻度,横坐标自然挤成一团。解法是用定位器控制刻度间隔。

import matplotlib.pyplot as plt import matplotlib.dates as mdates def plot_close(df, figsize=(12, 6), interval=30): fig, ax = plt.subplots(figsize=figsize) ax.plot(df["date"], df["close"], linewidth=1.2, label="close") ax.xaxis.set_major_locator(mdates.DayLocator(interval=interval)) ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d")) plt.xticks(rotation=45, ha="right") plt.ylabel("price") plt.legend() plt.tight_layout() plt.show()

逻辑说明:set_major_locator控制主刻度落在哪些日期上,DayLocator(interval=30)表示每30天标一个刻度。set_major_formatter把刻度标签格式化为“年-月-日”。rotation=45旋转标签,ha="right"让标签右对齐,减少重叠。tight_layout自动调整边距,防止底部标签被截断。

参数说明:

  • interval要结合数据跨度调整。数据只有30天,interval=5更合适;数据跨度一年以上,interval=60甚至90都可以。
  • 如果横坐标不是日期而是字符串,需要先转成datetime再plot,否则DateLocator不生效。
  • 另一种常见做法是只显示部分数据:df.iloc[::step]做数组切片降采样。但要清楚,切片后的趋势线形状不变,但真实数据点稀疏了,可以和定位器组合使用。

均线指标和交叉信号画出来后,基本能判断一段趋势是否可信。这套资源里还有MACD的扩展脚本,原理和上面一样,核心是先保证close序列干净且对齐。任何指标算出来全NaN,不要怀疑公式,先回头检查rolling的min_periods和索引顺序。

5. 避坑清单:爬虫、环境、画图三类高频翻车现场

下面把前面几章分散的踩坑经验汇总成清单。每条记录都按“现象、原因、解决”三行说明,方便直接对号入座。

5.1 环境与安装的坑

现象:python环境刚装好,pip install pandas时出现下载超时、编译报错,或者提示找不到匹配版本。

原因:默认包源访问慢,网络波动大;另外Python版本过新或过旧,导致某些包没有对应wheel包。最常见的是直接在系统全局环境装包,把环境搞脏,后面项目依赖互相冲突。

解决:先给这个项目建虚拟环境,在虚拟环境内安装。pip下载时显式指定国内镜像源,不要用默认源反复重试。如果某个包本地编译失败,去找对应Python版本的whl文件安装,安装顺序要注意:先装numpy,再装pandas,最后装matplotlib,依赖顺序反了会重复编译。用vscode配置python环境时,记得先激活虚拟环境再打开编辑器,否则解释器选不对,代码里import pandas会直接报ModuleNotFoundError。

5.2 爬虫请求与数据字段的坑

现象:同样的接口地址在浏览器里能打开,在python requests里返回403,或者返回的不是JSON而是验证页面。

原因:requests默认的User-Agent没有浏览器特征,接口识别出脚本访问后拒绝服务。还有一类情况是缺少Referer,接口按来源校验请求。这种反爬不算复杂,但第一次遇到时完全没有头绪,属于典型的黑匣子。

解决:用requests.Session保持会话,把浏览器开发者工具里的User-Agent、Referer、Accept完整复制到headers里。如果拿到的还是HTML,先打印前200个字符确认响应格式,不要直接调json(),否则会在解析阶段抛错。频率控制也是关键:循环请求时每次sleep 0.5到1秒,批量从一只股票扩展到几十只时,间隔不够很容易触发临时限制。别等到被限再补救,一开始就把延时节流写进采集函数。

现象:接口昨天还能返回字段,今天就少了某列,脚本直接KeyError中断。

原因:行情接口字段属于公共数据,接口方调整字段名或返回结构时一般不会提前通知脚本使用者。遇到这种问题只能靠代码兜底。

解决:解析时不要直接df["date"],用df.get("date")或先判断列是否在df.columns中。关键字段缺失时跳过该条数据并打印日志,而不是当场崩溃。这样至少能把问题隔离在一行数据上,让整体抓取任务跑完。下载数据后也要定期抽查字段结构,接口变更时第一反应不是改下游计算,而是检查解析层有没有拦住。

5.3 时间序列与指标计算的坑

现象:计算移动平均后,序列前段出现一大段NaN,和预期不符。

原因:rolling默认min_periods等于window,窗口没攒够前全是NaN,这是正常的。但如果你在回测里直接拿这段NaN去算信号,策略会在前window天完全无信号,影响对早期数据的评估。

解决:明确需求再决定。如果画图展示,保留NaN没问题;如果回测,建议给足窗口计算期,不要用0填充。用0填充会制造出均线突然从0开始回升的假象,属于自我欺骗。正确的做法是把样本起点往前推,给指标留出预热窗口,或者用min_periods=1让窗口内有多少数据就按多少数据算。但min_periods=1在早期数据少时,均值波动会异常大,要清楚这个代价。

现象:复权后的价格曲线和行情软件对不上,或者指标曲线有明显断裂。

原因:原始价格序列混入了未复权数据,或者前复权基准选错。有的接口返回的是“复权因子”而不是“复权价”,如果你直接把复权因子当成价格去算,结果全是错的。

解决:先确认数据源的字段含义。factor是乘数,不是价格。使用前复权时,基准应选最新交易日的因子。做完复权后,抽一个除权日和行情软件对比收盘价,误差在0.01以内才算过。这里没有玄学,逐行核对最可靠。

6. 进阶玩法:定时增量抓取与数据校验的一线习惯

6.1 增量更新:只抓新增区间,不重放全量

项目跑通之后,最实际的需求是每天更新数据。全量重抓虽然省脑子,但接口压力大,而且随着历史数据变长,每次全量重写Excel会越来越慢。增量更新的原则是:本地有的不重抓,只补本地最后日期之后的数据。

def incremental_update(exist_df, new_df): if len(exist_df) == 0: return new_df last_date = exist_df["date"].max() append_part = new_df[new_df["date"] > last_date] merged = pd.concat([exist_df, append_part], ignore_index=True) merged = merged.drop_duplicates(subset=["date"], keep="last") merged = merged.sort_values("date").reset_index(drop=True) return merged

逻辑说明:last_date取本地数据的最大日期,注意不是当天日期,避免在盘中拿到未收盘的残缺数据。new_df里只保留大于last_date的行,再和旧数据拼接。concat之后用drop_duplicates守一道,防止边界日期重复。最后按日期排序并重置索引。

参数说明:

  • keep="last"在这很关键,如果新数据和旧数据在last_date这一天都有一行,应该保留新的。
  • append_part如果为空,说明行情还没更新,不要强行更新。
  • 这个函数适合日线,不适合分钟线。分钟线要考虑复权因子变化会导致历史价格重算,不能只做增量拼接。

6.2 用三个样本点验证抓取结果

抓完数据后,不要直接拿去算指标,先验证。我的习惯是抽三个交易日:最近一天、一个月前、除权日,和行情软件对比收盘价。再检查涨跌幅列的最大值和最小值是否在正常范围,比如个股日涨跌幅一般不超过20%。最后看最后一行的日期是否为最近交易日。整个验证过程最好封装成一个函数,每次增量更新后自动执行。

6.3 参数化批量抓取:加股票不改代码

最后一个习惯是把股票代码列表和日期范围放到配置里,爬虫只负责按配置执行。常见做法是维护一个tickers.json,写一个循环逐只抓取。批量抓取的关键是控制节奏,单线程加sleep比多线程好调试,至少先把可靠跑通再提速度。从那以后,我每次写完抓取脚本都会强制走一遍字段检查和样本验证,先跑通一只股再放量跑几十只。这个顺序帮我挡掉了至少三回批量翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询