做股票数据分析、策略回测或者量化研究的朋友应该都懂,想要把研究做扎实,第一步也是最关键的一步,就是拿到高质量、全行业的股票数据。但我接触过很多刚入门的朋友,也自己踩过不少坑,总结下来无非就是三个最头疼的问题:要么数据东一块西一块,分类整理起来头大;要么下载太频繁,IP 直接被封,之前做的全白费;要么全靠手动整理数据,耗时间还容易出错。
今天我就把这套基于 Baostock 免费接口搭建的全流程自动化方案毫无保留的分享出来,完全不用花一分钱,还专门针对 “封 IP” 这个痛点做了优化。整套方案靠 main.py 和 read_stock.py 两个脚本配合,从下载行业总表、拆分数据,到自动创建各行业目录,再到批量下载股票数据,全程不用手动干预。而且我把代码里的关键技巧都做了简化,哪怕你是刚接触 Python 的编程新手,照着教程改改配置项就能直接用,再也不用为数据和 IP 封禁的问题发愁!
下面是我编写程序的部分截图:
做程序的几个Python文件
代码运行截图(正在下载股票数据)
以行业代码分类的目录
工业编号为C39的已下载股票数据(20250101-20251231)
一、核心逻辑:一套“分工明确”的股票数据下载架构这套方案的核心思路是“先分类、再下载”,通过两个Python脚本的协同配合,将复杂的批量下载任务拆解为4个简单步骤,既保证了数据的条理性,又降低了IP被封禁的风险。整体架构如下:
main.py:负责“源头数据获取”——下载全市场行业板块总数据,为后续分类提供基础;数据拆分:将总数据拆分为“行业分类索引”和“各行业股票代码列表”两部分;目录与文件初始化:自动创建各行业专属目录,将对应股票代码存储为CSV文件;read_stock.py:负责“精细化下载”——基于各行业的股票代码列表,批量下载K线数据并按行业分类存储。整个流程无需手动干预,从数据获取到分类存储一气呵成,完美解决了“数据混乱”“下载中断”“IP被封”三大痛点。下面我们按步骤详细拆解实现过程。
二、选择csv股票数据存储结构的原因前期的文章中,分析过股票存储结构的四种方式,但就近期测试结果来看,最终还是选择了csv股票数据存储结构。对于我们这些股票数据分析的新手来说,Parquet固然有比较多的节省存储空间、快速访问的优势,数据无法实现快捷直视,对于新手来说还是有些难以控制。
1、零成本适配免费方案做量化研究的新手,最忌讳一上来就被 “环境配置、付费工具” 劝退,而 CSV 刚好完美适配 Baostock 免费接口的使用场景。CSV 是纯文本格式,不需要安装 MySQL、MongoDB 这类数据库软件,可直接用记事本或者excel打开查看,数据变化情况可直观查看,另外用excel可以直接对数据做分析、曲线生成,便于数据分析结果后曲线变化的跟踪比对。
2、轻量灵活,完美适配防封策略的 “断点续传” 核心需求每只股票的数据单独存为一个 CSV 文件中,数据下载从哪里断开的可以再接着下载,不必因为其他数据因无法查看问题,而只能重新下载,减少了重复发起数据请求的量,大幅度降低了封IP的风险。
3、新手友好,排查问题 + 数据复用的成本极低因csv文件可直接用excel、记事本打开,遇到数据字段变化、数据不清晰、无法正常理解等问题时,可有针对的直接查看源文件,规避了很多不必要的麻烦。
4、扩展性强,新手入门→进阶研究都能适配csv数据本身具有方便查阅的优势,同时csv文件可以很方便的与其他数据库对接(诸如导入的方式),数据结构不发生变化就能快速的进阶到数据库管理。
三、快速实现对行业数据总表的下载与拆分要实现全行业股票数据的分类下载,首先需要获取“股票代码-行业分类”的对应关系,这一步就由main.py来完成,它主要由三个功能实现对行业数据总表的下载与拆分工作:
3.1 baostock库的登录与登出功能Pythonimport baostock as bs
lg = bs.login()##---其他代码bs.logout()
baostock 是一个免费的证券数据接口库,用于获取沪深 A 股的历史行情、财务数据等。它基于客户端 - 服务器(C/S)模式工作,login()和logout()是管理客户端与 baostock 服务器会话连接的核心函数。
3.2 数据下载数据下载功能中的关键代码如下:
Pythonrs = bs.query_stock_industry()
if rs.error_code != '0':
print(f"? 行业板块数据获取失败:{rs.error_msg}")
return pd.DataFrame()
# 读取分页数据
data_list: List[List[str]] = []
while (rs.error_code == '0') & rs.next():
row_data = rs.get_row_data()
data_list.append(rs.get_row_data())
#time.sleep(REQUEST_DELAY) # 逐行延迟,防高频
流程说明:
(1)这块代码中首先通过query_stock_industry()函数,得到数据集rs;
(2)建立了分页数据的数据列表data_list
(3)rs从头到尾不断读取数据,并将读取到的每条记录,通过append函数添加到data_list列表中。
3.3总表数据拆分和数据存储功能(1)总表数据拆分一般从baostock下载下来行业数据主要由四个字段组成:
UpdateDate:数据更新日期
Code:股票代码
Code_name:股票名称
Industry:工业名称(含工业代码)
这样保存的代码不符合常规数据查询的要求,因此在这里对这个表结构进行调整,最后形成了如下三个字段结构:
序号:排列编号值,自动生成
工业编号:由工业名称(Industry)中的前三个字符生成
工业名称:由工业名称(Industry)中的三个字符以后的字符生成
具体实现逻辑如下:
从industryClassification字段中提取工业编号(如C27、K70)和行业名称(如医药制造业、房地产业);去重后保存为“总表.csv”,存储路径为E:\py\stock-Baostock\行业分类数据\总表.csv;这个表的作用是后续遍历创建目录的依据,比如看到工业编号C27,就知道对应医药制造业,创建C27文件夹。(2)总表和行业分类表的存储Python# 2. 创建总表和行业分页
total_table, industry_pages = create_tables(df)
# 3. 保存到文件
output_excel = '行业板块分类数据.xlsx'
output_csv_folder = '行业分类数据'
save_to_files(total_table, industry_pages, output_excel, output_csv_folder)
这里面有两个核心代码:
create_tables()函数是将从读取的数据,建立总表和行业分类表
总表的建立和存储要求前面以有陈述,这里不在赘述;行业分类表的创建主要是将每个行业生成独立的CSV文件,文件名格式为“工业编号+行业名称.csv”(如C27医药制造业.csv)该CSV文件中会包含该工业分类下的所有股票,而这些股票生成的各类数据页存储在该目录下,为方便后期各类股票归类、提取等操作提供方便。拆分后的好处的是:后续下载时只需遍历每个行业的CSV文件,无需再次处理总表,既提高了效率,又便于维护——如果某个行业的数据需要重新下载,只需操作对应的文件即可,无需动整个数据集。四、创建行业分目录,规范存储股票代码列表数据拆分完成后,下一步就是创建各行业的专属目录,并将拆分后的股票代码列表存入对应目录。这一步的核心目标是“分类存储、便于检索”,避免后续下载的股票数据混乱。
4.1 目录创建逻辑:基于总表自动生成这部分功能主要在read_stock.py的read_GenTable和read_IndustryTable函数中实现,核心逻辑是“读取总表→遍历工业编号→创建对应目录”:
(1)读取总表:编码兼容+异常捕获Pythondef read_GenTable(folder_path):# 检查文件夹是否存在if not os.path.exists(folder_path):print(f"错误:文件夹 '{folder_path}' 不存在!")return Nonefile_path = os.path.join(folder_path, "总表.csv")df = Nonetry:df = pd.read_csv(file_path, encoding='utf-8-sig')print(f"成功读取文件:{file_path},数据行数:{len(df)}")except UnicodeDecodeError:df = pd.read_csv(file_path, encoding='gbk') # 注意:原代码是gkb,此处修正为gbkexcept Exception as e:print(f"读取文件 {file_path} 失败:{str(e)}")return df
这里有一个关键的“避坑技巧”——编码兼容处理:
Windows系统下生成的CSV文件可能是gbk编码,而Python默认用utf-8读取,直接读取会出现“解码错误”;代码中用try-except捕获UnicodeDecodeError,当utf-8-sig解码失败时,自动切换为gbk编码,确保总表能成功读取;同时捕获其他异常(如文件损坏、路径错误),并打印错误信息,便于排查问题。(2)遍历工业编号,创建行业目录Pythondef read_IndustryTable(df):# 1.校验输入类型是否为空if not isinstance(df, pd.DataFrame):print(f"错误:传入的不是DataFrame,而是{type(df)}类型")return None# 2.校验[工业编号]列是否存在if "工业编号" not in df.columns:print("错误:DataFrame中未找到「工业编号」列")return None# 提取[工业编号](去重+过滤空值)industry_num_series = df["工业编号"]industry_list = industry_num_series.dropna().tolist()# 3.遍历编号,拼接路径并创建目录for industry_num in industry_list:industry_num_str = str(industry_num).strip()if industry_num_str == "未分类":continue # 跳过未分类的行业industry_folder = os.path.join(FOLDER_PATH, industry_num_str)if not os.path.exists(industry_folder):os.makedirs(industry_folder) # 不存在则创建目录print(f"? 创建行业目录:{industry_folder}")else:print(f"?? 行业目录已存在:{industry_folder}")
这段代码的核心是“按工业编号创建目录”,有几个细节值得关注:
输入校验:先判断传入的是否为DataFrame,再检查是否有“工业编号”列,避免因数据格式错误导致程序崩溃;数据清洗:用dropna()过滤空值,避免遍历到NaN值导致目录创建失败;用strip()去除编号前后的空格,避免创建“ C27 ”(带空格)这样的无效目录;跳过“未分类”:总表中存在“未分类”的条目,这些股票没有明确行业归属,直接跳过,避免创建无用目录;幂等性设计:创建目录前先判断是否存在,不存在才创建,避免重复创建导致的错误,即使程序中途中断,重新运行也不会有问题。4.2 存储股票代码列表:规范命名+结构统一目录创建完成后,将拆分后的各行业股票代码列表存入对应目录,文件名格式为“工业编号+行业名称.csv”(如C27医药制造业.csv),文件内容包含code(股票代码)和code_name(股票名称)两列。
这样设计的好处是:
文件名直观:一眼就能知道文件对应的行业,便于手动查找和维护;结构统一:所有行业文件的列名一致,后续遍历下载时无需修改代码,提高通用性;数据精简:只保留股票代码和名称,避免冗余字段占用存储空间,提高读取效率。五、批量下载股票数据,按行业分类存储这是整个方案的核心执行步骤,基于第二步创建的行业目录和股票代码列表,通过read_stock.py的get_single_stock_data函数批量下载股票K线数据,并存储到对应行业目录中。
5.1 单只股票数据下载:字段丰富+数据清洗Pythondef get_single_stock_data(stock_code, start_date, end_date):"""读取单只股票的K线数据(默认日线)"""# 初始化Baostock连接lg = bs.login()print(f"登录结果:{lg.error_msg}")# 定义需要获取的字段(14个核心指标)fields = "date,code,open,high,low,close,volume,amount,pctChg,turn,peTTM,pbMRQ,psTTM,pcfNcfTTM"# 调用接口获取历史K线数据rs = bs.query_history_k_data_plus(code=stock_code, # 股票代码fields=fields, # 要获取的字段start_date=start_date, # 开始日期end_date=end_date, # 结束日期frequency="d", # 频率:d=日线,w=周线,m=月线adjustflag="3" # 复权类型:3=不复权(0=后复权,1=前复权))# 解析数据到列表data_list = []while (rs.error_code == '0') & rs.next():data_list.append(rs.get_row_data())# 关闭Baostock连接(必须执行)bs.logout()# 转换为DataFrame并处理数据类型if data_list:df = pd.DataFrame(data_list, columns=rs.fields)# 将数值型字段转为float(默认是字符串)numeric_fields = ["open", "high", "low", "close", "volume", "amount", "pctChg", "turn", "peTTM", "pbMRQ", "psTTM", "pcfNcfTTM"]for col in numeric_fields:df[col] = pd.to_numeric(df[col], errors='coerce') # 无法转换的设为NaNprint(f"成功获取 {stock_code} 数据,共 {len(df)} 条")return dfelse:print(f"未获取到 {stock_code} 数据,错误信息:{rs.error_msg}")return None
这个函数是下载股票数据的核心,包含了“连接-请求-解析-清洗-返回”的完整流程,亮点如下:
(1)字段丰富,满足多样化需求一次性获取14个核心指标,覆盖了技术分析和基本面分析的关键数据:
价格类:开盘价(open)、最高价(high)、最低价(low)、收盘价(close);交易量类:成交量(volume)、成交额(amount);波动类:涨跌幅(pctChg)、换手率(turn);估值类:滚动市盈率(peTTM)、市净率(pbMRQ)、滚动市销率(psTTM)、滚动市现率(pcfNcfTTM)。用户可以根据需求增删字段,比如需要MACD、KDJ等技术指标,可以在fields参数中添加对应的字段名(Baostock接口支持的字段可参考官方文档)。
(2)数据类型清洗:避免后续分析出错Baostock接口返回的数据默认是字符串类型,直接用于分析会导致“字符串无法运算”的错误。代码中通过pd.to_numeric将12个数值型字段转为float类型,同时设置errors='coerce'——遇到无法转换的值(如异常字符、空值)时,自动转为NaN,既保证了数据类型的统一性,又避免了程序崩溃。
(3)连接管理:登录后必登出,避免资源占用每下载一只股票,都会执行“登录→请求→登出”的流程,而不是长期保持登录状态。这样做的原因是:
Baostock的连接有超时限制,长期保持登录可能会被强制断开;频繁登录/登出虽然看似繁琐,但能模拟人工操作,降低被判定为爬虫的风险;避免因程序中途中断导致连接未关闭,占用接口资源。5.2 批量下载逻辑:遍历行业+逐股下载Python# 4.遍历查找*.csv文件(行业股票列表)for filename in os.listdir(industry_folder):# 过滤条件:文件名以行业编号开头(如K70房地产业.csv),避免读取sh.600015.csv这类文件if not filename.startswith(industry_num_str):print(f"?? 跳过非行业列表文件:{filename}")continueif not filename.lower().endswith(".csv"):continuefile_full_path = os.path.join(industry_folder, filename)print(f"\n 正在处理行业文件:{file_full_path}")# 读取csv文件(行业股票列表)try:df_industry = pd.read_csv(normalized_path, encoding='utf-8-sig')print(f"成功读取文件:{normalized_path},数据行数:{len(df_industry)}")# 提取[code]一列的列值stock_code = df_industry["code"]# 去除空值(避免列表中有NaN)stock_code = stock_code.dropna().tolist()for code in stock_code:# 调用函数获取数据stock_df = get_single_stock_data(code, START_DATE, END_DATE)# 保存数据到当前行业目录stock_df.to_csv(f"{industry_folder}/{code}.csv", index=False, encoding="utf-8-sig")# 延时:2-5秒随机延时time.sleep(random.uniform(2, 5))except UnicodeDecodeError:df_industry = pd.read_csv(normalized_path, encoding='gbk')except Exception as e:print(f"读取文件 {normalized_path} 失败:{str(e)}")
这段代码实现了“遍历行业→读取股票列表→逐股下载→保存”的自动化流程,核心细节如下:
(1)文件过滤:避免读取错误文件行业目录中除了“行业股票列表.csv”(如C27医药制造业.csv),后续还会存储下载的股票数据文件(如sh.600007.csv)。为了避免误读这些文件,代码加入了双重过滤:
前缀过滤:filename.startswith(industry_num_str)——只读取以行业编号开头的文件(如C27开头的文件);后缀过滤:filename.lower().endswith(".csv")——只读取CSV文件,避免读取文件夹或其他格式文件。这一设计确保了程序只会处理“任务清单”文件,不会被其他文件干扰。
(2)逐股下载+按行业保存提取股票代码:从行业股票列表文件中提取code列,去除空值后转为列表;逐股调用下载函数:遍历股票代码列表,调用get_single_stock_data下载单只股票数据;保存路径规范:下载的股票数据文件以“股票代码.csv”命名(如sh.600007.csv),直接保存到对应行业目录中。这样最终的目录结构会非常清晰:
Plain Text行业分类数据/├─ C27/│ ├─ C27医药制造业.csv(股票代码列表)│ ├─ sh.600007.csv(单只股票数据)│ ├─ sz.000002.csv(单只股票数据)│ └─ ...├─ K70/│ ├─ K70房地产业.csv(股票代码列表)│ ├─ sh.600009.csv(单只股票数据)│ └─ ...└─ 总表.csv(行业分类索引)
后续分析时,只需进入对应行业目录,就能获取该行业所有股票的数据,无需手动筛选,效率极高。
六、关键技术:防封IP+优化技巧,确保下载连续稳定批量下载股票数据时,最担心的就是“IP被封”导致下载中断,这套方案通过多重技术手段,最大限度降低封禁风险,同时保证下载效率。
6.1 核心防封技术:随机延时,模拟人工操作这是方案中最关键的防封手段,代码中主要有两处延时设计:
(1)登录后延时([main.py](main.py))Pythontime.sleep(REQUEST_DELAY) # REQUEST_DELAY=0.2秒
登录是单次请求,延时0.2秒主要是为了避免“瞬间登录+瞬间请求”的高频行为,让请求节奏更平缓。
(2)逐股下载延时([read_stock.py](read_stock.py))Pythontime.sleep(random.uniform(2, 5))
这是最核心的延时设计,为什么选择“2-5秒随机延时”而不是固定延时?
固定延时的弊端:如果设置固定3秒延时,容易被接口识别为“机器规律请求”,增加封禁风险;随机延时的优势:2-5秒的随机区间,模拟了人工操作的不确定性——人下载数据时,每只股票的查看时间不可能完全一致,随机延时能最大程度降低被判定为爬虫的概率;延时时长选择:2-5秒是经过实践验证的安全区间——太短(如1秒以内)仍有高频风险,太长(如10秒以上)会导致下载效率极低(全市场4000多只股票,按平均3秒/只计算,约3.5小时完成,效率尚可)。6.2 其他优化技巧:细节决定成败除了延时技术,这套方案还包含多个“小技巧”,这些细节看似不起眼,却能大幅提升程序的稳定性和容错性。
(1)编码兼容:解决Windows系统解码错误Windows系统默认的CSV编码是gbk,而Mac/Linux系统是utf-8,为了兼容不同系统,代码中所有读取CSV的地方都加入了编码容错:
Pythontry:df = pd.read_csv(file_path, encoding='utf-8-sig')except UnicodeDecodeError:df = pd.read_csv(file_path, encoding='gbk')
utf-8-sig编码能兼容带BOM的UTF-8文件,gbk编码适配Windows系统,双重保障确保文件能成功读取,避免因编码问题导致程序中断。
(2)数据校验:过滤无效股票代码代码中对股票代码进行了多重校验,避免因无效代码导致请求失败:
去除空值:stock_code = stock_code.dropna().tolist()——过滤列表中的NaN值;格式隐含校验:Baostock接口要求股票代码必须是9位(如sh.600000、sz.000002),如果代码格式错误(如缺少前缀、长度不对),get_single_stock_data函数会返回错误信息并跳过该股票,不会影响后续下载。(3)异常捕获:避免单点失败导致整体中断程序中加入了多层try-except异常捕获:
读取文件异常:捕获解码错误、文件损坏、路径错误等,打印错误信息后跳过该文件,继续处理下一个行业;数据获取异常:如果某只股票数据获取失败(如代码无效、接口临时不可用),函数返回None,后续可通过判断stock_df is not None再保存,避免NoneType错误导致程序崩溃。(4)幂等性设计:支持断点续传程序的多个环节都考虑了“幂等性”——即重复执行不会产生错误或冗余数据:
目录创建:先判断是否存在,不存在才创建;数据保存:股票数据文件以代码命名,若已存在,重新运行会覆盖原有文件(如需保留历史数据,可修改文件名格式,如sh.600007_2025.csv);去重处理:总表数据和行业股票列表都进行了去重,避免重复下载同一股票。这意味着,即使程序中途因网络中断、IP临时限制等原因停止,重新运行后会从上次中断的地方继续下载,无需从头开始,大幅节省时间。
(5)路径标准化:兼容不同系统代码中使用os.path.join和os.path.normpath处理路径:
Pythonindustry_folder = os.path.join(FOLDER_PATH, industry_num_str)normalized_path = os.path.normpath(file_full_path)
os.path.join:自动适配不同系统的路径分隔符(Windows用\,Mac/Linux用/),避免手动拼接路径导致的错误;os.path.normpath:标准化路径字符串,处理路径中的..、重复分隔符等,确保路径格式统一。6.3 额外防封建议:进阶优化如果需要下载的数据量极大(如全市场+多年历史数据),还可以加入以下进阶技巧,进一步降低封禁风险:
(1)增加重试机制对get_single_stock_data函数加入重试逻辑,当接口返回临时错误(如502、504)时,自动重试2-3次:
Pythonfrom retrying import retry@retry(stop_max_attempt_number=3, wait_random_min=2000, wait_random_max=5000)def get_single_stock_data(stock_code, start_date, end_date):# 原有逻辑不变
需要先安装retrying库(pip install retrying),重试间隔2-5秒,避免瞬间重复请求。
(2)限制单日下载数量Baostock作为免费接口,可能对单日请求次数有隐性限制,建议单日下载股票数量不超过1000只,分多天完成,降低封禁风险。
(3)更换IP代理(进阶)如果IP被临时封禁,可以使用IP代理池切换IP,继续下载。但需注意:免费代理IP稳定性较差,付费代理更可靠,且使用时需遵守Baostock的用户协议。
七、总结:一套免费、高效、稳定的股票数据解决方案这套基于Baostock接口的股票数据下载方案,通过“总表下载→分类拆分→目录创建→批量下载”的结构化流程,实现了全行业股票数据的自动化获取,核心优势如下:
免费无门槛:Baostock接口完全免费,无需注册付费账号,只需安装依赖即可使用;分类清晰:按行业创建目录,股票数据按行业存储,后续分析无需手动筛选;防封可靠:随机延时+多重优化技巧,最大限度降低IP封禁风险,支持断点续传;灵活适配:支持自定义日期范围、字段选择、保存路径,满足不同分析需求;新手友好:代码结构清晰,配置项简单,加入详细打印信息,便于排查问题。无论是量化交易新手、股票数据分析爱好者,还是需要批量获取数据进行策略回测的研究者,这套方案都能快速满足需求。按照本文的步骤,只需修改少量配置项,就能轻松下载全行业股票数据,为后续的分析工作打下坚实基础。
最后提醒:Baostock接口的稳定性依赖于社区维护,使用时请遵守接口的使用规范,不要过度高频请求,共同维护免费资源的可用性。如果在使用过程中遇到问题(如编码错误、股票代码格式错误、IP被封),可以查看程序打印的错误信息,针对性排查——大部分问题都能通过调整编码、检查股票代码格式或暂停下载一段时间解决。