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 你的tokenPython代码里直接读取避免明文写在脚本中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_code000001.SZ, start_date20240101, end_date20241231 ) 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_code000001.SZ, adjqfq, # 前复权 start_date20240101, end_date20241231 ) # 拉取后复权数据 df_hfq ts.pro_bar( ts_code000001.SZ, adjhfq, # 后复权 start_date20240101, end_date20241231 )初次接触复权概念的读者可以这样理解前复权是保持最新价格不变把历史价格按分红送股比例调低后复权是保持最早价格不变把后续价格调高。两者趋势一致但数值量级不同。日常回测中前复权用得最多因为它能保证最近的价格是真实价格方便和实时行情对照。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_retry5): 带重试的日线获取函数 for attempt in range(max_retry): try: df pro.daily(ts_codets_code, start_datestart_date, end_dateend_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代码运行时也不报错但最后统计结果就是不对劲。遇到这种情况别急着改业务逻辑先做以下三步排查打印出NaN出现的那一行对比前后交易日的字段看是不是除权除息导致数据本身缺失。调用接口单独查询该日的原始数据确认Tushare源头是否就没有返回该字段。检查自己的清洗代码里是否有过度的空值替换比如将0写成了NaN或反向操作。我遇到过最隐蔽的一次是某一行的amount字段被接口返回为0我以为是正常数值后来发现是当时的公司没有成交记录。这类数据需要结合成交量字段综合判断单独看很难察觉。7. 进阶扩展从日线到周月线、指数和涨停数据7.1 周线与月线怎么拿Tushare提供weekly和monthly接口字段逻辑和daily基本一致。如果你做中期策略直接在接口层面把周期切好回测速度会快不少。我记得一个简单用法# 周线数据 df_week ts.pro_bar(ts_code000001.SZ, freqW, start_date20240101, end_date20241231) # 月线数据 df_month ts.pro_bar(ts_code000001.SZ, freqM, start_date20240101, end_date20241231)这里有件事要提醒不同周期的数据是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给我的最大感受是把宝贵时间留给策略研究和模型验证而不是每天趴在网页解析器上跟动态渲染做斗争。你在搭建自己的历史数据体系时越早看清这个方向后面走的弯路就越少。
阅读完成 · 觉得有帮助?