""" 主程序入口 - 股吧人气指示器 """ import sys import time from datetime import datetime from PySide6.QtWidgets import QApplication from PySide6.QtCore import QTimer, Signal, QObject, QThread from loguru import logger from config_manager import ConfigManager from database import DatabaseManager from spider import SpiderManager from llm_analyzer import LLMAnalyzer from main_window import MainWindow class ScreenshotWorker(QObject): """截图工作器 - 专门处理上证所截图""" sse_screenshot_fetched = Signal(str) error_occurred = Signal(str) def __init__(self, spider: SpiderManager): super().__init__() self.spider = spider self.running = False logger.info("ScreenshotWorker 初始化完成") def start(self): """启动截图任务""" self.running = True logger.info("截图任务已启动") def stop(self): """停止截图任务""" self.running = False logger.info("截图任务已停止") def do_fetch(self): """执行截图获取""" if not self.running: return try: logger.info("开始爬取上证所网页截图") screenshot_path = self.spider.fetch_sse_screenshot() if screenshot_path: logger.info(f"成功获取截图: {screenshot_path}") self.sse_screenshot_fetched.emit(screenshot_path) else: logger.warning("未能获取有效的截图") except Exception as e: logger.error(f"爬取截图失败: {str(e)}") self.error_occurred.emit(f"截图失败: {str(e)}") class BackendWorker(QObject): """后台工作器 - 处理爬取和分析任务""" fetch_finished = Signal(list) analysis_finished = Signal(float) analysis_result = Signal(dict) # 传递分析结果详情 error_occurred = Signal(str) status_update = Signal(str) stock_data_fetched = Signal(str, float) # 股票数据获取信号 sse_screenshot_fetched = Signal(str) # 上证所截图获取信号 def __init__(self, config_manager: ConfigManager, db_manager: DatabaseManager, spider: SpiderManager, analyzer: LLMAnalyzer): super().__init__() self.config = config_manager self.db = db_manager self.spider = spider self.analyzer = analyzer self.running = False self.last_fetch_time = 0 self.fetch_interval = 15 # 默认15秒 self.no_new_content_count = 0 # 无新内容计数 self.is_running_cycle = False # 防止并发执行 self.fetch_count = 0 # 爬取次数统计 self.analysis_count = 0 # API分析次数统计 logger.info("BackendWorker 初始化完成") def start(self): """启动后台任务""" self.running = True logger.info("后台任务已启动") # 启动时立即执行第一次任务 QTimer.singleShot(1000, self._run_cycle) def stop(self): """停止后台任务""" self.running = False logger.info("后台任务已停止") def _run_cycle(self): """运行一个周期""" if self.is_running_cycle: logger.debug("上一个周期仍在执行,跳过本次") return self.is_running_cycle = True if not self.running: self.is_running_cycle = False return try: # 1. 先尝试获取截图 logger.info("开始获取截图") self.status_update.emit("正在获取截图...") self.fetch_sse_screenshot() # 2. 爬取评论 logger.info("开始爬取评论") self.status_update.emit("正在爬取评论...") self.fetch_count += 1 comments = self.spider.fetch() if not comments: # 爬取失败,使用固定间隔重试,不计入无新内容计数 interval = 15 # 固定15秒重试 logger.warning(f"未获取到新评论,{int(interval)}秒后重试") self.status_update.emit(f"无新内容,{int(interval)}秒后重试...") self.is_running_cycle = False QTimer.singleShot(int(interval * 1000), self._run_cycle) return # 2. 写入数据库 logger.info(f"获取到 {len(comments)} 条评论") self.status_update.emit(f"获取到 {len(comments)} 条评论...") new_ids = self.db.add_comments_batch(comments) if new_ids: self.no_new_content_count = 0 logger.info(f"新增 {len(new_ids)} 条评论到数据库") self.status_update.emit(f"新增 {len(new_ids)} 条评论") # 3. 获取未分析评论并分析 unanalyzed = self.db.get_unanalyzed_comments(limit=10) logger.info(f"获取到 {len(unanalyzed)} 条未分析评论") if unanalyzed: self.status_update.emit(f"开始分析 {len(unanalyzed)} 条评论...") self._analyze_comments(unanalyzed) else: self.no_new_content_count += 1 logger.info("评论已存在,未新增") # 4. 更新指示器 self._update_indicator() except Exception as e: logger.error(f"运行错误: {str(e)}") self.error_occurred.emit(f"运行错误: {str(e)}") # 安排下一次执行 if self.running: interval = self.fetch_interval * (1 + min(self.no_new_content_count * 1.0, 4.0)) logger.debug(f"下次执行将在 {int(interval)} 秒后") self.is_running_cycle = False QTimer.singleShot(int(interval * 1000), self._run_cycle) else: self.is_running_cycle = False def _analyze_comments(self, comments): """分析评论""" logger.info(f"开始分析 {len(comments)} 条评论") for i, comment in enumerate(comments): if not self.running: logger.warning("分析被中断") break try: self.status_update.emit(f"分析 {i+1}/{len(comments)}...") logger.debug(f"分析第 {i+1} 条评论: {comment['content'][:50]}...") self.analysis_count += 1 score, label = self.analyzer.analyze(comment['content']) # 获取分析结果详情 last_result = self.analyzer.get_last_result() if score is not None: self.db.mark_analyzed(comment['id'], score, label) logger.info(f"评论 {comment['id']} 分析完成: {score}分 - {label}") # 更新状态显示为简洁格式 self.status_update.emit(f"分析 {i+1}/{len(comments)}...返回{score}分") # 每条评论分析完成后立即更新指示器 self._update_indicator() time.sleep(1.0) # 延迟,避免API限流 else: self.db.mark_analyzed(comment['id'], 50, "无法判断") logger.warning(f"评论 {comment['id']} 无法判断") except Exception as e: logger.error(f"分析评论 {comment.get('id', 'unknown')} 失败: {str(e)}") self.error_occurred.emit(f"分析失败: {str(e)}") self.db.mark_analyzed(comment['id'], 50, "分析异常") def _update_indicator(self): """更新指示器显示""" # 获取最新的100条评论的分数 scores = self.db.get_all_scores(limit=100) if not scores: logger.debug("暂无分析分数") return # 计算平均分 avg_score = sum(scores) / len(scores) logger.info(f"当前平均分: {avg_score:.2f} (基于最新的 {len(scores)} 条评论)") # 根据阈值确定标签 thresholds = self.config.get('ui', 'thresholds', default={'cold': 30, 'warm': 70}) cold = thresholds.get('cold', 30) warm = thresholds.get('warm', 70) if avg_score < cold: label = "看跌" elif avg_score > warm: label = "看涨" else: label = "中性" logger.info(f"情感倾向: {label}") # 发送整数分数 self.analysis_finished.emit(int(avg_score)) def fetch_stock_data(self): """爬取股票数据并添加到波形图""" try: logger.info("开始爬取股票数据") stock_data = self.spider.fetch_sse_stock_data() if stock_data and 'time' in stock_data and 'value' in stock_data: logger.info(f"成功获取股票数据: {stock_data}") # 发送股票数据到主窗口 self.stock_data_fetched.emit(stock_data['time'], stock_data['value']) else: logger.warning("未能获取有效的股票数据") except Exception as e: logger.error(f"爬取股票数据失败: {str(e)}") def fetch_sse_screenshot(self): """爬取上证所网页元素截图""" try: logger.info("开始爬取上证所网页截图") screenshot_path = self.spider.fetch_sse_screenshot() if screenshot_path: logger.info(f"成功获取截图: {screenshot_path}") self.sse_screenshot_fetched.emit(screenshot_path) else: logger.warning("未能获取有效的截图") except Exception as e: logger.error(f"爬取截图失败: {str(e)}") def manual_refresh(self): """手动刷新""" logger.info("用户手动刷新") self.no_new_content_count = 0 if not self.is_running_cycle: self._run_cycle() else: logger.info("上一个周期仍在执行,跳过手动刷新") def update_fetch_interval(self, interval: int): """更新爬取间隔""" logger.info(f"更新爬取间隔: {interval} 秒") self.fetch_interval = interval def setup_logging(log_path: str, level: str = "INFO"): """配置日志""" logger.remove() # 移除默认的处理器 # 确保log_path不为None if log_path is None: log_path = 'guba.log' logger.add( log_path, rotation="10 MB", retention="7 days", level=level, encoding="utf-8", format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}" ) # 仅当sys.stdout可用时才添加控制台日志 if sys.stdout is not None: logger.add( sys.stdout, level=level, format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}" ) def main(): """主函数""" logger.info("=== 股吧人气指示器启动 ===") # 创建应用 app = QApplication(sys.argv) app.setQuitOnLastWindowClosed(False) # 允许最小化到托盘 # 加载配置 logger.info("加载配置文件...") config = ConfigManager("config.json") # 配置日志 log_config = config.logging_config log_path = log_config.get('path') or 'guba.log' log_level = log_config.get('level') or 'INFO' setup_logging(log_path, log_level) logger.info(f"日志配置完成: {log_path}, 级别: {log_level}") # 初始化组件 logger.info("初始化数据库...") db = DatabaseManager(config.database_config.get('path', 'guba.db')) logger.info("初始化爬虫...") spider = SpiderManager(config.spider_config) logger.info("初始化LLM分析器...") analyzer = LLMAnalyzer(config.llm_api_config) # 创建后台工作器 worker = BackendWorker(config, db, spider, analyzer) worker.update_fetch_interval(config.spider_config.get('fetch_interval', 15)) # 创建后台线程 logger.info("创建后台线程...") worker_thread = QThread() worker.moveToThread(worker_thread) # 线程启动时开始工作 worker_thread.started.connect(worker.start) # 线程结束时停止工作 worker_thread.finished.connect(worker.stop) # 创建截图后台线程 logger.info("创建截图后台线程...") screenshot_worker = ScreenshotWorker(spider) screenshot_thread = QThread() screenshot_worker.moveToThread(screenshot_thread) screenshot_thread.started.connect(screenshot_worker.start) screenshot_thread.finished.connect(screenshot_worker.stop) # 创建主窗口 logger.info("创建主窗口...") window = MainWindow(config, spider) window.show() # 连接截图信号(在创建window后连接) screenshot_worker.sse_screenshot_fetched.connect(window.update_sse_screenshot) screenshot_worker.error_occurred.connect(lambda msg: logger.error(msg)) # 连接信号 worker.status_update.connect(window.update_status) worker.analysis_finished.connect(window.update_indicator) worker.error_occurred.connect(lambda msg: window.show_message("错误", msg)) worker.stock_data_fetched.connect(window.add_waveform_data) worker.sse_screenshot_fetched.connect(window.update_sse_screenshot) # 启动时从数据库初始化指示器显示 worker._update_indicator() logger.info("初始化指示器显示完成") # 设置按钮回调 window.set_refresh_callback(worker.manual_refresh) window.set_config_callback(window.show_config) # 启动后台线程 worker_thread.start() logger.info("后台线程已启动") # 启动截图后台线程 screenshot_thread.start() logger.info("截图后台线程已启动") # 启动股票数据爬取定时器 stock_timer = QTimer() stock_timer.timeout.connect(worker.fetch_stock_data) stock_timer.start(60000) # 每分钟爬取一次股票数据 logger.info("股票数据爬取定时器已启动,间隔60秒") # 启动上证所截图爬取定时器(使用独立的截图线程,不再阻塞主循环) screenshot_timer = QTimer() screenshot_timer.timeout.connect(screenshot_worker.do_fetch) screenshot_timer.start(60000) # 每1分钟爬取一次截图 logger.info("上证所截图爬取定时器已启动,间隔60秒") # 确保应用退出时清理线程 def cleanup(): logger.info("清理资源,停止后台线程...") worker.stop() worker_thread.quit() worker_thread.wait() # 停止截图线程 screenshot_worker.stop() screenshot_thread.quit() screenshot_thread.wait() # 显示统计信息 logger.info("=== 程序运行统计 ===") logger.info(f"爬取网站次数: {worker.fetch_count}") logger.info(f"提交API分析次数: {worker.analysis_count}") logger.info("=== 统计结束 ===") # 写入统计信息到文件 try: stats_file = "statistics.txt" with open(stats_file, "a", encoding="utf-8") as f: f.write(f"=== 程序运行统计 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ===\n") f.write(f"爬取网站次数: {worker.fetch_count}\n") f.write(f"提交API分析次数: {worker.analysis_count}\n") f.write("=== 统计结束 ===\n\n") logger.info(f"统计信息已写入到文件: {stats_file}") except Exception as e: logger.error(f"写入统计文件失败: {str(e)}") logger.info("后台线程已停止") app.aboutToQuit.connect(cleanup) logger.info("应用启动完成,进入主循环") # 运行应用 sys.exit(app.exec()) if __name__ == "__main__": main()