实战解析:东方财富网股票数据爬取与多板块自动化抓取策略
1. 东方财富网数据接口分析实战
第一次接触东方财富网的数据抓取时,我也被它复杂的接口参数搞得一头雾水。但经过反复测试,发现其实只要掌握几个关键点,就能轻松获取到想要的股票数据。先来看看这个典型的接口URL:
url = "https://62.push2.eastmoney.com/api/qt/clist/get?cb=jQuery1124007675389012158473_1703949729655&pn=1&pz=20&po=1&np=1&ut=bd1d9ddb04089700cf9c27f6f7426281&fltt=2&invt=2&wbp2u=|0|0|0|web&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&fields=f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f12,f13,f14,f15,f16,f17,f18,f20,f21,f23,f24,f25,f22,f11,f62,f128,f136,f115,f152&_=1703949729656"这个URL虽然看起来很长,但真正需要关注的参数其实就几个:
- pn:当前页码
- pz:每页数据量(默认20条)
- fid:接口类型标识
- fs:板块筛选条件(最重要)
- fields:需要返回的字段列表
实测发现,修改pn参数就能实现翻页,而fs参数决定了获取哪个板块的数据。比如把fs值改为"m:1+t:2"就只获取上证A股数据。这里有个小技巧:直接在浏览器地址栏修改参数测试,比反复运行脚本更高效。
2. JSONP数据处理技巧
东方财富的接口返回的是JSONP格式数据,常规的json解析方法会直接报错。我最初尝试用字符串截取处理,后来发现用正则表达式更可靠:
import re def parse_jsonp(jsonp_str): try: # 匹配JSONP前缀和后缀 json_str = re.sub(r'^.*?\(', '', jsonp_str) json_str = re.sub(r'\);$', '', json_str) return eval(json_str) except Exception as e: print(f"解析JSONP失败: {e}") return None这里有几个坑需要注意:
- 正则表达式中的括号需要转义,否则会被识别为捕获组
- 返回数据中可能包含JavaScript变量null,需要提前声明null = None
- 不同接口的JSONP回调函数名可能变化,不要写死匹配规则
我建议封装一个通用的JSONP解析函数,在项目里复用。实际测试中,这个方法对东方财富所有接口都有效。
3. 多板块爬取策略
要实现全自动抓取所有板块数据,关键在于构建板块配置字典。经过分析,我整理出了完整的板块映射关系:
板块配置 = { "沪深京A股": "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股": "f3&fs=m:1+t:2,m:1+t:23", "深证A股": "f3&fs=m:0+t:6,m:0+t:80", "创业板": "f3&fs=m:0+t:80", "科创板": "f3&fs=m:1+t:23", # 其他板块... }爬取循环的逻辑要点:
- 外层循环遍历板块配置字典
- 内层while循环处理分页
- 当接口返回data为null时终止当前板块爬取
for 板块名称, 板块参数 in 板块配置.items(): 页码 = 1 while True: 数据 = 获取数据(板块参数, 页码) if 数据["data"] is None: break # 处理数据... 页码 += 1实测发现,有些板块的页数会动态变化,所以不能依赖固定的页数判断,必须根据接口返回的data字段是否为null来判断终止条件。
4. 异常处理与优化建议
在实际爬取过程中,会遇到各种意外情况。根据我的经验,这几个地方最容易出问题:
网络请求方面:
- 添加合理的超时设置(建议请求超时10秒,读取超时30秒)
- 实现自动重试机制(3次重试,每次间隔递增)
- 使用随机User-Agent避免被封
请求头 = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "application/json" } def 获取数据(url, 最大重试=3): for 尝试 in range(最大重试): try: resp = requests.get(url, headers=请求头, timeout=(10, 30)) return resp.text except Exception as e: if 尝试 == 最大重试 - 1: raise time.sleep(尝试 * 2) # 指数退避数据存储优化:
- 使用Pandas直接保存为Excel会占用大量内存,建议:
- 每100页保存一次临时文件
- 最后合并所有临时文件
- 添加爬取进度日志,方便断点续爬
- 对关键字段进行数据校验(如股价不能为字符串)
反爬应对策略:
- 控制请求频率(建议每请求间隔1-2秒)
- 使用代理IP池(如果需要大规模抓取)
- 模拟浏览器行为(如携带Cookies)
5. 完整代码实现
结合上述所有要点,这是优化后的完整实现:
import requests import re import pandas as pd import time from tqdm import tqdm class EastmoneySpider: def __init__(self): self.headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://quote.eastmoney.com/" } self.null = None # 处理接口返回的null值 def parse_jsonp(self, jsonp_str): try: json_str = re.sub(r'^.*?\(', '', jsonp_str) json_str = re.sub(r'\);$', '', json_str) return eval(json_str) except Exception as e: print(f"JSONP解析失败: {e}") return None def get_stock_data(self, cmd, page, retry=3): url = f"https://push2.eastmoney.com/api/qt/clist/get?pn={page}&pz=20&fid={cmd}" for i in range(retry): try: resp = requests.get(url, headers=self.headers, timeout=(10, 30)) data = self.parse_jsonp(resp.text) return data except Exception as e: if i == retry - 1: raise time.sleep(i * 2) def crawl_all_boards(self): 板块配置 = { "沪深京A股": "f3&fs=m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048", # 其他板块配置... } for 板块名称, 板块参数 in 板块配置.items(): print(f"开始爬取板块: {板块名称}") 所有数据 = [] 页码 = 1 with tqdm() as pbar: while True: try: 数据 = self.get_stock_data(板块参数, 页码) if 数据.get("data") is None: break for item in 数据["data"]["diff"]: 格式化数据 = { "代码": item.get("f12"), "名称": item.get("f14"), # 其他字段... } 所有数据.append(格式化数据) pbar.update(1) time.sleep(1) # 礼貌性延迟 页码 += 1 except Exception as e: print(f"爬取失败: {e}") break # 保存数据 pd.DataFrame(所有数据).to_excel(f"{板块名称}.xlsx", index=False) print(f"板块 {板块名称} 爬取完成,共 {len(所有数据)} 条数据") if __name__ == "__main__": spider = EastmoneySpider() spider.crawl_all_boards()这个实现加入了进度条显示、异常处理和代码注释,实际使用时可以根据需要调整板块配置和字段映射。建议首次运行时先测试单个板块,确认无误后再开启全量爬取。
