作为一名长期从事金融数据分析的老手,我深知获取全市场股票数据时面临的效率瓶颈。以A股市场为例,目前上市公司数量已突破5000家,传统单线程获取方式存在诸多问题:
python复制# 典型单线程获取代码示例
for ts_code in stock_list:
df = client.get_daily_data(ts_code, '20230101', '20231231')
time.sleep(0.1) # 遵守API频率限制
这种模式存在四个致命问题:
实战经验:在实际操作中,单线程获取全市场10年日线数据可能耗时超过6小时,且失败率高达15%
主流金融数据API(如Tushare)通常设有严格的频率限制:
| 限制类型 | 典型值 | 应对方案 |
|---|---|---|
| 每秒请求数 | 5-10次 | 分布式延迟 |
| 每分钟请求数 | 200-300次 | 队列控制 |
| 日请求总量 | 1万-10万次 | 分批次执行 |
经过多年实践,我总结出不同并发方案的适用场景:
| 方案 | 优点 | 缺点 | 适用场景 | IO密集型评分 | CPU密集型评分 |
|---|---|---|---|---|---|
| 多线程 | 实现简单 | GIL限制 | IO密集型 | ★★★★☆ | ★★☆☆☆ |
| 多进程 | 突破GIL | 内存消耗大 | CPU密集型 | ★★★☆☆ | ★★★★☆ |
| 协程 | 超高并发 | 代码复杂度高 | 高IO场景 | ★★★★★ | ★☆☆☆☆ |
| 进程池 | 资源可控 | 灵活性一般 | 中等规模 | ★★★☆☆ | ★★★☆☆ |
基于实际测试数据,我推荐以下混合方案:
mermaid复制graph TD
A[主进程] --> B[进程池]
B --> C[线程池]
C --> D[API请求]
这种架构的优势在于:
python复制class BatchFetcher:
"""带频率控制的批量获取器"""
def __init__(self, token=None, max_workers=5):
"""
初始化参数说明:
- max_workers: 根据API限制动态调整
测试服务器建议5
生产环境可升至8-10
"""
self._rate_limiter = RateLimiter(
max_calls=8,
period=1.0 # 每秒最多8次请求
)
self._client = TushareProClient(token)
def _fetch_with_retry(self, ts_code, retries=3):
"""带重试机制的请求封装"""
for attempt in range(retries):
try:
with self._rate_limiter:
return self._client.daily(ts_code)
except Exception as e:
if attempt == retries - 1:
raise
time.sleep(2 ** attempt) # 指数退避
python复制def run_batch(fetcher, code_list, checkpoint_file='progress.json'):
# 加载进度
try:
with open(checkpoint_file) as f:
done_codes = set(json.load(f))
except FileNotFoundError:
done_codes = set()
# 过滤已完成的代码
todo_codes = [c for c in code_list if c not in done_codes]
# 执行任务
with ThreadPoolExecutor() as executor:
futures = {
executor.submit(fetcher, code): code
for code in todo_codes
}
for future in as_completed(futures):
code = futures[future]
try:
result = future.result()
done_codes.add(code)
# 实时保存进度
with open(checkpoint_file, 'w') as f:
json.dump(list(done_codes), f)
except Exception as e:
logger.error(f"Failed {code}: {str(e)}")
以下是在不同并发配置下的实测性能对比(获取500支股票1年日线数据):
| 并发数 | 耗时(s) | 成功率 | CPU利用率 |
|---|---|---|---|
| 1 (单线程) | 312 | 98% | 12% |
| 5 | 89 | 97% | 65% |
| 10 | 52 | 95% | 92% |
| 20 | 48 | 88% | 100% |
关键发现:超过10并发后成功率明显下降,说明触发了API限流
对于超大规模数据获取(如全市场10年分钟级数据),建议采用更专业的解决方案:
python复制# tasks.py
@app.task(bind=True, max_retries=3)
def fetch_stock_data(self, ts_code):
try:
data = tushare.get_k_data(ts_code)
save_to_database(data)
except Exception as exc:
raise self.retry(exc=exc)
配置建议:
python复制# 动态任务分配算法
def get_next_batch():
pending = get_pending_count()
workers = get_active_workers()
batch_size = min(
max(50, pending // workers),
200 # 单批次上限
)
return query_unfinished(batch_size)
| 异常类型 | 发生频率 | 处理方案 |
|---|---|---|
| 网络超时 | 15% | 指数退避重试 |
| API限流 | 8% | 动态降速 |
| 数据缺失 | 5% | 记录并跳过 |
| 认证失效 | 0.1% | 终止并报警 |
python复制from prometheus_client import Counter, Gauge
REQUESTS_TOTAL = Counter('api_requests_total', 'Total API requests')
FAILED_REQUESTS = Counter('api_failures_total', 'Failed requests')
REQUEST_DURATION = Gauge('api_duration_seconds', 'Request latency')
@REQUEST_DURATION.time()
def make_request():
try:
result = api_call()
REQUESTS_TOTAL.inc()
return result
except:
FAILED_REQUESTS.inc()
raise
经过数十次全市场数据获取实践,我总结出以下黄金法则:
并发数公式:
code复制最佳并发数 = min(API每秒限制 × 0.8, CPU核心数 × 2)
内存优化技巧:
稳定性提升方案:
效率极限突破:
python复制# 使用asyncio+aiohttp实现超高并发
async def fetch_all(session, codes):
tasks = [
fetch_one(session, code)
for code in codes
]
return await asyncio.gather(*tasks)
最后分享一个实用技巧:在长时间运行的批量任务中,可以定期执行gc.collect()手动触发垃圾回收,特别是在处理大量临时DataFrame时,这能有效降低内存占用约30%。