东方财富行情数据采集与解析实战
本文介绍如何从东方财富网行情中心获取多板块股票数据,并导出为 Excel 文件。目标页面为沪深京等各大板块的股票列表,数据通过动态接口加载,需借助浏览器开发者工具进行抓包分析。
接口分析与请求构造
通过浏览器开发者工具(F12)观察网络请求,可发现数据由以下接口返回:
https://push2.eastmoney.com/api/qt/clist/get?cb=回调函数&pn=页码&pz=20&po=1&np=1&ut=...&fid=筛选条件&fs=板块代码&fields=字段列表
关键参数说明:
pn:页码,从 1 开始递增pz:每页条数,固定 20fid与fs:组合用于指定板块fields:返回字段,如 f12(代码)、f14(名称)、f2(最新价)等
JSONP 响应处理
接口返回 JSONP 格式,需提取有效 JSON 内容。以下采用字符串切片方式处理,避免正则转义问题:
import requests
def parse_jsonp(resp_text):
# 定位第一个左括号位置
start = resp_text.find('(')
# 定位末尾右括号位置
end = resp_text.rfind(')')
if start == -1 or end == -1:
raise ValueError("非法的 JSONP 格式")
# 提取括号内纯 JSON 字符串
json_str = resp_text[start + 1:end]
# 处理 null 字面量
json_str = json_str.replace('null', 'None')
return eval(json_str)
板块配置与字段映射
定义板块参数映射表,便于批量采集:
BOARD_CONFIG = {
"沪深京A股": {"fid": "f3", "fs": "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048"},
"上证A股": {"fid": "f3", "fs": "m:1+t:2,m:1+t:23"},
"深证A股": {"fid": "f3", "fs": "m:0+t:6,m:0+t:80"},
"北证A股": {"fid": "f3", "fs": "m:0+t:81+s:2048"},
"新股": {"fid": "f26", "fs": "m:0+f:8,m:1+f:8"},
"创业板": {"fid": "f3", "fs": "m:0+t:80"},
"科创板": {"fid": "f3", "fs": "m:1+t:23"},
"沪股通": {"fid": "f26", "fs": "b:BK0707"},
"深股通": {"fid": "f26", "fs": "b:BK0804"},
"B股": {"fid": "f3", "fs": "m:0+t:7,m:1+t:3"},
"风险警示板": {"fid": "f3", "fs": "m:0+f:4,m:1+f:4"},
}
字段与中文含义对照:
FIELD_MAP = {
"f12": "代码",
"f14": "名称",
"f2": "最新价",
"f3": "涨跌幅",
"f4": "涨跌额",
"f5": "成交量",
"f6": "成交额",
"f7": "振幅",
"f15": "最高",
"f16": "最低",
"f17": "今开",
"f18": "昨收",
"f10": "量比",
"f8": "换手率",
"f9": "市盈率",
"f23": "市净率",
}
完整采集程序
import requests
import pandas as pd
from time import sleep
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://quote.eastmoney.com/",
}
def fetch_board_data(board_name, config):
all_records = []
page_idx = 1
while True:
# 构造请求 URL
url = (
f"https://7.push2.eastmoney.com/api/qt/clist/get?"
f"cb=jQuery_{board_name}_{page_idx}&pn={page_idx}&pz=20&po=1&np=1&"
f"ut=bd1d9ddb04089700cf9c27f6f7426281&fltt=2&invt=2&"
f"wbp2u=|0|0|0|web&fid={config['fid']}&fs={config['fs']}&"
f"fields=f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f12,f13,f14,f15,f16,f17,f18,"
f"f20,f21,f23,f24,f25,f22,f11,f62,f128,f136,f115,f152"
)
try:
rsp = requests.get(url, headers=HEADERS, timeout=15)
rsp.raise_for_status()
except requests.RequestException as e:
print(f"{board_name} 第 {page_idx} 页请求异常: {e}")
break
# 解析 JSONP
payload = parse_jsonp(rsp.text)
# 判定终止条件:data 为空或不存在
if not payload.get('data') or not payload['data'].get('diff'):
print(f"{board_name} 采集完毕,共 {page_idx - 1} 页")
break
# 提取数据
for item in payload['data']['diff']:
row = {v: item.get(k) for k, v in FIELD_MAP.items()}
all_records.append(row)
print(f"{board_name} 第 {page_idx} 页已采集")
page_idx += 1
sleep(0.3) # 控制请求频率
return all_records
def main():
for name, cfg in BOARD_CONFIG.items():
dataset = fetch_board_data(name, cfg)
if not dataset:
continue
frame = pd.DataFrame(dataset)
filename = f"行情_{name}.xlsx"
frame.to_excel(filename, index=False, engine='openpyxl')
print(f"已保存: {filename},共 {len(dataset)} 条")
if __name__ == "__main__":
main()
关键注意点
- 终止判定:当接口返回的
data字段为null或diff为空列表时,表示该板块已遍历完毕 - 频率控制:建议添加延时(如 300ms),避免触发服务端限流
- 字段缺失:部分股票某些字段可能为空,
dict.get()可安全处理 - 动态节点:push2 子域名存在多个(如 62、7 等),若请求失败可尝试切换
