Files
tushare/fetch_sector_data.py
2025-10-19 07:46:53 +08:00

504 lines
20 KiB
Python
Executable File

#!/usr/bin/env python3
"""
板块数据抓取脚本
包括行业分类、概念板块、地域分类等数据
"""
import os
import sys
import time
import json
import pandas as pd
import pymysql
from datetime import datetime, timedelta
from typing import Dict, List, Optional
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
import tudata as ts
# 添加项目根目录到路径
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from config import Config
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('logs/sector_data_fetch.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
class SectorDataFetcher:
"""板块数据抓取器"""
def __init__(self):
self.ts_pro = ts.pro_api(Config.TS_TOKEN)
self.db_config = {
'host': Config.DB_HOST,
'port': Config.DB_PORT,
'user': Config.DB_USER,
'password': Config.DB_PASSWORD,
'database': Config.DB_NAME,
'charset': 'utf8mb4'
}
self.progress_file = 'logs/sector_fetch_progress.json'
self.stats = {
'start_time': datetime.now().isoformat(),
'processed_tables': 0,
'total_records': 0,
'failed_operations': []
}
def get_db_connection(self):
"""获取数据库连接"""
return pymysql.connect(**self.db_config)
def create_tables(self):
"""创建所需的数据表"""
tables_sql = {
'industry_classify': """
CREATE TABLE IF NOT EXISTS industry_classify (
id INT AUTO_INCREMENT PRIMARY KEY,
ts_code VARCHAR(20) NOT NULL COMMENT '股票代码',
symbol VARCHAR(20) COMMENT '股票简称',
name VARCHAR(100) COMMENT '股票名称',
industry VARCHAR(100) COMMENT '所属行业',
industry_code VARCHAR(20) COMMENT '行业代码',
level VARCHAR(10) COMMENT '行业级别',
src VARCHAR(10) COMMENT '数据源',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_ts_code (ts_code),
INDEX idx_industry (industry),
INDEX idx_industry_code (industry_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='行业分类表'
""",
'concept_classify': """
CREATE TABLE IF NOT EXISTS concept_classify (
id INT AUTO_INCREMENT PRIMARY KEY,
ts_code VARCHAR(20) NOT NULL COMMENT '股票代码',
name VARCHAR(100) COMMENT '股票名称',
concept_name VARCHAR(100) COMMENT '概念名称',
concept_code VARCHAR(20) COMMENT '概念代码',
src VARCHAR(10) COMMENT '数据源',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_ts_code (ts_code),
INDEX idx_concept (concept_name),
INDEX idx_concept_code (concept_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='概念分类表'
""",
'area_classify': """
CREATE TABLE IF NOT EXISTS area_classify (
id INT AUTO_INCREMENT PRIMARY KEY,
ts_code VARCHAR(20) NOT NULL COMMENT '股票代码',
name VARCHAR(100) COMMENT '股票名称',
area VARCHAR(50) COMMENT '所属地域',
province VARCHAR(50) COMMENT '省份',
city VARCHAR(50) COMMENT '城市',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_ts_code (ts_code),
INDEX idx_area (area),
INDEX idx_province (province)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='地域分类表'
""",
'market_segment': """
CREATE TABLE IF NOT EXISTS market_segment (
id INT AUTO_INCREMENT PRIMARY KEY,
ts_code VARCHAR(20) NOT NULL COMMENT '股票代码',
name VARCHAR(100) COMMENT '股票名称',
market VARCHAR(20) COMMENT '市场类型',
segment VARCHAR(50) COMMENT '市场板块',
is_hs VARCHAR(1) COMMENT '是否沪深港通',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_ts_code (ts_code),
INDEX idx_market (market),
INDEX idx_segment (segment)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='市场板块表'
"""
}
conn = self.get_db_connection()
try:
with conn.cursor() as cursor:
for table_name, sql in tables_sql.items():
cursor.execute(sql)
logger.info(f"✅ 创建/检查表: {table_name}")
conn.commit()
logger.info("🎯 所有板块相关表创建完成")
except Exception as e:
logger.error(f"❌ 创建表失败: {e}")
raise
finally:
conn.close()
def fetch_industry_classify(self):
"""抓取行业分类数据"""
logger.info("🚀 开始抓取行业分类数据...")
try:
# 获取行业分类
df = self.ts_pro.stock_company(
exchange='',
fields='ts_code,chairman,manager,secretary,reg_capital,setup_date,province'
)
if df.empty:
logger.warning("⚠️ 未获取到行业分类数据")
return 0
# 获取详细行业信息
industry_df = self.ts_pro.hs_const(hs_type='A')
conn = self.get_db_connection()
try:
with conn.cursor() as cursor:
# 清空现有数据
cursor.execute("DELETE FROM industry_classify")
# 批量插入数据
insert_sql = """
INSERT INTO industry_classify
(ts_code, symbol, name, industry, industry_code, level, src)
VALUES (%s, %s, %s, %s, %s, %s, %s)
"""
records = []
for _, row in industry_df.iterrows():
records.append((
row.get('ts_code', ''),
row.get('symbol', ''),
row.get('name', ''),
row.get('industry', ''),
row.get('industry_code', ''),
row.get('level', ''),
'tushare'
))
cursor.executemany(insert_sql, records)
conn.commit()
record_count = len(records)
self.stats['total_records'] += record_count
logger.info(f"✅ 行业分类数据抓取完成: {record_count}条记录")
return record_count
except Exception as e:
logger.error(f"❌ 行业分类数据插入失败: {e}")
conn.rollback()
return 0
finally:
conn.close()
except Exception as e:
logger.error(f"❌ 抓取行业分类数据失败: {e}")
self.stats['failed_operations'].append({
'operation': 'fetch_industry_classify',
'error': str(e),
'time': datetime.now().isoformat()
})
return 0
def fetch_concept_classify(self):
"""抓取概念分类数据"""
logger.info("🚀 开始抓取概念分类数据...")
try:
# 获取概念分类
df = self.ts_pro.concept()
if df.empty:
logger.warning("⚠️ 未获取到概念分类数据")
return 0
# 获取概念成分股
concept_detail_records = []
for _, concept_row in df.iterrows():
try:
concept_code = concept_row['code']
concept_name = concept_row['name']
# 获取该概念的成分股
detail_df = self.ts_pro.concept_detail(id=concept_code)
time.sleep(0.2) # API限频
for _, detail_row in detail_df.iterrows():
concept_detail_records.append((
detail_row.get('ts_code', ''),
detail_row.get('name', ''),
concept_name,
concept_code,
'tushare'
))
except Exception as e:
logger.warning(f"⚠️ 获取概念{concept_name}成分股失败: {e}")
continue
conn = self.get_db_connection()
try:
with conn.cursor() as cursor:
# 清空现有数据
cursor.execute("DELETE FROM concept_classify")
# 批量插入数据
insert_sql = """
INSERT INTO concept_classify
(ts_code, name, concept_name, concept_code, src)
VALUES (%s, %s, %s, %s, %s)
"""
cursor.executemany(insert_sql, concept_detail_records)
conn.commit()
record_count = len(concept_detail_records)
self.stats['total_records'] += record_count
logger.info(f"✅ 概念分类数据抓取完成: {record_count}条记录")
return record_count
except Exception as e:
logger.error(f"❌ 概念分类数据插入失败: {e}")
conn.rollback()
return 0
finally:
conn.close()
except Exception as e:
logger.error(f"❌ 抓取概念分类数据失败: {e}")
self.stats['failed_operations'].append({
'operation': 'fetch_concept_classify',
'error': str(e),
'time': datetime.now().isoformat()
})
return 0
def fetch_area_classify(self):
"""抓取地域分类数据"""
logger.info("🚀 开始抓取地域分类数据...")
try:
# 直接使用已有的股票基本信息,避免重复调用API
conn = self.get_db_connection()
try:
with conn.cursor() as cursor:
# 从stock_basic表获取地域信息
cursor.execute("""
SELECT ts_code, name, area, industry
FROM stock_basic
WHERE list_status = 'L'
""")
stock_data = cursor.fetchall()
if not stock_data:
logger.warning("⚠️ 未获取到股票基本信息")
return 0
# 清空现有数据
cursor.execute("DELETE FROM area_classify")
# 批量插入数据
insert_sql = """
INSERT INTO area_classify
(ts_code, name, area, province, city)
VALUES (%s, %s, %s, %s, %s)
"""
records = []
for row in stock_data:
ts_code, name, area, industry = row
records.append((
ts_code,
name,
area or '', # 地域
area or '', # 省份 (使用area字段)
'' # 城市 (暂时为空)
))
if records:
cursor.executemany(insert_sql, records)
conn.commit()
record_count = len(records)
self.stats['total_records'] += record_count
logger.info(f"✅ 地域分类数据抓取完成: {record_count}条记录")
return record_count
else:
logger.warning("⚠️ 没有数据需要插入")
return 0
except Exception as e:
logger.error(f"❌ 地域分类数据插入失败: {e}")
conn.rollback()
return 0
finally:
conn.close()
except Exception as e:
logger.error(f"❌ 抓取地域分类数据失败: {e}")
self.stats['failed_operations'].append({
'operation': 'fetch_area_classify',
'error': str(e),
'time': datetime.now().isoformat()
})
return 0
def fetch_market_segment(self):
"""抓取市场板块数据"""
logger.info("🚀 开始抓取市场板块数据...")
try:
# 获取沪深港通成分股
hsgt_df = self.ts_pro.hs_const(hs_type='SH') # 沪股通
hk_df = self.ts_pro.hs_const(hs_type='SZ') # 深股通
# 获取股票基本信息
basic_df = self.ts_pro.stock_basic(
exchange='',
list_status='L',
fields='ts_code,symbol,name,area,industry,market,list_date'
)
conn = self.get_db_connection()
try:
with conn.cursor() as cursor:
# 清空现有数据
cursor.execute("DELETE FROM market_segment")
# 批量插入数据
insert_sql = """
INSERT INTO market_segment
(ts_code, name, market, segment, is_hs)
VALUES (%s, %s, %s, %s, %s)
"""
records = []
hsgt_codes = set(hsgt_df['ts_code'].tolist() + hk_df['ts_code'].tolist())
for _, row in basic_df.iterrows():
ts_code = row.get('ts_code', '')
is_hs = 'Y' if ts_code in hsgt_codes else 'N'
records.append((
ts_code,
row.get('name', ''),
row.get('market', ''),
row.get('industry', ''), # 使用行业作为板块
is_hs
))
cursor.executemany(insert_sql, records)
conn.commit()
record_count = len(records)
self.stats['total_records'] += record_count
logger.info(f"✅ 市场板块数据抓取完成: {record_count}条记录")
return record_count
except Exception as e:
logger.error(f"❌ 市场板块数据插入失败: {e}")
conn.rollback()
return 0
finally:
conn.close()
except Exception as e:
logger.error(f"❌ 抓取市场板块数据失败: {e}")
self.stats['failed_operations'].append({
'operation': 'fetch_market_segment',
'error': str(e),
'time': datetime.now().isoformat()
})
return 0
def save_progress(self):
"""保存进度"""
try:
os.makedirs('logs', exist_ok=True)
with open(self.progress_file, 'w') as f:
json.dump(self.stats, f, indent=2, ensure_ascii=False)
except Exception as e:
logger.error(f"❌ 保存进度失败: {e}")
def run(self):
"""执行板块数据抓取"""
logger.info("🎯 开始板块数据抓取任务...")
try:
# 创建数据表
self.create_tables()
# 按优先级抓取数据
tasks = [
('行业分类', self.fetch_industry_classify),
('概念分类', self.fetch_concept_classify),
('地域分类', self.fetch_area_classify),
('市场板块', self.fetch_market_segment)
]
for task_name, task_func in tasks:
logger.info(f"📊 正在处理: {task_name}")
try:
record_count = task_func()
self.stats['processed_tables'] += 1
logger.info(f"✅ {task_name}完成: {record_count}条记录")
# 保存进度
self.save_progress()
# API限频
time.sleep(1)
except Exception as e:
logger.error(f"❌ {task_name}失败: {e}")
self.stats['failed_operations'].append({
'task': task_name,
'error': str(e),
'time': datetime.now().isoformat()
})
# 最终统计
self.stats['end_time'] = datetime.now().isoformat()
self.stats['duration'] = (datetime.now() - datetime.fromisoformat(self.stats['start_time'])).total_seconds()
logger.info("🎉 板块数据抓取任务完成!")
logger.info(f"📊 处理表数: {self.stats['processed_tables']}")
logger.info(f"📈 总记录数: {self.stats['total_records']:,}")
logger.info(f"⏱️ 耗时: {self.stats['duration']:.2f}秒")
if self.stats['failed_operations']:
logger.warning(f"⚠️ 失败操作数: {len(self.stats['failed_operations'])}")
self.save_progress()
except Exception as e:
logger.error(f"❌ 板块数据抓取任务失败: {e}")
self.stats['error'] = str(e)
self.save_progress()
raise
def main():
"""主函数"""
try:
fetcher = SectorDataFetcher()
fetcher.run()
except KeyboardInterrupt:
logger.info("👋 用户中断程序")
except Exception as e:
logger.error(f"❌ 程序执行失败: {e}")
return 1
return 0
if __name__ == "__main__":
exit(main())