第一次用Python从Baostock下载A股数据存到MySQL时,我按照网上的教程很快跑通了代码。但真正开始分析数据时,才发现各种隐藏的问题:数据类型不匹配导致计算错误、MySQL 8.0认证失败、API调用频率超限被封禁、字段含义混淆导致分析结果偏差...这些问题让我的量化分析项目整整耽误了两周。本文将分享这些"坑"的解决方案,帮助新手快速搭建稳定可靠的数据采集系统。
很多新手在连接MySQL 8.0时都会遇到caching_sha2_password认证错误。网上的解决方案通常是修改MySQL的认证方式,但这存在安全隐患。更安全的做法是在代码中正确处理认证插件。
python复制import mysql.connector
def create_secure_connection():
config = {
'host': 'localhost',
'user': 'quant_user',
'password': 'complex_password_123',
'auth_plugin': 'mysql_native_password',
'ssl_disabled': False # 强制启用SSL加密
}
try:
connection = mysql.connector.connect(**config)
print("安全连接建立成功")
return connection
except Exception as e:
print(f"连接失败: {str(e)}")
return None
关键点:
mysql_native_password插件兼容旧版认证注意:生产环境中应将数据库密码存储在环境变量或专业密钥管理服务中,切勿直接写在代码里。
原始教程中的表结构设计存在几个明显问题:日期字段作为主键可能导致冲突、DECIMAL精度设置不合理、缺少索引影响查询性能。优化后的表结构应该这样设计:
sql复制CREATE TABLE `stock_daily` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`date` DATE NOT NULL,
`code` VARCHAR(10) NOT NULL,
`open` DECIMAL(12,4) UNSIGNED,
`high` DECIMAL(12,4) UNSIGNED,
`low` DECIMAL(12,4) UNSIGNED,
`close` DECIMAL(12,4) UNSIGNED,
`volume` DECIMAL(20,4) UNSIGNED,
`amount` DECIMAL(20,4) UNSIGNED,
`adjustflag` TINYINT UNSIGNED,
`turn` DECIMAL(10,6) UNSIGNED,
`pctChg` DECIMAL(10,6),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `idx_date_code` (`date`, `code`),
KEY `idx_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
改进说明:
| 优化点 | 原方案问题 | 新方案优势 |
|---|---|---|
| 主键设计 | 仅用date字段 | 新增自增ID,添加(date,code)唯一索引 |
| 精度设置 | DECIMAL(10,5) | 调整为DECIMAL(12,4)更符合A股实际范围 |
| 字段类型 | 无符号约束 | 价格/成交量等添加UNSIGNED防止负数 |
| 索引设计 | 无索引 | 添加复合索引加速查询 |
直接拼接SQL语句的INSERT方式不仅效率低,还存在SQL注入风险。应该使用参数化查询配合executemany实现批量插入:
python复制def batch_insert_data(cursor, data):
insert_sql = """
INSERT INTO stock_daily
(date, code, open, high, low, close, volume, amount, adjustflag, turn, pctChg)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
open=VALUES(open), high=VALUES(high), low=VALUES(low),
close=VALUES(close), volume=VALUES(volume), amount=VALUES(amount),
adjustflag=VALUES(adjustflag), turn=VALUES(turn), pctChg=VALUES(pctChg)
"""
# 准备批量插入数据
batch_data = []
for item in data.values:
batch_data.append((
item[0], # date
item[1], # code
float(item[2]), # open
float(item[3]), # high
float(item[4]), # low
float(item[5]), # close
float(item[6]), # volume
float(item[7]), # amount
int(item[8]), # adjustflag
float(item[9]), # turn
float(item[10]) # pctChg
))
try:
cursor.executemany(insert_sql, batch_data)
cursor.connection.commit()
print(f"成功插入/更新 {len(batch_data)} 条记录")
except Exception as e:
cursor.connection.rollback()
print(f"批量插入失败: {str(e)}")
性能对比:
| 操作方法 | 1000条记录耗时 | 安全性 |
|---|---|---|
| 原始单条INSERT | ~12秒 | 低 |
| executemany批量 | ~0.8秒 | 高 |
Baostock对API调用有频率限制,直接连续请求可能导致临时封禁。需要实现智能的请求控制:
python复制import time
import random
from datetime import datetime
class BaostockController:
def __init__(self):
self.last_request_time = None
self.request_count = 0
def safe_query(self, query_func, *args, **kwargs):
# 遵守API速率限制
if self.last_request_time:
elapsed = (datetime.now() - self.last_request_time).total_seconds()
if elapsed < 0.5: # 至少500毫秒间隔
sleep_time = 0.5 - elapsed + random.uniform(0, 0.1)
time.sleep(sleep_time)
# 每30次请求后长暂停
if self.request_count >= 30:
time.sleep(60)
self.request_count = 0
try:
result = query_func(*args, **kwargs)
self.last_request_time = datetime.now()
self.request_count += 1
return result
except Exception as e:
print(f"查询失败: {str(e)}")
time.sleep(60) # 出错后等待1分钟
return self.safe_query(query_func, *args, **kwargs)
使用示例:
python复制controller = BaostockController()
data = controller.safe_query(bs.query_history_k_data_plus,
code="000001.SH",
fields="date,code,open,high,low,close",
start_date="2023-01-01",
end_date="2023-12-31",
frequency="d",
adjustflag="3")
从API获取的原始数据可能存在缺失或异常值,直接存储会导致后续分析出错。必须添加数据验证层:
python复制def validate_and_clean(dataframe):
# 检查必要字段是否存在
required_columns = ['date', 'code', 'open', 'high', 'low', 'close']
for col in required_columns:
if col not in dataframe.columns:
raise ValueError(f"缺失必要字段: {col}")
# 处理缺失值
df = dataframe.copy()
df.replace('', None, inplace=True)
# 类型转换
numeric_cols = ['open', 'high', 'low', 'close', 'volume', 'amount', 'turn', 'pctChg']
for col in numeric_cols:
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors='coerce')
# 价格合理性检查
price_cols = ['open', 'high', 'low', 'close']
for col in price_cols:
if col in df.columns:
df = df[(df[col] > 0) | (df[col].isna())]
# 日期格式标准化
if 'date' in df.columns:
df['date'] = pd.to_datetime(df['date']).dt.strftime('%Y-%m-%d')
return df.drop_duplicates(subset=['date', 'code'])
常见数据问题处理:
| 问题类型 | 检测方法 | 处理方案 |
|---|---|---|
| 价格为零 | price <= 0 | 标记为异常或删除 |
| 价格倒挂 | high < low 或 open > high | 修正为合理值 |
| 成交量异常 | volume <= 0 | 设为NULL |
| 日期格式混乱 | 非标准日期格式 | 统一转为YYYY-MM-DD |