news 2026/10/10 15:03:30

告别爬虫!用Tushare Pro高效构建股票历史数据库

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
告别爬虫!用Tushare Pro高效构建股票历史数据库

1. 从"爬虫抓行情"到"接口拿数据":先把路走对

1.1 为什么爬虫方案天然有天花板

很多人拿到"爬取股票历史数据"这个需求,第一反应就是写个爬虫去抓行情网站。这个思路不能说错,但实际跑起来会发现自己陷入一场无休止的猫鼠游戏:先是对面加了请求频率限制,你需要伪造请求头、维护代理池;接着你会发现列表页和详情页的数据对不上,复权价格永远算不准;最难受的是涨停历史数据这种高价值字段,很多网站要么不提供,要么渲染在JS里,爬下来还要自己做解析清洗。

我见过不止一个人,光在数据采集上就耗了两三周,最后拿到的表还是缺胳膊少腿的。所以对于"股票历史数据"这种强结构化、强时效性、高一致性要求的数据,最理性的方案不是爬虫,而是直接走数据接口。这也是为什么国内做量化的人,无论个人还是团队,最终都会回到Tushare这条路线上来。

1.2 三种主流取数方案对比

为了让你更清楚为什么选Tushare,我把市面上常见的三种取数方式放在一起做了个对比,大家可以根据自己的实际场景对号入座。

方案数据完整性维护成本复权支持限速与权限适合场景
自写爬虫抓行情网站字段碎片化,全市场抓取困难高,页面改版就要重写基本不支持,需自己算容易封IP爬取特定页面的临时任务
部分互联网财经免费接口有基础日线,但字段有限中,接口经常悄悄变更有限支持请求次数限制严格个人简单看盘
Tushare Pro接口全市场日线、复权因子、财务、涨停等完整字段低,接口稳定文档全内置复权因子,计算方便积分制,高积分高权限量化回测、数据研究、模型训练

从我自己的经验看,Tushare最核心的价值不是"多一个数据源",而是把最脏最累的数据规整工作替你做了。比如复权因子、交易日历、上市状态这些,都是标准化后直接可用的,不需要你再去各家公告里拼凑。

1.3 这条路适合谁、不适合谁

如果你是做量化策略回测、机器学习模型训练,或者想搭一套个人行情数据库,Tushare是选择。但如果你要的是毫秒级实时行情,或者高频tick级数据,Tushare并不是那一类方案,它更适合日线级别的历史数据研究,而不是极速交易链路。

所以这篇文章讨论的"爬取",本质是"通过Tushare接口批量、增量地拉取历史行情并落地存储"。

2. 注册、积分与token:真正动手前的三道门槛

2.1 注册后先搞清楚积分规则

Tushare的数据不是注册就能拿全量,它有一套积分权限体系。新注册用户会获得一定的基础积分,基础积分可以调用一部分低频、低数据量的接口,比如日线行情、股票列表等。但如果你要拉全市场历史数据、财务数据、或者高频调用,就需要积分达到一定阈值。

我当时第一次用的时候犯了个错误:代码写好,直接跑,结果返回抱歉,您没有访问该接口的权限。一查才知道是积分不够。所以动手前务必去官网查看最新的积分说明和对应接口权限,避免白写一晚上代码。

为了保险起见,你在注册后应该先检查两件事:一是账号当前积分,二是目标接口的最低积分要求。如果积分不够,也先别慌,Tushare有积分提升机制,通过完善个人信息、社区贡献、邀请等都可以提升。

2.2 token的正确保管方式

注册后你会拿到一个token字符串,这是所有接口调用的凭证。很多教程会告诉你直接把它贴在代码里,我强烈不建议这么做。一旦你的代码被传到公开仓库,token泄露就意味着别人可以冒用你的账号额度。

我在实际项目中习惯用环境变量或者独立配置文件来管理:

# Linux / mac 环境变量写法 export TUSHARE_TOKEN="你的token" # Windows PowerShell setx TUSHARE_TOKEN "你的token"

Python代码里直接读取,避免明文写在脚本中:

import os import tushare as ts # 从环境变量中读取token token = os.getenv("TUSHARE_TOKEN") if not token: raise ValueError("请先设置 TUSHARE_TOKEN 环境变量") ts.set_token(token)

2.3 最小可用Demo:拉取一只股票近一年的日线

环境准备完毕,先用最小代码验证整条链路是否通畅。注意,需要先安装一下tushare库:

pip install tushare pandas

然后写一个最小调用:

import tushare as ts import pandas as pd # 初始化接口 pro = ts.pro_api() # 拉取某只股票最近一年的日线 df = pro.daily( ts_code='000001.SZ', start_date='20240101', end_date='20241231' ) print(df.head()) print(df.shape)

如果你是从零开始的初学者,可能对ts_code的格式不太熟悉。它由股票代码加交易所后缀组成,000001.SZ表示深交所的平安银行,600000.SH表示上交所的浦发银行。后缀其实很容易记,SH是上海,SZ是深圳,BJ是北交所。

跑通这个demo之后,你会发现Tushare返回的dataframe非常规整,字段齐全,连中文列名都不需要你自己映射。但别高兴太早,后面才是真正考验人的地方。

3. 核心接口与复权机制:想要数据不出错,先搞懂这几点

3.1 为什么不直接调daily,而是用pro_bar

很多人在Tushare上拿到的第一份数据是pro.daily(),它返回的是未复权的原始价格。但做回测的时候,如果直接用未复权价格计算收益率,遇到分红送股的日子计算就会严重失真。

比如某只股票10派5元,除息日当天价格会"凭空"低开,你的策略可能会被误判为暴跌信号。所以真正专业的做法是使用复权价格。Tushare里最省心的接口是ts.pro_bar(),它已经帮你把复权逻辑封装好了,只需要通过adj参数指定复权方式:

import tushare as ts # 拉取前复权数据 df_qfq = ts.pro_bar( ts_code='000001.SZ', adj='qfq', # 前复权 start_date='20240101', end_date='20241231' ) # 拉取后复权数据 df_hfq = ts.pro_bar( ts_code='000001.SZ', adj='hfq', # 后复权 start_date='20240101', end_date='20241231' )

初次接触复权概念的读者,可以这样理解:前复权是保持最新价格不变,把历史价格按分红送股比例"调低";后复权是保持最早价格不变,把后续价格"调高"。两者趋势一致,但数值量级不同。日常回测中前复权用得最多,因为它能保证最近的价格是真实价格,方便和实时行情对照。

3.2 复权因子adj_factor到底要怎么用

Tushare的adj_factor接口返回每日复权因子,很多文档没有讲清楚这个因子和复权价之间的关系。实际上,daily接口的原始收盘价乘以当日的adj_factor,再除以一个基准日的adj_factor,就能算出任意口径的复权价,公式大致是:

前复权价 = 原始价 × (当日adj_factor / 最新交易日adj_factor) 后复权价 = 原始价 × 当日adj_factor

所以如果你想把所有股票的复权因子统一管理,完全可以一次性拉取全市场的adj_factor存到本地,后续通过因子自行计算复权价,而不必每次重复请求复权行情。这样既能减少接口调用次数,也能让本地数据和Tushare新接口保持解耦。

我当时做某量化回测项目时,就选择只存原始价格和复权因子两张表,需要复权数据时在本地算好,整个过程很快,而且逻辑透明,排查问题也方便。

3.3 返回字段里面那些容易误读的量纲

Tushare的日线字段基本是以下这些:

字段名含义量纲陷阱
ts_code股票代码注意带上交易所后缀
trade_date交易日期字符串格式YYYYMMDD,不是日期对象
open / high / low / close开高低收单位是元,未复权时为原始价
pre_close昨日收盘价用于计算涨跌幅
change涨跌额元
pct_chg涨跌幅百分比,注意是百分数不是小数
vol成交量单位是"手",1手=100股
amount成交额单位是"千元",不是元

这里最坑的地方是vol和amount。你要是直接用vol去乘收盘价算成交额,会发现数量级对不上,原因就是"手"和"千元"这两个量纲。我第一次用的时候,校验成交额算出来比真实值小了100倍,找了半天才发现是这个原因。建议入库前统一换算成股和元,后续计算会省心很多。

4. 批量抓全市场历史数据:限速条件下也有优雅方案

4.1 全量任务拆解思路

真实项目中几乎不会只抓一只股票,往往需求是"把A股全市场的历史日线都拉下来,越快越好"。这时你不能简单地写一个for循环遍历所有股票代码然后逐个请求,因为Tushare对单次调用频率有严格限制,积分不高的话,每秒只能请求几次,跑几千只股票耗时很长。

我的做法是把任务拆成两个维度:一个是时间维度,一个是股票维度。

时间维度上,Tushare的daily接口单次最多能返回多少条数据取决于你的积分,但即便没有明确条数上限,单次全市场返回的数据量也非常大,不利于断点续跑。所以我会按年切分时间段,比如2019年、2020年、2021年这样分开请求,每次只拉一个区间。

股票维度上,则把全市场股票列表按一定数量分组,例如每50只一组,组内串行请求,组间可以适当并发,但别忘了限速控制。

4.2 限速控制与重试机制

Tushare的限制通常体现为返回错误码,比如"每分钟访问次数超过限制"之类的提示。你在代码里要捕获这类异常,做一个退避重试。下面是一个简单版的限速重试封装:

import time import tushare as ts pro = ts.pro_api() def fetch_daily_with_retry(ts_code, start_date, end_date, max_retry=5): """带重试的日线获取函数""" for attempt in range(max_retry): try: df = pro.daily(ts_code=ts_code, start_date=start_date, end_date=end_date) return df except Exception as e: wait_seconds = 2 ** attempt # 2的幂次退避,1s、2s、4s... print(f"请求 {ts_code} 失败:{e},{wait_seconds}秒后重试") time.sleep(wait_seconds) return None

退避重试的核心思想是:第一次失败等1秒,第二次失败等2秒,第三次等4秒,逐步拉开间隔,给服务器喘息的机会。这种策略比固定等待更高效,因为并不是每次失败都需要等同样长的时间。

4.3 断点续跑:给任务做上进度标记

一次性拉完全市场历史数据可能要跑几十分钟甚至几个小时。一旦网络波动导致脚本中途崩溃,你就前功尽弃了。所以写批量脚本时,最好做一个简单的进度记录文件,每成功处理一只股票就记录一行,下次启动时跳过已完成的部分。

我当时的一个模拟项目里就是这么做的:

import os finished_file = "finished_stocks.txt" def load_finished(): """加载已完成的股票列表""" if not os.path.exists(finished_file): return set() with open(finished_file, "r") as f: return set(line.strip() for line in f if line.strip()) def mark_finished(ts_code): """标记一只股票已完成""" with open(finished_file, "a") as f: f.write(ts_code + "\n") finished = load_finished() for stock in all_stock_list: if stock in finished: continue df = fetch_daily_with_retry(stock, start_date, end_date) if df is not None and not df.empty: save_to_local(stock, df) mark_finished(stock)

这个方案的优点是即使中断,重跑时只会处理未完成的股票,不会重复请求已经成功的部分,既节省时间又降低被封风险。

5. 数据本地化:SQLite增量更新带来的持久收益

5.1 为什么我最终放弃了CSV

刚开始做数据落地时,图省事,按照"一只股票一个CSV"的方式存储。滞后发现这种方案非常痛苦:更新数据时,需要先读取整个CSV,再判断最新日期,再决定追加还是覆盖;要跨股票做筛选统计时,得把所有CSV遍历一遍,效率极其低下。

后来我改用SQLite,一套单文件数据库就把所有股票日线装进来,查询、更新、去重都变成了标准SQL操作。SQLite对个人项目来说足够稳定,不需要额外起服务,还支持事务,配合Python自带的sqlite3库就能直接用。

5.2 表结构设计与主键去重

建表时最核心的是主键设计。日线数据的天然唯一键是(ts_code, trade_date),只要保证同一只股票同一交易日只有一条记录,就不会出现重复。建表语句如下:

CREATE TABLE IF NOT EXISTS daily_data ( ts_code TEXT NOT NULL, trade_date TEXT NOT NULL, open REAL, high REAL, low REAL, close REAL, pre_close REAL, change REAL, pct_chg REAL, vol REAL, amount REAL, PRIMARY KEY (ts_code, trade_date) ); CREATE INDEX IF NOT EXISTS idx_trade_date ON daily_data (trade_date);

注意一点:trade_date我直接用TEXT存储,因为Tushare返回的日期就是格式化的字符串。如果你在Python里转成date对象,写库时还得做转换,容易出错。保持一致用字符串,查询时也方便排序。

写入数据时,使用"先查存在再插入"的逻辑,或者直接使用INSERT OR REPLACE,让SQLite帮你去重:

import sqlite3 def upsert_daily(conn, df): """将Tushare日线数据写入SQLite,重复数据自动覆盖""" rows = df[['ts_code', 'trade_date', 'open', 'high', 'low', 'close', 'pre_close', 'change', 'pct_chg', 'vol', 'amount']].values.tolist() sql = """ INSERT OR REPLACE INTO daily_data (ts_code, trade_date, open, high, low, close, pre_close, change, pct_chg, vol, amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ conn.executemany(sql, rows) conn.commit()

INSERT OR REPLACE的好处是简单粗暴,只要有重复记录就直接覆盖,非常适合从Tushare拉取完整日线后,直接全量同步到本地表。这种用法依赖主键约束,所以前面的建表语句一定不能省主键。

5.3 增量更新策略:只拉本地缺失的那段

数据库搭好之后,更新策略就非常清晰了:每次更新前先查一下每只股票在本地已有的最大交易日期,然后从那一天的下一个交易日开始拉新数据。

def get_max_trade_date(conn, ts_code): """查询某只股票本地最大已经存在的交易日""" sql = "SELECT MAX(trade_date) FROM daily_data WHERE ts_code = ?" cur = conn.execute(sql, (ts_code,)) row = cur.fetchone() return row[0] if row and row[0] else None

如果本地没有这只股票的数据,这个函数返回None,那就从历史最早日期开始全量拉取。如果有,就把start_date设为max_date加1天,利用Tushare的exchange规则,只请求本地缺失的部分。

增量更新还有个隐藏好处:每天跑定时任务时,接口调用量大大减少。全市场刷新一次可能只需要几分钟,对积分有限的用户来说非常友好。

6. 数据质量问题与排查链路:文档里没有明说的坑

6.1 停牌日期缺口超出你预期

A股股票经常因为各种原因停牌,停牌期间没有交易数据,接口也不会返回任何记录。这意味着你在做收益统计时,不能简单地把日期缺口的复权价当作上一交易日的延续,而应该做对应的填充或剔除处理。

我当时处理某只长期停牌股票时,发现连续60天没有数据,但复牌后第一天最高涨幅达到系统限制,直接把我的动量策略信号扭曲了。后来我一律以trade_cal交易日历为准,将所有股票的日期对齐到完整日历上,缺口数据用前值填充,这种处理才让回测结果稳定下来。

6.2 财务数据对齐是另一层地狱

如果你不只做行情,还要结合财务指标选股,比如市盈率、市净率、ROE,那就要小心了。Tushare的财务数据发布时间和报告期往往不一致,直接按报告期匹配会碰到未来函数问题。

最稳妥的方式是使用Tushare的income、balancesheet接口配合end_date和ann_date字段,其中ann_date是公告日期,用它去匹配交易日,才能保证在回测当天真的能拿到这份财报。由于这是个很容易出错的地方,我特别强调:永远用公告日期,而不是报告期。

6.3 指标字段突然为NaN的排查思路

项目中最常遇到的诡异问题就是某个字段某一天突然变成NaN,代码运行时也不报错,但最后统计结果就是不对劲。遇到这种情况,别急着改业务逻辑,先做以下三步排查:

  1. 打印出NaN出现的那一行,对比前后交易日的字段,看是不是除权除息导致数据本身缺失。
  2. 调用接口单独查询该日的原始数据,确认Tushare源头是否就没有返回该字段。
  3. 检查自己的清洗代码里是否有过度的空值替换,比如将0写成了NaN,或反向操作。

我遇到过最隐蔽的一次,是某一行的amount字段被接口返回为0,我以为是正常数值,后来发现是当时的公司没有成交记录。这类数据需要结合成交量字段综合判断,单独看很难察觉。

7. 进阶扩展:从日线到周月线、指数和涨停数据

7.1 周线与月线怎么拿

Tushare提供weekly和monthly接口,字段逻辑和daily基本一致。如果你做中期策略,直接在接口层面把周期切好,回测速度会快不少。

我记得一个简单用法:

# 周线数据 df_week = ts.pro_bar(ts_code='000001.SZ', freq='W', start_date='20240101', end_date='20241231') # 月线数据 df_month = ts.pro_bar(ts_code='000001.SZ', freq='M', start_date='20240101', end_date='20241231')

这里有件事要提醒:不同周期的数据是Tushare服务端聚合好的,它已经帮你处理了"每周最后一个交易日"这种逻辑,不会因为复权基准不同而出现偏差,所以优先使用接口而不是自己去daily表里重采样。

7.2 涨停历史数据脚本

前面网络热词里提到的"股票涨停历史数据",Tushare中也提供了相应字段。通过daily的pct_chg和close逻辑自行筛选,或者直接使用涨停相关的市场统计接口。我自己做短线策略时,通常用涨停股数量、连板高度这些统计特征作为情绪指标。

可以基于本地SQLite快速统计某一段时间的涨停股票数量:

SELECT trade_date, COUNT(*) AS limit_up_count FROM daily_data WHERE pct_chg >= 9.5 AND close = high GROUP BY trade_date ORDER BY trade_date;

需要注意A股不同板块的涨停幅度不同,主板是10%,创业板和科创板是20%,ST股票是5%。所以严谨的做法是先拿到每只股票的板块属性,再按板块设置不同的涨停阈值,不能用统一的9.5这个阈值去套所有股票。

7.3 与本地投资框架联动的个人体会

数据入库只是第一步。我目前经常用的模式是:每天收盘后跑一遍更新脚本,把新数据写入SQLite,然后直接在本地做因子计算、可视化、策略信号生成。整个过程不依赖任何外部服务,全部由Tushare加SQLite组成。

有一次我临时要做一个跨板块的历史回测,因为本地库里已经包含了所有股票、复权因子和交易日历,直接用SQL聚合,不到两分钟就完成了数据准备工作。如果没有前期的本地化沉淀,这肯定又是一次漫长的拉数过程。

从数据获取到数据清洗再到数据落地,Tushare给我的最大感受是:把宝贵时间留给策略研究和模型验证,而不是每天趴在网页解析器上跟动态渲染做斗争。你在搭建自己的历史数据体系时,越早看清这个方向,后面走的弯路就越少。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 14:59:08

MediaPipe+Unity虚拟人驱动实战:手部面部关键点实时映射

简介:本资源是一套基于Python与MediaPipe实现手部/面部关键点识别,并通过网络通信驱动Unity虚拟角色的完整跨平台开发方案,面向计算机视觉初学者、Unity开发者及XR交互应用实践者,解决实时生物特征识别与3D角色动画联动的技术落地…

作者头像 李华
网站建设 2026/10/10 14:56:31

MNIST手写数字识别实战:从数据加载到模型部署的完整指南

简介:这份资源面向深度学习入门者与需要快速验证手写数字识别效果的开发者,围绕MNIST数据集提供一套可直接运行的前馈神经网络训练方案,省去从零搭建与调参的重复劳动。压缩包共5个文件,以3个Python脚本和2个h5模型文件为主&#…

作者头像 李华
网站建设 2026/10/10 14:54:18

从零搭建Matrix Synapse自建即时通讯服务器:部署、调优与避坑指南

1. 从零认识Synapse:为什么自建即时通讯服务值得折腾很多人第一次听到"自建即时通讯服务器"这个概念时,第一反应是——现在聊天软件这么多,为什么还要自己搭一套?我当初也是这个想法,直到有一次团队内部讨论…

作者头像 李华
网站建设 2026/10/10 14:52:54

基于SpringBoot+Vue+MySQL的船舶监造管理系统实战解析

做船舶监造的人肯定都懂,监造不是坐在办公室看看图纸就行,真正业务一铺开,报验单、现场见证、NCR整改闭环、试验计划、图纸送审,每个环节都是需要“有人跟、有记录、有闭环”的。早几年我在船厂和监造组干活时,全靠Exc…

作者头像 李华
网站建设 2026/10/10 14:48:47

汉明码纠错原理与C语言实现:从(7,4)到(12,8)及ECC内存

做嵌入式通信和存储的同学,大概率都遇到过这种诡异情况:数据在链路上跑一圈回来,某个字节悄无声息地变了,最常用的奇偶校验却只告诉你“出错了”,至于哪一位出错,一脸茫然。汉明码就是专门解决“单比特翻转…

作者头像 李华