Files
aiagents-stock/qstock_news_data.py
T
songzhuoyuan befdc32aea tushare
2026-08-11 20:41:31 +08:00

384 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
新闻数据获取模块
优先使用tushare,失败时使用akshare获取股票的最新新闻信息
"""
import pandas as pd
import sys
import io
import warnings
from datetime import datetime, timedelta
import akshare as ak
from data_source_manager import data_source_manager
warnings.filterwarnings('ignore')
# 设置标准输出编码为UTF-8(仅在命令行环境,避免streamlit冲突)
def _setup_stdout_encoding():
"""仅在命令行环境设置标准输出编码"""
if sys.platform == 'win32' and not hasattr(sys.stdout, '_original_stream'):
try:
# 检测是否在streamlit环境中
import streamlit
# 在streamlit中不修改stdout
return
except ImportError:
# 不在streamlit环境,可以安全修改
try:
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='ignore')
except:
pass
_setup_stdout_encoding()
# 记录tushare news接口是否无权限(避免每次分析都重复请求失败)
_tushare_news_unavailable = False
class QStockNewsDataFetcher:
"""新闻数据获取类(使用akshare作为数据源)"""
def __init__(self):
self.max_items = 30 # 最多获取的新闻数量
self.available = True
print("✓ 新闻数据获取器初始化成功(tushare优先/akshare备用)")
def get_stock_news(self, symbol):
"""
获取股票的新闻数据
Args:
symbol: 股票代码(6位数字)
Returns:
dict: 包含新闻数据的字典
"""
data = {
"symbol": symbol,
"news_data": None,
"data_success": False,
"source": "qstock"
}
if not self.available:
data["error"] = "qstock库未安装或不可用"
return data
# 只支持中国股票
if not self._is_chinese_stock(symbol):
data["error"] = "新闻数据仅支持中国A股股票"
return data
try:
# 获取新闻数据
print(f"📰 正在获取 {symbol} 的最新新闻...")
news_data = self._get_news_data(symbol)
if news_data:
data["news_data"] = news_data
print(f" ✓ 成功获取 {len(news_data.get('items', []))} 条新闻")
data["data_success"] = True
print("✅ 新闻数据获取完成")
else:
print("⚠️ 未能获取到新闻数据")
except Exception as e:
print(f"❌ 获取新闻数据失败: {e}")
data["error"] = str(e)
return data
def _is_chinese_stock(self, symbol):
"""判断是否为中国股票"""
return symbol.isdigit() and len(symbol) == 6
def _get_news_data(self, symbol):
"""获取新闻数据(优先tushare,失败时使用akshare"""
try:
# 优先使用tushare新闻接口
tushare_items = self._get_news_from_tushare(symbol)
if tushare_items:
print(f" ✓ 从tushare获取到 {len(tushare_items)} 条相关新闻")
return {
"items": tushare_items,
"count": len(tushare_items),
"query_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
"date_range": "最近新闻"
}
print(f" 使用 akshare 获取新闻(备用数据源)...")
news_items = []
# 方法1: 尝试获取个股新闻(东方财富)
try:
# stock_news_em(symbol="600519") - 东方财富个股新闻
df = ak.stock_news_em(symbol=symbol)
if df is not None and not df.empty:
print(f" ✓ 从东方财富获取到 {len(df)} 条新闻")
# 处理DataFrame,提取新闻
for idx, row in df.head(self.max_items).iterrows():
item = {'source': '东方财富'}
# 提取所有列
for col in df.columns:
value = row.get(col)
# 跳过空值
if value is None or (isinstance(value, float) and pd.isna(value)):
continue
# 保存字段
try:
item[col] = str(value)
except:
item[col] = "无法解析"
if len(item) > 1: # 如果有数据才添加
news_items.append(item)
except Exception as e:
print(f" ⚠ 从东方财富获取失败: {e}")
# 方法2: 如果没有获取到,尝试获取新浪财经新闻
if not news_items:
try:
# stock_zh_a_spot_em() - 获取股票信息,包含代码和名称
df_info = ak.stock_zh_a_spot_em()
# 查找股票名称
stock_name = None
if df_info is not None and not df_info.empty:
match = df_info[df_info['代码'] == symbol]
if not match.empty:
stock_name = match.iloc[0]['名称']
print(f" 找到股票名称: {stock_name}")
# 使用股票名称搜索新闻
if stock_name:
# stock_news_sina - 新浪财经新闻
try:
df = ak.stock_news_sina(symbol=stock_name)
if df is not None and not df.empty:
print(f" ✓ 从新浪财经获取到 {len(df)} 条新闻")
for idx, row in df.head(self.max_items).iterrows():
item = {'source': '新浪财经'}
for col in df.columns:
value = row.get(col)
if value is None or (isinstance(value, float) and pd.isna(value)):
continue
try:
item[col] = str(value)
except:
item[col] = "无法解析"
if len(item) > 1:
news_items.append(item)
except:
pass
except Exception as e:
print(f" ⚠ 从新浪财经获取失败: {e}")
# 方法3: 尝试获取财联社电报
if not news_items or len(news_items) < 5:
try:
# stock_news_cls() - 财联社电报
df = ak.stock_news_cls()
if df is not None and not df.empty:
# 筛选包含股票代码或名称的新闻
df_filtered = df[
df['内容'].str.contains(symbol, na=False) |
df['标题'].str.contains(symbol, na=False)
]
if not df_filtered.empty:
print(f" ✓ 从财联社获取到 {len(df_filtered)} 条相关新闻")
for idx, row in df_filtered.head(self.max_items - len(news_items)).iterrows():
item = {'source': '财联社'}
for col in df_filtered.columns:
value = row.get(col)
if value is None or (isinstance(value, float) and pd.isna(value)):
continue
try:
item[col] = str(value)
except:
item[col] = "无法解析"
if len(item) > 1:
news_items.append(item)
except Exception as e:
print(f" ⚠ 从财联社获取失败: {e}")
if not news_items:
print(f" 未找到股票 {symbol} 的新闻")
return None
# 限制数量
news_items = news_items[:self.max_items]
return {
"items": news_items,
"count": len(news_items),
"query_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
"date_range": "最近新闻"
}
except Exception as e:
print(f" 获取新闻数据异常: {e}")
import traceback
traceback.print_exc()
return None
def _get_news_from_tushare(self, symbol):
"""从tushare获取个股新闻(按股票名称/代码过滤)"""
global _tushare_news_unavailable
try:
if _tushare_news_unavailable or not data_source_manager.tushare_available:
return None
# 获取股票名称
stock_name = None
try:
basic = data_source_manager.get_stock_basic_info(symbol)
if basic and basic.get('name') and basic['name'] != '未知':
stock_name = basic['name']
except Exception as e:
print(f" 获取股票名称失败: {e}")
# 查询最近7天的全市场新闻(东方财富源)
end_date = datetime.now().strftime('%Y-%m-%d')
start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
df = data_source_manager.tushare_api.news(
src='eastmoney',
start_date=start_date,
end_date=end_date
)
if df is None or df.empty:
return None
# 按股票代码或名称过滤
mask = df['title'].str.contains(symbol, na=False) | df['title'].str.contains(stock_name, na=False) if stock_name else df['title'].str.contains(symbol, na=False)
if 'content' in df.columns:
mask = mask | df['content'].str.contains(symbol, na=False)
if stock_name:
mask = mask | df['content'].str.contains(stock_name, na=False)
df_filtered = df[mask]
if df_filtered.empty:
return None
news_items = []
for _, row in df_filtered.head(self.max_items).iterrows():
item = {'source': 'tushare-东方财富'}
for col in ['title', 'content', 'pub_time']:
if col in df_filtered.columns:
value = row.get(col)
if value is None or (isinstance(value, float) and pd.isna(value)):
continue
try:
item[col] = str(value)
except:
item[col] = "无法解析"
if len(item) > 1:
news_items.append(item)
return news_items or None
except Exception as e:
error_msg = str(e)
if "权限" in error_msg or "积分" in error_msg:
_tushare_news_unavailable = True
print(" ⚠ tushare news 接口需要较高积分,当前账号无权限,已自动使用 akshare 获取新闻")
else:
print(f" ⚠ 从tushare获取新闻失败: {error_msg}")
return None
def format_news_for_ai(self, data):
"""
将新闻数据格式化为适合AI阅读的文本
"""
if not data or not data.get("data_success"):
return "未能获取新闻数据"
text_parts = []
# 新闻数据
if data.get("news_data"):
news_data = data["news_data"]
text_parts.append(f"""
【最新新闻 - tushare/akshare自动切换】
查询时间:{news_data.get('query_time', 'N/A')}
时间范围:{news_data.get('date_range', 'N/A')}
新闻数量:{news_data.get('count', 0)}
""")
for idx, item in enumerate(news_data.get('items', []), 1):
text_parts.append(f"新闻 {idx}:")
# 优先显示的字段
priority_fields = ['title', 'date', 'time', 'source', 'content', 'url']
# 先显示优先字段
for field in priority_fields:
if field in item:
value = item[field]
# 限制content长度
if field == 'content' and len(str(value)) > 500:
value = str(value)[:500] + "..."
text_parts.append(f" {field}: {value}")
# 再显示其他字段
for key, value in item.items():
if key not in priority_fields and key != 'source':
# 跳过过长的字段
if len(str(value)) > 300:
value = str(value)[:300] + "..."
text_parts.append(f" {key}: {value}")
text_parts.append("") # 空行分隔
return "\n".join(text_parts)
# 测试函数
if __name__ == "__main__":
print("测试新闻数据获取(akshare数据源)...")
print("="*60)
fetcher = QStockNewsDataFetcher()
if not fetcher.available:
print("❌ 新闻数据获取器不可用")
sys.exit(1)
# 测试股票
test_symbols = ["000001", "600519"] # 平安银行、贵州茅台
for symbol in test_symbols:
print(f"\n{'='*60}")
print(f"正在测试股票: {symbol}")
print(f"{'='*60}\n")
data = fetcher.get_stock_news(symbol)
if data.get("data_success"):
print("\n" + "="*60)
print("新闻数据获取成功!")
print("="*60)
formatted_text = fetcher.format_news_for_ai(data)
print(formatted_text)
else:
print(f"\n获取失败: {data.get('error', '未知错误')}")
print("\n")