ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python爬虫实战:5分钟抓取东方财富股票数据并保存为CSV

Python爬虫实战:5分钟抓取东方财富股票数据并保存为CSV 做投资分析最烦的一件事就是数据散得七零八落特别是你想快速看一眼整个市场的股票表现时手动复制粘贴能把人逼疯。今天这套 Python 爬虫方案目标很简单5分钟之内把东方财富网的股票数据抓下来整理成一张干净表格直接用 Excel 打开就能分析。它特别适合刚开始学爬虫的人、有选股需求但没有专业数据终端的个人投资者也适合想快速验证某个交易想法、不想在数据采集上耗时间的量化入门者。这套方案不搞花活核心就三步找到东方财富网行情接口、用 requests 请求 JSON 数据、再用 pandas 整理落盘。全程不需要登录、不需要 Cookie、不需要模拟浏览器代码量也很小哪怕你只写过几十行 Python 也能跑通。后面我会把接口参数、代码逻辑、常见问题、合规边界一次讲透跟着做就能复现。1. 先把方案拆开看抓取目标、数据结构和整体路径1.1 东方财富网提供了哪些值得抓的数据一说到“爬虫抓股票”很多人第一反应是去抓网页上的 HTML 表格再用 BeautifulSoup 去解析tr、td。这种思路对静态网页没问题但东方财富这种级别的财经网站页面数据多数是异步加载的你直接 GET 下来的 HTML 里往往只有框架真正的股票数据是浏览器渲染后通过接口动态填充进去的。所以我们要换一种思路直接找页面背后那个给浏览器提供数据的接口。东方财富旗下的行情中心、数据中心、个股页面背后是一整套公开的 HTTP 接口。接口返回的是结构化 JSON里面包含股票代码、名称、最新价、涨跌幅、成交量、成交额、市盈率、市净率、总市值、流通市值等常见字段。和密密麻麻的 HTML 表格相比JSON 干净多了字段名虽然是一堆 f 开头的编号但只要做一次映射就能变成一张人能看懂的 DataFrame。这个方案抓的是全市场A股列表数据也就是“某个交易瞬间所有股票长什么样”。如果你需要的是K线历史数据、财务数据、资金流向也可以在东方财富的其他接口里找到类似结构学会了这个例子举一反三并不难。1.2 5分钟方案的结构接口请求 → JSON 解析 → 表格落盘整个流程只有三个环节。第一步是构造请求URL把市场筛选条件、字段列表、排序方式、分页参数拼进去然后 GET 出去。第二步是解析返回的JSON从嵌套的数据结构里把股票列表取出来把 f12、f14 这种编码字段转成可读的中文列名。第三步是把数据放进 DataFrame按需排序、过滤最后保存成 CSV。为什么这个方案能做到“5分钟”因为它的每一步都有明确产出物请求完能打印原始 JSON 验证解析完能打印表格前几行确认字段落盘后能直接打开文件检查结果。任何一个环节出问题都能快速定位。更重要的是这套代码不吃机器性能普通笔记本跑单页请求只要几百毫秒就算全量抓几千只股票控制好频率也不过两三分钟。2. 为什么走公开接口而不是解析网页2.1 页面解析的两个坑很多爬虫新手会先尝试抓东方财富的 HTML 页面我早期也这么干过后来发现这条路有两大痛点。第一个痛点是页面结构经常变动昨天还正常的 XPath 表达式今天可能就失效了。网站的渲染逻辑、CSS 类名、DOM 层级每隔一段时间就会调整一旦调整你的解析代码就全线崩溃。第二个痛点是页面数据本身是动态加载的。你用 requests 拿到的是服务端返回的初始 HTML里面可能只有一个 loading 状态真正的股票数据是页面加载后浏览器触发额外 Ajax 请求拿到的。这就意味着你两手空空抓回来的 HTML 里根本没有你要的数据BeautifulSoup 解析得再熟练也无从下手。除非动用 Selenium、Playwright 这类浏览器自动化工具但那样一来启动浏览器开销大、运行慢和“5分钟搞定”的目标完全背道而驰。2.2 公开接口的优势与代价对比之下接口方案有几个非常现实的好处。第一响应体小、速度快不携带无关的 HTML 标签和脚本解析成本极低。第二字段命名和数据结构相对稳定虽然也是会变的但变动的频率比页面改版低得多而且一旦发现字段对不上打印一下原始 JSON 就能看出新结构。第三接口天然支持分页、排序和按条件筛选比如你想按成交额排序只需要改一个请求参数不用在本地做大量数据排序。但接口方案也有“代价”字段名不直观。你打开返回的 JSON看到的是一堆 f2、f3、f12 这种编号一开始会很不适应。解决办法很简单——建一个字段映射字典把 f12 映射成“代码”、f14 映射成“名称”二次封装之后就跟普通表格没区别了。这属于一次性成本做完之后用起来很顺手。3. 完整代码与实操步骤从请求到 CSV 一步到位3.1 环境准备Python、requests、pandas写代码之前先把环境准备好。我建议用 Python 3.8 以上版本3.10、3.11 都没问题。需要安装的第三方库只有两个直接用 pip 装pip install requests pandasrequests 负责发 HTTP 请求pandas 负责整理数据和导出 CSV。如果你还没装 Python去 Python 官网下载安装包安装时记得勾选“Add Python to PATH”不然后面在命令行里找不到 python 命令。装完测试一下命令行输入python --version能输出版本号就算成功。有人可能会问要不要装 scrapy、pyppeteer 这些重型框架不需要。这个场景是单机、低频率、单接口抓取requests 完全够用。杀鸡用牛刀反而会把简单问题复杂化。3.2 接口参数与返回结构拆解在贴完整代码之前我们先花两分钟看懂接口参数否则代码跑通了也只是“知其然不知其所以然”。请求的核心是 push2 行情接口关键参数如下表参数作用我们这里怎么传pn页码从 1 开始pz每页条数单页示例传 20全量抓取传 200po排序方向1 表示降序np接口约定参数固定传 1fltt返回数字格式2 表示转成 float 数字fid排序字段f3 表示按涨跌幅排序fs市场筛选沪深A股fields需要返回的字段逗号分隔的 f 编码列表fs 参数是很多人容易卡住的地方它就是告诉接口“我要哪些市场的股票”。我示例里用的是m:1t:2,m:0t:6含义是“沪市A股 深市A股”。如果你后面想加创业板、科创板需要在现有基础上追加对应的市场代码不同板块的 t 值不一样网上搜“东方财富 fs 参数”能找到整理好的对照表。fields 参数决定了接口返回哪些列。我常用的字段集合包含 f2最新价、f3涨跌幅、f4涨跌额、f5成交量、f6成交额、f7振幅、f8换手率、f9市盈率、f10量比、f12代码、f14名称、f15最高、f16最低、f17今开、f18昨收、f20总市值、f21流通市值、f23市净率。一次接口调用就能拿全这些基础指标足够做很多分析。3.3 核心代码单页抓取 全量翻页下面这份代码可以直接复制运行。为了照顾不同需求我把两部分写在一起先演示单页抓取再演示全量翻页。默认只用单页避免新手一上来就把频率冲太高。import time import requests import pandas as pd COLUMN_MAP { f2: 最新价, f3: 涨跌幅, f4: 涨跌额, f5: 成交量, f6: 成交额, f7: 振幅, f8: 换手率, f9: 市盈率, f10: 量比, f12: 代码, f14: 名称, f15: 最高, f16: 最低, f17: 今开, f18: 昨收, f20: 总市值, f21: 流通市值, f23: 市净率, } def fetch_page(page_no1, page_size20): url https://push2.eastmoney.com/api/qt/clist/get params { pn: page_no, pz: page_size, po: 1, np: 1, fltt: 2, invt: 2, fid: f3, fs: m:1t:2,m:0t:6, fields: ,.join(COLUMN_MAP.keys()), } headers { 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://quote.eastmoney.com/, } resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() return resp.json() def parse_stock_list(data): diff data.get(data, {}).get(diff, []) if not diff: return pd.DataFrame() df pd.DataFrame(diff) df df.rename(columnsCOLUMN_MAP) return df def fetch_all_stocks(): page_size 200 first_page fetch_page(1, page_size) total first_page.get(data, {}).get(total, 0) total_pages (total page_size - 1) // page_size all_df parse_stock_list(first_page) for page_no in range(2, total_pages 1): data fetch_page(page_no, page_size) df parse_stock_list(data) if df.empty: break all_df pd.concat([all_df, df], ignore_indexTrue) time.sleep(0.3) return all_df if __name__ __main__: df fetch_all_stocks() df df[list(COLUMN_MAP.values())] print(df.head(10)) print(f共获取 {len(df)} 条记录) filename fstock_{pd.Timestamp.now():%Y%m%d_%H%M%S}.csv df.to_csv(filename, indexFalse, encodingutf-8-sig) print(f已保存到 {filename})代码逻辑不复杂。fetch_page 负责发请求parse_stock_list 把接口返回的 JSON 转成 DataFrame并完成字段中文映射fetch_all_stocks 先拿第一页读出 total 总数再根据总数计算总页数逐页抓取并合并。每次翻页之间 sleep 0.3 秒这是一个非常关键的保护措施能显著降低被限流的概率。pandas 的 concat 在这里的作用是把每一页的 DataFrame 拼成一个大表。concat 之前建议把 ignore_index 设置为 True不然每页的索引都会从 0 开始最终表格里会出现大量重复索引虽然不影响数据分析但看着难受。3.4 运行结果与落地验证跑完脚本之后我会习惯先看两样东西一是 print 出来的前10行确认列名是不是预期的中文二是生成的 CSV 文件能不能用 Excel 正常打开。代码里保存 CSV 用了encodingutf-8-sig这是专门针对中文环境的小技巧。如果你用默认的utf-8编码保存再用 Excel 直接双击打开中文列名和股票名称大概率会乱码成“中国”之类的。加上-sig这个后缀会在文件头部写入一个 BOM 标记Excel 就能正确识别 UTF-8 编码。这种行为在某些场景下会被吐槽“不够标准”但在国内处理 Excel 兼容性时非常实用。这个方案抓下来的数据是快照性质的也就是“你请求那一刻的市场状态”。股票价格是实时波动的同一只股票在不同时间运行脚本结果会不一样这不算 bug是业务场景决定的。如果你需要做历史对比建议在文件名里加入时间戳每次抓取都单独存一份后面回测时才有据可查。4. 高频访问、字段漂移、接口变化的踩坑实录4.1 请求被拒与频控策略我第一次写这个脚本的时候没有加任何延时for 循环以最快的速度连续翻页大概抓到第5页就收到了HTTP 403。403 表示服务器拒绝了这次请求最常见的触发原因是请求频率过高触发了站点的风控策略。解决办法很简单在每次请求之间增加 sleep 延时。我实测下来0.3 到 0.5 秒是个人使用场景下比较稳妥的间隔既不会等太久也能降低触发风控的概率。如果你只是抓一两页做学习间隔可以更短如果抓全量几千条建议至少保留 0.5 秒。另一种容易被忽略的情况是同一 IP 短时间内的请求次数被限流不是带什么特殊参数能解决的最直接的办法是“少抓、慢抓”。如果你把抓取频率降下来之后仍然频繁被拒可以尝试用 requests 的 Session 对象复用连接并在 headers 里加一个Accept: */*有时候能改善但这不是银弹。4.2 JSON 解析失败和 JSONP 干扰有读者反映requests 请求之后返回的内容不是 JSON而是一段像jQuery...(...)的 JavaScript 代码然后用resp.json()解析直接报错。这种情况多半是因为请求 URL 中带了cb参数比如cbjQuery1124...服务器会返回 JSONP 格式而不是纯 JSON。解决办法是在构造参数时不要包含任何cb参数。如果怀疑代码其他地方拼上了这个参数打印一下resp.text[:200]看看开头是不是(或jQuery确认之后把 cb 去掉即可。另外解析之前可以先检查 HTTP 状态码和resp.url有时候 302 跳转也会让最终 URL 和预期不一致。4.3 字段对不上的排查方法接口偶尔会调整字段比如把某些板块的字段换成新的编码或者字段值从数字变成-、空字符串。这种问题在代码跑通很久之后才出现最典型的现象是 DataFrame 里某一列全是 NaN。我的排查习惯是三步走先用浏览器直接访问接口 URL把返回的 JSON 原样打印出来然后对比实际返回的键名和代码里的字段映射表找出消失或新增的字段最后更新 COLUMN_MAP再跑一次。这个方法比盲目调代码快得多因为接口返回永远是最好的“真相”。4.4 常见问题速查表现象可能原因处理方法返回 403请求频率过高被风控增加 sleep 间隔降速重试JSON 解析报错URL 带了 cb 参数去掉 cb检查最终请求 URL某些列为空/NaN接口字段调整或停用打印原始 JSON更新字段映射中文乱码CSV 编码不是 utf-8-sig保存时指定 encodingutf-8-sig抓取数据只有一页fs 参数未覆盖目标板块按需增加市场筛选代码连接超时网络波动或 IP 被临时限制增加 timeout 参数稍后重试这张表可以先收藏以后遇到相似问题直接对照能省下不少排查时间。5. 写爬虫之前必须想清楚的事合规与边界5.1 合规操作的三个基本前提这个方案用的是东方财富公开的 HTTP 接口不涉及登录态、不绕过任何验证码、不破解任何加密参数但它依然要遵守一定的边界。我的经验是做爬虫至少守住三个底线第一只抓公开数据不碰需要账号权限才能访问的内容第二控制频率不做高并发请求不影响目标站点的正常使用第三抓下来的数据仅用于个人学习、研究不批量转售不对外提供收费的数据服务。很多人会担心“会不会因为爬虫出事”。我的态度是技术和工具本身是中性的关键看你拿它干什么。合规的数据抓取行为是常见的自动化和研究手段但非法获得权限、恶意采集公民个人信息、大规模攻击性抓取这些性质完全不同碰都不要碰。5.2 数据版权与后续用途股票行情数据的源头是交易所东方财富网是数据的加工和展示方。行情数据本身有一定版权归属个人用来自选股研究没有问题但如果要做成商业产品对外发布数据服务就需要评估相关授权。这不是耸人听闻而是每个数据从业者都应该有的基本意识。合规使用还有一个非常实际的角度的考虑你的目标和目标网站之间不应该是“猫鼠关系”。如果把接口请求频率压得很低脚本就是温和的自动化访问而不是攻击性爬虫。温和的脚本更稳定跑得更久对双方都友好。我在写任何爬虫之前都会先想清楚这个脚本如果被对方运维看到我会不会觉得心虚如果会就一定是我越界了。6. 进阶玩法把一次性脚本升级成数据服务6.1 定时刷新Windows 计划任务 / Linux crontab脚本跑通一次只是开始我更推荐把它变成一个定时任务每天收盘后自动抓一轮数据长期累积下来就是自己的小型行情库。在 Windows 上可以用“任务计划程序”指定每天特定时间执行 Python 脚本在 Linux 上更简单直接写 crontab 配置即可。# 每天下午 15:30 执行抓取脚本 30 15 * * 1-5 cd /path/to/project /usr/bin/python3 fetch_stock.py stock.log 21要注意的是crontab 和 Windows 计划任务里执行的 Python 必须是绝对路径日志输出也别省重定向到文件里方便以后排查。第一次跑定时任务前先手动执行一次确认路径无误不然很容易出现“任务提示成功但没产出数据”的情况。6.2 增量更新与数据入库逐日抓取会产生大量 CSV 文件时间一长就不方便查了。我的做法是引入 SQLite 做轻量存储每次抓取都往同一张表里插入数据用股票代码加日期做联合主键重复跑同一天的任务不会产生脏数据。SQLite 不需要额外装数据库服务Python 标准库自带sqlite3上手成本极低。导入历史 CSV 数据时先按代码和日期去重再批量 INSERT OR REPLACE后续做筛选、统计、回测都比打开一堆 CSV 文件高效得多。6.3 结合可视化与量化分析有了稳定的数据流水线后面能玩的花样就多了。比如用 pyecharts 画全市场涨跌分布直方图一眼看出当天市场情绪或者把每日涨幅排行榜导出来跟踪一段时间观察市场热点轮动。数据量积累到一两个月后你甚至可以自己验证那些“连板股”“高换手率股”的策略是否真的有效。我个人比较推荐从简单的策略验证入手用几天数据算完平均值和标准差再用最近一天的真实数据做对照看看哪些条件能选出第二天表现更好的股票。这个过程中你会发现数据采集只是最底层的环节真正的价值在清洗、加工和分析把这些想清楚后这个5分钟脚本的意义就远远不止“抓表”本身了。最后说一个我自己的习惯每次写这类爬虫之前我都会先用浏览器手动访问一次接口 URL把返回的 JSON 原样存下来再开始写解析代码。别嫌这一步麻烦它能在后面帮你省下至少两个小时的排错时间。无论是字段名变了还是返回结构改了有一份当时的原始样本在手排查思路会清晰很多。做爬虫这件事很多时候不是比谁的工具更炫而是比谁更细心、更克制。希望这套方案能让你少走点弯路踏踏实实把数据拿到手。
返回列表