数据采集基础:用requests库抓取金融API数据
金融数据采集,说白了就是跟各种API打交道。我做了这么多年数据工程,发现很多新手一上来就想着搞什么分布式爬虫,其实连最基础的requests库都没用好。今天咱们就把这块地基打扎实。
3.1 为什么选requests库?
Python里做HTTP请求的库不少,urllib、httpx、aiohttp都有。但我个人习惯用requests,原因很简单——它把复杂的东西藏起来了。你想想看,金融数据接口返回的要么是JSON,要么是CSV,requests处理这两种格式就像切豆腐一样顺手。
我记得刚入行那会儿,有个同事用urllib写了个数据采集脚本,光处理请求头就写了三十行。我用requests重构后,代码量直接砍掉三分之二。嗯,这就是工具选对的好处。
3.2 基础用法:GET请求与参数
先看一个最典型的场景——从股票API获取实时行情。大多数金融API都支持通过GET请求传递参数,比如股票代码、时间范围这些。
import requests
# 设置请求参数
params = {
'symbol': '000001.SZ',
'fields': 'open,high,low,close,volume',
'start_date': '2024-01-01',
'end_date': '2024-01-31'
}
# 发送GET请求
response = requests.get(
'https://api.example.com/stock/daily',
params=params,
timeout=10 # 设置超时,这个很重要
)
# 检查状态码
if response.status_code == 200:
data = response.json()
print(f"获取到 {len(data['records'])} 条记录")
else:
print(f"请求失败,状态码: {response.status_code}")
小技巧:我个人习惯把API的base_url和参数分开定义,这样后期维护时改起来方便。你想想看,如果哪天接口地址变了,你只需要改一个变量就行。
3.3 处理JSON格式数据
金融API返回的数据,90%以上都是JSON格式。处理JSON其实就三步:解析、提取、转换。但这里有个坑——很多新手拿到数据就直接用,完全不检查结构。
我曾经在项目中遇到过这么个事:某家券商的API文档写的是返回字段叫"close_price",结果实际返回的是"closing_price"。如果代码里写死了字段名,那数据就全丢了。所以我的做法是——先打印出来看看。
# 安全解析JSON
try:
json_data = response.json()
# 先打印结构,确认字段名
print("返回数据结构:", json_data.keys())
# 提取核心数据
records = json_data.get('data', {}).get('records', [])
# 转换为结构化数据
stock_data = []
for record in records:
stock_data.append({
'date': record['trade_date'],
'open': float(record['open_price']),
'high': float(record['high_price']),
'low': float(record['low_price']),
'close': float(record['close_price']),
'volume': int(record['volume'])
})
except ValueError as e:
print(f"JSON解析失败: {e}")
except KeyError as e:
print(f"字段缺失: {e}")
注意:千万别直接用response.json()而不做异常处理。我见过太多线上事故就是因为API返回了错误信息,结果json()直接抛异常,整个采集流程就断了。
3.4 处理CSV格式数据
有些金融数据接口会返回CSV格式,比如一些历史行情数据。处理CSV我推荐用pandas,但如果你不想引入那么重的库,用csv模块也够用。
import csv
from io import StringIO
# 假设response.text是CSV格式的字符串
csv_content = response.text
# 用StringIO包装成文件对象
csv_file = StringIO(csv_content)
reader = csv.DictReader(csv_file)
# 逐行读取
rows = []
for row in reader:
rows.append({
'date': row['日期'],
'open': float(row['开盘价']),
'close': float(row['收盘价']),
'volume': int(row['成交量'])
})
print(f"共读取 {len(rows)} 行CSV数据")
避坑指南:我曾经遇到过CSV文件里混入了BOM头(就是文件开头那几个看不见的字符),导致第一列字段名前面多了个"\ufeff"。解决办法很简单——读取前用.lstrip('\ufeff')处理一下。
3.5 异常处理与重试机制
做金融数据采集,最怕的就是网络波动。API偶尔超时、返回500错误、甚至直接断连,这些都是家常便饭。我的原则是——能自动恢复的绝不手动干预。
这里分享一个我用了好几年的重试模板:
import time
from requests.exceptions import RequestException
def fetch_with_retry(url, params=None, max_retries=3, backoff_factor=2):
"""
带重试机制的数据采集函数
参数:
url: API地址
params: 请求参数
max_retries: 最大重试次数
backoff_factor: 退避因子(每次重试等待时间翻倍)
"""
for attempt in range(max_retries):
try:
response = requests.get(
url,
params=params,
timeout=10,
headers={'User-Agent': 'Mozilla/5.0'}
)
# 处理HTTP错误状态码
if response.status_code == 429:
# 请求太频繁,等一会儿再试
wait_time = 2 ** attempt
print(f"触发限流,等待 {wait_time} 秒...")
time.sleep(wait_time)
continue
elif response.status_code == 503:
# 服务暂时不可用
wait_time = backoff_factor ** attempt
print(f"服务不可用,{wait_time}秒后重试...")
time.sleep(wait_time)
continue
elif response.status_code != 200:
print(f"HTTP错误: {response.status_code}")
continue
# 成功返回数据
return response.json()
except RequestException as e:
print(f"第 {attempt + 1} 次请求失败: {e}")
if attempt < max_retries - 1:
wait_time = backoff_factor ** attempt
print(f"等待 {wait_time} 秒后重试...")
time.sleep(wait_time)
else:
print("重试次数用尽,放弃请求")
return None
return None
# 使用示例
data = fetch_with_retry(
'https://api.example.com/stock/daily',
params={'symbol': '000001.SZ'}
)
if data:
print("数据采集成功")
else:
print("数据采集失败,请检查网络或API状态")
核心要点:
- 重试次数一般设3次就够了,太多反而会加重服务器负担
- 退避策略用指数退避,第一次等2秒,第二次等4秒,第三次等8秒
- 一定要区分不同类型的错误——限流(429)和服务不可用(503)的处理方式不同
3.6 实战:完整的金融数据采集流程
把上面这些知识点串起来,就是一个完整的采集流程。我一般会把它封装成一个类,方便复用:
class FinancialDataCollector:
"""金融数据采集器"""
def __init__(self, base_url, api_key=None):
self.base_url = base_url
self.session = requests.Session()
if api_key:
self.session.headers.update({'X-API-Key': api_key})
def collect_stock_data(self, symbol, start_date, end_date):
"""采集股票日线数据"""
params = {
'symbol': symbol,
'start_date': start_date,
'end_date': end_date,
'format': 'json'
}
data = fetch_with_retry(
f"{self.base_url}/stock/daily",
params=params
)
if data:
return self._parse_stock_data(data)
return []
def _parse_stock_data(self, raw_data):
"""解析原始数据"""
records = raw_data.get('data', [])
parsed = []
for record in records:
try:
parsed.append({
'symbol': record['symbol'],
'date': record['trade_date'],
'open': float(record['open']),
'high': float(record['high']),
'low': float(record['low']),
'close': float(record['close']),
'volume': int(record['volume'])
})
except (KeyError, ValueError, TypeError) as e:
print(f"解析记录失败: {e}, 原始数据: {record}")
continue
return parsed
# 使用示例
collector = FinancialDataCollector(
base_url='https://api.example.com',
api_key='your_api_key_here'
)
result = collector.collect_stock_data(
symbol='000001.SZ',
start_date='2024-01-01',
end_date='2024-01-31'
)
print(f"成功采集 {len(result)} 条数据")
3.7 本章知识体系
下面这张图把本章的核心内容串起来了,你可以对照着回顾一下:
我的建议:刚开始做数据采集时,别想着一步到位。先把GET请求跑通,再慢慢加上JSON解析、异常处理、重试机制。每加一个功能就测试一次,这样出了问题能快速定位。
好了,这一章的内容就到这里。记住一句话——数据采集是ETL流程的入口,入口不稳,后面全是白搭。把requests库用熟练了,后面的路就好走了。
公众号:蓝海数据掘金营,微信deep3321