346 lines
12 KiB
Python
346 lines
12 KiB
Python
"""
|
|
新闻公告数据获取模块
|
|
使用pywencai获取股票的最新新闻和公告信息
|
|
"""
|
|
|
|
import pandas as pd
|
|
import pywencai
|
|
import sys
|
|
import io
|
|
import warnings
|
|
from datetime import datetime
|
|
from http_timeout import call_with_timeout
|
|
|
|
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()
|
|
|
|
|
|
class NewsAnnouncementDataFetcher:
|
|
"""新闻公告数据获取类"""
|
|
|
|
def __init__(self):
|
|
self.max_items = 20 # 最多获取的新闻/公告数量
|
|
|
|
def get_news_and_announcements(self, symbol):
|
|
"""
|
|
获取股票的新闻和公告数据
|
|
|
|
Args:
|
|
symbol: 股票代码(6位数字)
|
|
|
|
Returns:
|
|
dict: 包含新闻和公告数据的字典
|
|
"""
|
|
data = {
|
|
"symbol": symbol,
|
|
"news_data": None,
|
|
"announcement_data": None,
|
|
"data_success": False
|
|
}
|
|
|
|
# 只支持中国股票
|
|
if not self._is_chinese_stock(symbol):
|
|
data["error"] = "新闻公告数据仅支持中国A股股票"
|
|
return data
|
|
|
|
try:
|
|
# 获取新闻数据
|
|
print("📰 正在获取最新新闻数据...")
|
|
news_data = self._get_news_data(symbol)
|
|
if news_data:
|
|
data["news_data"] = news_data
|
|
print(f" ✓ 成功获取 {len(news_data.get('items', []))} 条新闻")
|
|
|
|
# 获取公告数据
|
|
print("📢 正在获取最新公告数据...")
|
|
announcement_data = self._get_announcement_data(symbol)
|
|
if announcement_data:
|
|
data["announcement_data"] = announcement_data
|
|
print(f" ✓ 成功获取 {len(announcement_data.get('items', []))} 条公告")
|
|
|
|
# 如果至少有一个成功,则标记为成功
|
|
if news_data or announcement_data:
|
|
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):
|
|
"""获取新闻数据"""
|
|
try:
|
|
# 构建问句
|
|
query = f"{symbol}新闻"
|
|
|
|
print(f" 使用问财查询: {query}")
|
|
|
|
# 使用pywencai查询
|
|
result = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
|
|
|
|
if result is None:
|
|
print(f" 问财查询返回None")
|
|
return None
|
|
|
|
# 处理不同类型的返回结果
|
|
df_result = None
|
|
|
|
if isinstance(result, dict):
|
|
try:
|
|
df_result = pd.DataFrame([result])
|
|
except Exception as e:
|
|
print(f" 无法转换为DataFrame: {e}")
|
|
return None
|
|
elif isinstance(result, pd.DataFrame):
|
|
df_result = result
|
|
else:
|
|
print(f" 问财返回未知类型: {type(result)}")
|
|
return None
|
|
|
|
if df_result is None or df_result.empty:
|
|
print(f" 查询结果为空")
|
|
return None
|
|
|
|
# 检查是否是嵌套结构
|
|
if 'tableV1' in df_result.columns and len(df_result.columns) == 1:
|
|
table_v1_data = df_result.iloc[0]['tableV1']
|
|
if isinstance(table_v1_data, pd.DataFrame):
|
|
df_result = table_v1_data
|
|
elif isinstance(table_v1_data, list) and len(table_v1_data) > 0:
|
|
df_result = pd.DataFrame(table_v1_data)
|
|
else:
|
|
print(f" tableV1数据类型不支持: {type(table_v1_data)}")
|
|
return None
|
|
|
|
if df_result is None or df_result.empty:
|
|
return None
|
|
|
|
# 提取新闻数据
|
|
news_items = []
|
|
|
|
# 限制数量
|
|
df_result = df_result.head(self.max_items)
|
|
|
|
for idx, row in df_result.iterrows():
|
|
item = {}
|
|
|
|
# 尝试提取常见的新闻字段
|
|
for col in df_result.columns:
|
|
col_lower = str(col).lower()
|
|
value = row.get(col)
|
|
|
|
# 跳过空值和DataFrame类型
|
|
if value is None or (isinstance(value, float) and pd.isna(value)):
|
|
continue
|
|
if isinstance(value, pd.DataFrame):
|
|
continue
|
|
|
|
# 保存字段
|
|
try:
|
|
item[col] = str(value)
|
|
except:
|
|
item[col] = "无法解析"
|
|
|
|
if item: # 如果有数据才添加
|
|
news_items.append(item)
|
|
|
|
if not news_items:
|
|
return None
|
|
|
|
return {
|
|
"items": news_items,
|
|
"count": len(news_items),
|
|
"columns": df_result.columns.tolist(),
|
|
"query_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f" 获取新闻数据异常: {e}")
|
|
return None
|
|
|
|
def _get_announcement_data(self, symbol):
|
|
"""获取公告数据"""
|
|
try:
|
|
# 构建问句
|
|
query = f"{symbol}公告"
|
|
|
|
print(f" 使用问财查询: {query}")
|
|
|
|
# 使用pywencai查询
|
|
result = call_with_timeout(pywencai.get, timeout=30, query=query, loop=True)
|
|
|
|
if result is None:
|
|
print(f" 问财查询返回None")
|
|
return None
|
|
|
|
# 处理不同类型的返回结果
|
|
df_result = None
|
|
|
|
if isinstance(result, dict):
|
|
try:
|
|
df_result = pd.DataFrame([result])
|
|
except Exception as e:
|
|
print(f" 无法转换为DataFrame: {e}")
|
|
return None
|
|
elif isinstance(result, pd.DataFrame):
|
|
df_result = result
|
|
else:
|
|
print(f" 问财返回未知类型: {type(result)}")
|
|
return None
|
|
|
|
if df_result is None or df_result.empty:
|
|
print(f" 查询结果为空")
|
|
return None
|
|
|
|
# 检查是否是嵌套结构
|
|
if 'tableV1' in df_result.columns and len(df_result.columns) == 1:
|
|
table_v1_data = df_result.iloc[0]['tableV1']
|
|
if isinstance(table_v1_data, pd.DataFrame):
|
|
df_result = table_v1_data
|
|
elif isinstance(table_v1_data, list) and len(table_v1_data) > 0:
|
|
df_result = pd.DataFrame(table_v1_data)
|
|
else:
|
|
print(f" tableV1数据类型不支持: {type(table_v1_data)}")
|
|
return None
|
|
|
|
if df_result is None or df_result.empty:
|
|
return None
|
|
|
|
# 提取公告数据
|
|
announcement_items = []
|
|
|
|
# 限制数量
|
|
df_result = df_result.head(self.max_items)
|
|
|
|
for idx, row in df_result.iterrows():
|
|
item = {}
|
|
|
|
# 尝试提取常见的公告字段
|
|
for col in df_result.columns:
|
|
value = row.get(col)
|
|
|
|
# 跳过空值和DataFrame类型
|
|
if value is None or (isinstance(value, float) and pd.isna(value)):
|
|
continue
|
|
if isinstance(value, pd.DataFrame):
|
|
continue
|
|
|
|
# 保存字段
|
|
try:
|
|
item[col] = str(value)
|
|
except:
|
|
item[col] = "无法解析"
|
|
|
|
if item: # 如果有数据才添加
|
|
announcement_items.append(item)
|
|
|
|
if not announcement_items:
|
|
return None
|
|
|
|
return {
|
|
"items": announcement_items,
|
|
"count": len(announcement_items),
|
|
"columns": df_result.columns.tolist(),
|
|
"query_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f" 获取公告数据异常: {e}")
|
|
return None
|
|
|
|
def format_news_announcements_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"""
|
|
【最新新闻】
|
|
查询时间:{news_data.get('query_time', 'N/A')}
|
|
新闻数量:{news_data.get('count', 0)}条
|
|
|
|
""")
|
|
|
|
for idx, item in enumerate(news_data.get('items', []), 1):
|
|
text_parts.append(f"新闻 {idx}:")
|
|
for key, value in item.items():
|
|
# 跳过过长的字段
|
|
if len(str(value)) > 500:
|
|
value = str(value)[:500] + "..."
|
|
text_parts.append(f" {key}: {value}")
|
|
text_parts.append("") # 空行分隔
|
|
|
|
# 公告数据
|
|
if data.get("announcement_data"):
|
|
announcement_data = data["announcement_data"]
|
|
text_parts.append(f"""
|
|
【最新公告】
|
|
查询时间:{announcement_data.get('query_time', 'N/A')}
|
|
公告数量:{announcement_data.get('count', 0)}条
|
|
|
|
""")
|
|
|
|
for idx, item in enumerate(announcement_data.get('items', []), 1):
|
|
text_parts.append(f"公告 {idx}:")
|
|
for key, value in item.items():
|
|
# 跳过过长的字段
|
|
if len(str(value)) > 500:
|
|
value = str(value)[:500] + "..."
|
|
text_parts.append(f" {key}: {value}")
|
|
text_parts.append("") # 空行分隔
|
|
|
|
return "\n".join(text_parts)
|
|
|
|
|
|
# 测试函数
|
|
if __name__ == "__main__":
|
|
print("测试新闻公告数据获取...")
|
|
fetcher = NewsAnnouncementDataFetcher()
|
|
|
|
# 测试平安银行
|
|
symbol = "000001"
|
|
print(f"\n正在获取 {symbol} 的新闻公告数据...\n")
|
|
|
|
data = fetcher.get_news_and_announcements(symbol)
|
|
|
|
if data.get("data_success"):
|
|
print("\n" + "="*60)
|
|
print("新闻公告数据获取成功!")
|
|
print("="*60)
|
|
|
|
formatted_text = fetcher.format_news_announcements_for_ai(data)
|
|
print(formatted_text)
|
|
else:
|
|
print(f"\n获取失败: {data.get('error', '未知错误')}")
|