ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python股票数据爬虫实战:从接口解析到CSV存储

Python股票数据爬虫实战:从接口解析到CSV存储 简介面向数据分析、量化投研或金融建模开发者这套源码实现方案提供了一条基于Python爬取股票数据的快捷路径可直接复用到个人项目中。资源共10个文件压缩包仅7KB以Python主脚本为核心辅以编译缓存文件与IDE工程配置便于直接导入开发环境查看或扩展目前已有993人学习。方案覆盖两条技术路线一是通过yfinance库一键获取雅虎财经的历史行情与实时报价二是使用requests与BeautifulSoup解析HTML页面自定义抓取开盘价、收盘价、最高价、最低价及成交量字段。工程内保留的项目配置与缓存结构有助于理解Python工程组织方式及运行机制。读者可据此搭建自己的股票数据采集模块并借鉴合规抓取、异常处理等实践经验为后续投资分析与量化建模提供相对稳定、可持续的数据源。1. 项目概述与核心思路股票数据爬取这大概是Python爬虫圈里最常被问到、也最值得动手做的一个方向了。不管你是想做量化分析、盯盘辅助还是单纯想把股价历史数据存下来做可视化第一步都是“先得有数据”。而这个项目标题说得很直接——用Python实现一套股票数据的爬取方案并且要有完整的源码。先说清楚这套方案能解决什么问题。市面上的股票数据接口并不少但要么收费要么有频率限制要么返回格式乱七八糟。自己动手写爬虫最大的好处是可以按自己的需求定制想爬日线、分钟线、实时行情想存CSV还是数据库想跑一次还是每天定时更新全都由你说了算。适合的人群也很明确刚学完Python基础、想用真实项目练手的朋友有投资分析需求但不想买昂贵数据服务的个人用户以及想快速搭一套数据采集管道的开发者。我个人的态度是股票数据爬取本质上是一个“数据获取—清洗—存储”的完整链路搞懂它之后你再去爬别的网站、处理其他类型的数据思路是完全通用的。所以这篇文章我不打算只丢一段代码就跑而是把整个方案的从零设计过程、每个关键节点的选择逻辑、踩过的坑都尽量展开来讲。2. 数据源选型与方案设计2.1 为什么我不建议直接爬网页HTML先说说一个常见的误区。很多人一提到“爬股票数据”第一反应是用Requests或Selenium去抓东方财富、同花顺、雪球的页面然后用解析库去提取HTML里的表格数据。这个思路不能说错但效率太低了。原因有两个一是网页结构经常改版你今天写好的XPath或者CSS选择器明天页面一改就全部失效维护成本极高二是HTML页面里通常包含大量无关的布局代码、脚本、广告你要的数据可能只是其中一小段JSON或者一个表格解析起来又慢又痛苦。更关键的是三大行情网站都有比较严格的反爬机制请求频率稍微高一点就会被封IP你连页面都打不开。我建议的方案是优先找这些网站背后的数据接口HTTP API。它们通常是在页面加载时通过Ajax请求拿到的JSON数据数据结构干净、字段完整、请求方式简单而且只要请求头伪装得当触发反爬的概率远低于直接爬HTML页面。2.2 免费数据源横向对比在我实际测试过的方案里有四个数据源值得放在一起对比各有各的优缺点。数据源是否需要注册数据完整性请求频率限制适合场景新浪财经接口否日线/分钟线/实时行情都有较宽松快速验证、轻量使用腾讯财经接口否实时行情为主历史数据一般较宽松实时行情监控akshare库否本质封装其他源覆盖面极广包含财务、基金、期货等受原始数据源限制一站式获取多类数据baostock库免费注册历史日线、分钟线、除权除息等非常完整无严格限制量化分析、历史回测如果你想要最简单、最容易上手的方案我个人推荐用akshare。它就是一个Python库把所有公开的财经数据接口帮你封装好了一句import akshare就能拿到绝大多数你想要的数据。但akshare也有个问题——它底层的很多数据源是公开网页接口稳定性一般偶尔会出现字段变更或接口失效的情况。所以在这篇文章的源码方案里我打算采用“新浪财经接口为主、akshare为辅”的策略。新浪接口的稳定性经过多年考验数据格式也比较统一适合作为主数据源而akshare则作为补充用来获取财务指标、板块信息这类新浪接口不提供的扩展数据。2.3 技术栈与环境准备整个项目用到的Python库非常少核心只有四个requests发HTTP请求、pandas数据处理、csv写入文件、json解析返回数据。如果要用akshare作为辅助数据源就额外加一个akshare。环境方面我建议直接用Anaconda来管理因为它自带了pandas、numpy这类常用数据处理库省去了很多安装依赖的麻烦。版本上使用Python 3.9以上即可不需要最新的3.13因为有些第三方库对新版本的适配会滞后。安装命令很简单pip install requests pandas akshare这里没有用虚拟环境是因为这个项目的依赖非常少不太存在版本冲突的问题。如果你的机器上装了多个Python版本还是建议用conda create -n stock python3.10单独建一个环境避免和系统环境打架。3. 核心源码解析与实现3.1 数据接口分析与参数计算我这套方案的主数据源是新浪财经的K线数据接口它的请求URL格式是这样的https://money.finance.sina.com.cn/quotes_service/api/json_v2.php/CN_MarketData.getKLineData?symbolsh600519scale240manodatalen100这个URL里几个关键参数的含义symbol股票代码格式是“市场前缀代码”。上海交易所是sh深圳交易所是sz。比如贵州茅台就是sh600519平安银行是sz000001。scaleK线周期单位是分钟。240代表日线一天4小时交易时间60代表60分钟线30代表30分钟线5代表5分钟线。datalen返回的数据条数。如果你想要最近250个交易日的日线数据一年的交易量这里就填250。ma是否计算均线一般填no即可均线可以自己后续用pandas算。这里有一个值得说说的参数计算逻辑。如果你想要特定时间范围内的数据比如从2023年1月1日到现在直接用datalen不太够因为你无法提前知道这段时间有多少个交易日。我的做法是先用一个较大值比如1000把数据拉回来再用pandas的日期过滤功能截取目标区间。这样更稳健不用去猜交易日数量。3.2 完整源码实现下面这段是我在实际项目中一直在用的核心代码我把异常处理、参数校验、重试机制都加了进去可以直接拿去做二次开发import requests import pandas as pd import json import time import random from typing import Optional, Dict, List class StockDataCrawler: 股票K线数据爬虫 def __init__(self, retry_times: int 3, timeout: int 10): self.session requests.Session() self.session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://finance.sina.com.cn, Accept: */*, }) self.retry_times retry_times self.timeout timeout def _normalize_symbol(self, code: str) - str: 将纯数字代码转换为带市场前缀的完整代码。 规则如下 6开头是上证sh0开头是深证sz3开头是创业板sz 688开头是科创板sh4/8开头是新三板bj走北交所接口。 code code.strip() if code.startswith((sh, sz, bj)): return code if code.startswith(6): return sh code elif code.startswith((0, 3)): return sz code elif code.startswith(4) or code.startswith(8): return bj code else: raise ValueError(f无法识别的股票代码: {code}) def get_kline_data( self, code: str, scale: int 240, datalen: int 250, start_date: Optional[str] None, end_date: Optional[str] None, ) - pd.DataFrame: 获取K线数据。 参数说明 - code: 股票代码如 600519 或 sh600519 - scale: K线周期240日线6060分钟3030分钟 - datalen: 最大返回条数 - start_date/end_date: 日期过滤范围格式 2023-01-01 symbol self._normalize_symbol(code) url ( https://money.finance.sina.com.cn/quotes_service/api/json_v2.php/ CN_MarketData.getKLineData ) params { symbol: symbol, scale: scale, ma: no, datalen: datalen, } for attempt in range(self.retry_times): try: resp self.session.get(url, paramsparams, timeoutself.timeout) resp.raise_for_status() data json.loads(resp.text) if not data: return pd.DataFrame() df pd.DataFrame(data) df.columns [date, open, high, low, close, volume] df[date] pd.to_datetime(df[date]) for col in [open, high, low, close]: df[col] pd.to_numeric(df[col], errorscoerce) df[volume] pd.to_numeric(df[volume], errorscoerce) df df.dropna().reset_index(dropTrue) if start_date: df df[df[date] pd.to_datetime(start_date)] if end_date: df df[df[date] pd.to_datetime(end_date)] return df.reset_index(dropTrue) except (requests.RequestException, json.JSONDecodeError) as e: wait_time 2 ** attempt random.uniform(0, 1) time.sleep(wait_time) if attempt self.retry_times - 1: print(f[错误] 股票 {code} 在 {attempt 1} 次尝试后仍失败: {e}) return pd.DataFrame() def save_to_csv(self, df: pd.DataFrame, file_path: str) - None: 保存数据到CSV文件 if df.empty: print(f[警告] 数据为空跳过保存: {file_path}) return df.to_csv(file_path, indexFalse, encodingutf-8-sig) print(f[成功] 已保存 {len(df)} 条数据到 {file_path}) def batch_crawl(codes: List[str], scale: int 240, datalen: int 250): 批量爬取多只股票的日线数据 crawler StockDataCrawler() for code in codes: try: df crawler.get_kline_data(code, scalescale, datalendatalen) if not df.empty: crawler.save_to_csv(df, f{code}_kline.csv) # 关键细节每次请求后随机休眠避免触发频率限制 sleep_time random.uniform(1.5, 3.5) time.sleep(sleep_time) except Exception as e: print(f[异常] 股票 {code} 处理失败: {e}) continue3.3 关键实现细节讲解这段代码里有很多细节是你看文档学不到的我一个一个说。首先是请求头伪装。新浪财经虽然不算反爬最严格的网站但你用一个默认的Python-requests请求头去访问很容易被风控系统标记为异常流量。我这里设置了User-Agent和Referer两个字段让请求看起来像是从浏览器发起的——Referer指向新浪财经首页是因为这个接口本身是给网页端用的带这个字段更自然。实测下来这样做基本不会遇到403或被拒绝的情况。其次是_normalize_symbol这段代码。我在实际使用中发现很多人拿到的股票代码是纯数字形式比如600519而接口要求带sh/sz前缀。所以我在内部做了自动补全。这个判断规则看起来简单但是要特别注意港股和新三板港股代码是5位数字新三板代码以4、8开头它们的接口地址和沪深都不一样。如果是做A股为主的项目先按上面的规则处理就够了后续要扩展再单独加分支。第三是异常重试机制。网络请求这东西谁也不能保证每次都成功。我在这里用的策略是指数退避Exponential Backoff第一次失败等大概2秒第二次失败等4秒第三次失败等8秒每次再加一个0到1秒的随机扰动。这样做的目的是避免所有失败请求在同一个时刻重试导致服务端压力瞬时暴增。对于股票接口这种公共服务来说这种礼貌的请求方式是必须的。4. 数据存储方案与增量更新策略4.1 为什么优先推荐CSV而不是数据库对于绝大多数个人用户来说股票历史数据的量级最多也就是几百只股票乘以几千个交易日总共几百万行数据。这种规模完全没必要上MySQL或者MongoDB一个CSV文件就能搞定。CSV方案有两个明显优势第一是零部署成本不需要安装维护任何数据库服务第二是通用性强pandas可以直接读取Excel也可以打开编辑后续做数据分析、接机器学习模型都方便。唯一需要注意的是一点编码问题保存CSV时一定要用utf-8-sig编码而不是utf-8否则你用Excel打开文件时会看到中文列名全部变成乱码。-sig的意思是加一个BOM头Excel靠它来识别UTF-8编码。4.2 增量更新的实现思路如果你需要长期维护一份股票数据集比如每天收盘后自动追加当天的数据那就要考虑增量更新了。最简单的实现思路是每天先读取已有的CSV文件拿到最后一条数据的日期然后爬取新数据时只请求这个日期之后的那部分数据最后把新数据追加到原文件末尾并去重。用我上面的代码增量更新的核心逻辑可以这样实现import os import pandas as pd def incremental_update(crawler: StockDataCrawler, code: str, file_path: str): 增量更新只爬取新增部分的数据 if os.path.exists(file_path): old_df pd.read_csv(file_path, parse_dates[date]) last_date old_df[date].max().strftime(%Y-%m-%d) print(f[增量更新] {code} 已有 {len(old_df)} 条数据最后日期 {last_date}) else: old_df pd.DataFrame() last_date None print(f[增量更新] {code} 无历史数据执行全量爬取) # 先拉最近250个交易日的日线数据 new_df crawler.get_kline_data(code, scale240, datalen250) if last_date and not new_df.empty: # 只保留最后日期之后的数据 new_df new_df[new_df[date] last_date] if new_df.empty: print(f[增量更新] {code} 没有新增数据) return # 合并并保存 combined_df pd.concat([old_df, new_df], ignore_indexTrue) combined_df combined_df.drop_duplicates(subsetdate).sort_values(date) crawler.save_to_csv(combined_df, file_path) print(f[增量更新] {code} 新增 {len(new_df)} 条数据总数据量 {len(combined_df)})这里有一个比较隐蔽的坑用datalen250并不保证能覆盖到你需要的所有增量区间。比如你上一次更新是半年前那么最近250个交易日的日线数据是从大约一年前开始的覆盖不了这半年的全部数据。解决办法是更新时动态计算需要的数据条数根据“今天到上次更新日期之间的间隔天数”换算成交易日大致数量再乘以1.3作为安全冗余。我个人建议如果间隔超过60个自然日直接全量重爬更省事反正新浪接口的数据量也不大全量重爬一次也就几秒钟的事情。4.3 定时任务自动更新数据爬下来了下一步自然是让它每天自动更新。这里我分享两种方式。如果你用的是Windows系统最直接的办法是写一个批处理文件然后用“任务计划程序”来定时运行。批处理内容很简单echo off cd /d D:\stock_data D:\anaconda3\python.exe update_task.py任务计划程序里设置每天下午5点A股收盘后执行触发器选每天操作里指向这个批处理文件即可。如果你是Linux服务器那就是crontab的老本行了30 17 * * 1-5 /usr/bin/python3 /home/user/stock_data/update_task.py这段的意思是周一至周五的17点30分运行更新脚本。注意crontab的时间是服务器本地时间如果你用的是云服务器默认可能是UTC时区需要自己换算或者先执行timedatectl set-timezone Asia/Shanghai把时区改过来否则你会发现任务每天都在晚上跑而不是下午跑。5. 常见问题与排查技巧实录5.1 高频踩坑问题速查表问题现象可能原因解决方法返回结果为空列表股票代码市场前缀错误检查代码是否以6开头沪或0/3开头深请求返回403 ForbiddenUser-Agent被识别为爬虫按上文代码设置浏览器UA并带上Referer中文列名在Excel中乱码CSV保存编码不是utf-8-sig改为encodingutf-8-sig日期字段是object类型未做日期解析使用pd.to_datetime()转换某些股票的成交量明显偏大返回单位是“股”而非“手”如需“手”为单位除以100即可触发新浪风控请求被限速请求频率过高每次请求后强制sleep 1.5-3.5秒5.2 字段单位问题很多人在这里栽过爬下来的价格和成交量字段单位问题必须说清楚。价格字段open、high、low、close单位是元这个没问题但成交量字段volume单位是股不是手。A股市场1手100股如果后续你做量化计算需要用到“成交量手”这个单位记得要除以100。另一个隐藏的坑是有些接口返回的成交额字段单位是元有些接口比如腾讯则是万元。我建议在写数据入库前把所有字段的单位统一做一次规范化这样后续分析时不用每次都提心吊胆去猜。我的习惯是在保存前增加一个处理步骤df[volume_lot] df[volume] / 100 # 手 df[amount] df[close] * df[volume] # 成交额约等于元5.3 反爬策略的正确打开方式关于反爬我分享一个比较务实的观点对于新浪财经这类免费数据接口反爬的目的是“防止恶意高频请求”不是“彻底封锁爬虫”。所以只要你表现得像一个正常的浏览器用户基本不会被封。我的实操经验是做到以下三点就够了第一请求频率控制在每1.5秒以上一次不要在循环里不加sleep连续猛拉第二如果数据量大程序运行中要定期打印进度日志方便你观察是否出现了异常情况并及早中断第三尽量避免在同一时间窗口内用同一IP并发爬取大量数据。如果你真的需要一次爬几千只股票建议分批执行每批之间休息个几分钟。5.4 数据库选型避坑如果你看到这里决定不用CSV而是用SQLite来做存储我也很支持。SQLite是Python自带的轻量级数据库不需要额外安装服务非常适合个人项目。这里提醒一个操作上常见的坑用pandas写入SQLite时如果数据里包含NaN值df.to_sql()默认会把它写成None重新读出来时变成NA类型和原始数据不一致容易在下游分析时报错。解决办法是在写入前显式处理空值import sqlite3 from sqlalchemy import create_engine # 先清洗空值 df_clean df.dropna(subset[date, close]) engine create_engine(sqlite:///stock_data.db) df_clean.to_sql( kline_data, engine, if_existsappend, indexFalse, dtype{ date: sqlalchemy.Date, open: sqlalchemy.Float, close: sqlalchemy.Float, }, )我个人在实际项目中一开始图省事用的CSV但后来数据量大了之后发现每次做条件查询都要全量加载到内存速度越来越慢。后来迁移到SQLite几百万条数据的条件查询基本是毫秒级体验差别非常明显。这里我的建议是在项目初期就用SQLite不要等数据积累起来之后再迁移迁移的成本远比你想象的高。6. 从源码到完整项目扩展思路与个人体会6.1 这个项目还能往哪些方向延伸代码跑通了数据也存下来了接下来能做什么我根据自己的使用经验列几个值得尝试的方向。如果你对量化分析感兴趣可以做“数据清洗技术指标计算”层。爬下来的原始数据只有OHLCV开高低收量五个字段但你在技术分析中常用的MACD、RSI、布林带等指标都可以基于这些字段用pandas和numpy自行计算。这样做的好处是你能彻底理解每个指标的计算逻辑而不是直接调库函数当黑盒使用。如果你对实时监控感兴趣可以在新浪接口的基础上改成轮询模式。把scale240改成scale5或scale1然后每隔一分钟请求一次最近数据就能实现一个简易的行情监控小工具。再接入钉钉机器人或者邮件通知就能在价格突破某个阈值时收到提醒。如果你对全市场扫描感兴趣可以做交易标的的批量数据拉取。把所有A股代码列表存成一个CSV用本文的batch_crawl函数批量拉取配合多线程优化几千只股票的日线数据大概十几分钟就能全部拉下来。这就能支撑你做全市场的风格分析、因子测试之类的事情。6.2 关于合规使用的一些提醒讲完技术必须说一下使用边界。本文实现的爬虫目标是新浪财经等公开数据接口这类接口提供的数据是公开的个人学习和研究用途下获取是没有问题的。但在实际使用中你仍然需要注意几点不要用爬取的数据做商业转售不要对数据源服务器造成过大压力不要在公开平台大批量发布从这些接口采集的实时行情数据。这些都是从业者的基本素养。另外数据本身也有时效性和准确性的问题。免费接口偶尔会出现盘中数据延迟、历史数据缺失的情况如果你做的决策对数据准确性要求极高比如实盘自动交易那么使用付费的正规数据服务才是负责任的选择。对我来说这套爬虫方案的价值更多在于“可控、可自定义、可学习”而不是替代专业金融数据终端。6.3 最后再说说我在实际使用中的体会这个项目我从最早的HTML解析方案一路改到现在的接口直连方案踩过的坑也算不少了。回头来看最值得分享的一点感受是爬虫项目的核心价值从来不在“爬”本身而在数据清洗和存储设计上。很多初学者以为爬下数据就万事大吉但真正面对残缺字段、单位不统一、日期格式混乱这些脏数据问题时才会发现前期设计数据管道的重要性。这也是为什么我在上面的代码中花了很多精力在字段规范化、类型转换和去重逻辑上。另外一个体会是好的爬虫代码一定是“礼貌”的。控制请求频率、做好异常重试、记录清晰日志这些东西看起来不起眼但在长时间运行的项目里它们决定了你的工具是能用一天还是能用一年。你可以在自己的项目里试着把这些经验一点点加进去相信我等你维护一段时间后再回看你会发现当初加的这些“不性感”的代码才是最有价值的。本文还有配套的精品资源点击获取
返回列表