Open "SetupInfo.txt" For Input As #1

Private Sub Form_Load()
Dim Lstr As String
'Dim b As Byte
Open "SetupInfo.txt" For Input As #1
Do While Not EOF(1)
Line Input #1, Lstr
'Text4.Text = Text4.tex & Lstr & vbCrLf
'b = Lstr(0) 'Print b
If Left(Lstr, 1) = "!" Then Exit Do
If Lstr = "" Or Left(Lstr, 1) = " " Or Left(Lstr, 1) = Chr(9) Or Left(Lstr, 1) = ";" Then GoTo MyTrap
Debug.Print Lstr
If Lstr = "#" Then

Else

End If
MyTrap:
Loop
Close #1


End Sub

import sys,os from PyQt5.QtCore import * from PyQt5.QtWidgets import * from PyQt5.QtGui import * import cv2 from ultralytics import YOLO from collections import Counter import logging class MainWindow(QMainWindow): def __init__(self): super().__init__() self.xiaolian_ui() self.con=0.25 self.current_annotated_image = None self.detection_type = None self.result = None def xiaolian_ui(self): self.setWindowTitle("@author:笑脸惹桃花") self.setWindowIcon(QIcon("icon.png")) self.setFixedSize(1380, 590) # 创建主左右布局 main_layout = QHBoxLayout() left_panel = QVBoxLayout() left_panel.setContentsMargins(10, 20, 10, 20) # 增加上下边距 left_panel.setSpacing(15) # 垂直排列的功能按钮 button_layout = QVBoxLayout() button_layout.setSpacing(12) # 设置按钮间距 # 创建并添加所有按钮 self.load_model_button = QPushButton("📁 模型选择") self.image_detect_button = QPushButton("🖼️ 图片检测") self.folder_detect_button = QPushButton("📂 文件夹检测") self.save_button = QPushButton("💾 保存结果") self.exit_button = QPushButton("❌ 退出程序") # 统一设置按钮属性 for btn in [self.load_model_button, self.image_detect_button, self.folder_detect_button, self.save_button, self.exit_button]: btn.setFixedSize(145, 35) btn.setCursor(Qt.PointingHandCursor) button_layout.addWidget(btn) # 添加按钮组与信息框之间的伸缩空间 button_layout.addStretch(1) # 信息输出框 status_group = QGroupBox("检测信息") status_group.setStyleSheet(""" QGroupBox { border: none; background: rgba(245, 245, 245, 0.9); border-radius: 8px; padding: 10px; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; color: #2980b9; font: bold 14px "Segoe UI"; background: transparent; } """) status_layout = QVBoxLayout() self.output_text = QTextEdit(self) self.output_text.setReadOnly(True) self.output_text.setStyleSheet(""" background: white; border: 1px solid #dcdcdc; border-radius: 5px; padding: 8px; font: 13px "Segoe UI"; color: #2c3e50; min-height: 150px; """) status_layout.addWidget(self.output_text) status_group.setLayout(status_layout) status_layout.addStretch() # 组装左侧面板 left_panel.addLayout(button_layout) left_panel.addWidget(status_group) # 右侧视频显示区域 (宽度占比3/4) right_panel = QHBoxLayout() right_panel.setContentsMargins(10, 0, 10, 0) # 创建图像显示区域 self.label1 = QLabel() self.label1.setAlignment(Qt.AlignCenter) self.label1.setMinimumSize(580, 560) self.label1.setMaximumSize(580, 560) self.label1.setStyleSheet(''' border:3px solid #6950a1; background-color: black; border-radius: 8px; ''') self.label2 = QLabel() self.label2.setAlignment(Qt.AlignCenter) self.label2.setMinimumSize(580, 560) self.label2.setMaximumSize(580, 560) self.label2.setStyleSheet(''' border:3px solid #6950a1; background-color: black; border-radius: 8px; ''') # 添加图像标签并设置间距 right_panel.addWidget(self.label1) right_panel.addSpacing(15) # 两个显示区域之间的间距 right_panel.addWidget(self.label2) # 设置主布局比例 main_layout.addLayout(left_panel, 1) main_layout.addLayout(right_panel, 3) # 设置中心部件 central_widget = QWidget() central_widget.setLayout(main_layout) self.setCentralWidget(central_widget) # 连接信号槽 self.load_model_button.clicked.connect(self.load_model) self.image_detect_button.clicked.connect(self.select_image) self.folder_detect_button.clicked.connect(self.detect_folder) self.save_button.clicked.connect(self.save_detection) self.exit_button.clicked.connect(self.exit_application) # 增强按钮样式 button_style = """ QPushButton { background-color: #f0f0f0; border: 2px solid #d0d0d0; border-radius: 6px; color: #404040; font: bold 13px "Microsoft YaHei"; padding: 8px 12px; text-align: center; transition: all 0.3s; } QPushButton:hover { background-color: #e0e0e0; border-color: #c0c0c0; color: #202020; } QPushButton:pressed { background-color: #d0d0d0; border-color: #b0b0b0; } """ for btn in [self.load_model_button, self.image_detect_button, self.folder_detect_button, self.save_button, self.exit_button]: btn.setStyleSheet(button_style) def save_detection(self): detection_type = self.detection_type if detection_type == "image": self.save_detection_results() def save_detection_results(self): if self.current_annotated_image is not None: self.save_image(self.current_annotated_image) def detect_folder(self): self.label1.clear() self.label2.clear() folder_path = QFileDialog.getExistingDirectory(self, "选择图片文件夹") self.flag = 1 if folder_path: image_paths = [] for filename in os.listdir(folder_path): if filename.lower().endswith((".jpg", ".jpeg", ".png")): image_path = os.path.join(folder_path, filename) image_paths.append(image_path) for image_path in image_paths: self.detect_image(image_path) def save_image(self, image): if image is not None: file_name, _ = QFileDialog.getSaveFileName(None, "保存图片", "", "JPEG (*.jpg);;PNG (*.png);;All Files (*)") if file_name: cv2.imwrite(file_name, image) def select_image(self): image_path, _ = QFileDialog.getOpenFileName(None, "选择图片文件", "", "图片文件 (*.jpg *.jpeg *.png)") self.flag = 0 self.detect_image(image_path) def detect_image(self,image_path): if image_path: image = cv2.imread(image_path) if image is not None: if self.flag == 0: results = self.model.predict(image) elif self.flag == 1: results = self.model.predict(image_path, save=True) self.detection_type = "image" if results: annotated_image = results[0].plot() image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) #转换为 RGB height1, width1, channel1 = image_rgb.shape bytesPerLine1 = 3 * width1 qimage1 = QImage(image_rgb.data, width1, height1, bytesPerLine1, QImage.Format_RGB888) pixmap1 = QPixmap.fromImage(qimage1) self.label1.setPixmap(pixmap1.scaled(self.label1.size(), Qt.KeepAspectRatio)) annotated_image = cv2.cvtColor(annotated_image, cv2.COLOR_BGR2RGB) #转换为 RGB height2, width2, channel2 = annotated_image.shape bytesPerLine2 = 3 * width2 qimage2 = QImage(annotated_image.data, width2, height2, bytesPerLine2, QImage.Format_RGB888) pixmap2 = QPixmap.fromImage(qimage2) self.label2.setPixmap(pixmap2.scaled(self.label2.size(), Qt.KeepAspectRatio)) self.result = results self.current_annotated_image = results[0].plot() self.display_statistics() cv2.waitKey(300) # 修改图片切换时间 def stat(self): detected_classes = [] target_range = {0, 1} # 修改为自己的类别 if self.result == None: return None for r in self.result: classes = r.boxes.cls.cpu().numpy().astype(int).tolist() # 筛选出在目标范围内的类别,并去重 detected_classes.extend([cls for cls in classes if cls in target_range]) class_counts = Counter(detected_classes) class_counts_dic = dict(class_counts) return class_counts_dic def display_statistics(self): class_counts = self.stat() if class_counts == None: self.output_text.setText('') return # 修改class_labels为自己的类别对应关系,可中文 class_labels = { 0: "helmet", 1: "vest" } # 构建输出字符串 output_string = "" for class_id, count in class_counts.items(): label = class_labels.get(class_id, f"类别{class_id}") # 如果没有找到标签,则使用默认标签 output_string += f"{label}: {count} 个\n" self.output_text.setText(output_string) def load_model(self): model_path, _ = QFileDialog.getOpenFileName(None, "选择模型文件", "", "模型文件 (*.pt)") if model_path: self.model = YOLO(model_path) def exit_application(self): sys.exit() class LoginDialog(QDialog): def __init__(self): super().__init__() self.setWindowTitle("@author:笑脸惹桃花") self.setWindowIcon(QIcon("icon.png")) self.setWindowFlags(Qt.WindowCloseButtonHint) self.setFixedSize(500, 800) self.setup_ui() def setup_ui(self): main_layout = QVBoxLayout() main_layout.setContentsMargins(40, 30, 40, 40) # 增加边距 main_layout.setSpacing(30) logo_frame = QFrame() logo_layout = QHBoxLayout(logo_frame) logo_layout.setContentsMargins(0, 0, 0, 0) logo_label = QLabel() logo_pixmap = QPixmap("icon.png").scaled(150, 150, Qt.KeepAspectRatio, Qt.SmoothTransformation) logo_label.setPixmap(logo_pixmap) logo_layout.addWidget(logo_label, 0, Qt.AlignCenter) main_layout.addWidget(logo_frame) title_label = QLabel("@author:笑脸惹桃花") title_label.setFont(QFont("微软雅黑", 24, QFont.Bold)) title_label.setStyleSheet("color: #2c3e50;") title_label.setAlignment(Qt.AlignCenter) main_layout.addWidget(title_label) form_frame = QFrame() form_layout = QVBoxLayout(form_frame) form_layout.setContentsMargins(20, 20, 20, 20) form_layout.setSpacing(25) username_layout = QHBoxLayout() username_label = QLabel("账号:") username_label.setFont(QFont("微软雅黑", 14)) username_label.setStyleSheet("color: #2c3e50;") username_layout.addWidget(username_label) self.username_input = QLineEdit() self.username_input.setPlaceholderText("请输入用户名") self.username_input.setFont(QFont("微软雅黑", 14)) self.username_input.setStyleSheet( "QLineEdit { padding: 12px; border-radius: 6px; border: 2px solid #bdc3c7; }" "QLineEdit:focus { border-color: #3498db; }" ) username_layout.addWidget(self.username_input) form_layout.addLayout(username_layout) password_layout = QHBoxLayout() password_label = QLabel("密码:") password_label.setFont(QFont("微软雅黑", 14)) password_label.setStyleSheet("color: #2c3e50;") password_layout.addWidget(password_label) self.password_input = QLineEdit() self.password_input.setPlaceholderText("请输入密码") self.password_input.setEchoMode(QLineEdit.Password) self.password_input.setFont(QFont("微软雅黑", 14)) self.password_input.setStyleSheet( "QLineEdit { padding: 12px; border-radius: 6px; border: 2px solid #bdc3c7; }" "QLineEdit:focus { border-color: #3498db; }" ) password_layout.addWidget(self.password_input) form_layout.addLayout(password_layout) main_layout.addWidget(form_frame) button_frame = QFrame() button_layout = QHBoxLayout(button_frame) button_layout.setContentsMargins(0, 0, 0, 0) button_layout.setSpacing(15) login_button = QPushButton("登 录") login_button.setFont(QFont("微软雅黑", 14, QFont.Bold)) login_button.clicked.connect(self.handle_login) login_button.setFixedSize(120, 45) login_button.setStyleSheet(""" QPushButton { background-color: #3498db; color: white; border-radius: 8px; border: none; } QPushButton:hover { background-color: #2980b9; } QPushButton:pressed { background-color: #1d6fa5; } """) register_button = QPushButton("注 册") register_button.setFont(QFont("微软雅黑", 14, QFont.Bold)) register_button.clicked.connect(self.handle_register) register_button.setFixedSize(120, 45) register_button.setStyleSheet(""" QPushButton { background-color: #2ecc71; color: white; border-radius: 8px; border: none; } QPushButton:hover { background-color: #27ae60; } QPushButton:pressed { background-color: #219a52; } """) exit_button = QPushButton("退 出") exit_button.setFont(QFont("微软雅黑", 14, QFont.Bold)) exit_button.clicked.connect(self.close) exit_button.setFixedSize(120, 45) exit_button.setStyleSheet(""" QPushButton { background-color: #e74c3c; color: white; border-radius: 8px; border: none; } QPushButton:hover { background-color: #c0392b; } QPushButton:pressed { background-color: #a93226; } """) button_layout.addWidget(login_button) button_layout.addWidget(register_button) button_layout.addWidget(exit_button) main_layout.addWidget(button_frame, 0, Qt.AlignCenter) hint_label = QLabel("提示:请使用您的用户名和密码登录") hint_label.setFont(QFont("微软雅黑", 10)) hint_label.setStyleSheet("color: #95a5a6;") hint_label.setAlignment(Qt.AlignCenter) main_layout.addWidget(hint_label) self.setLayout(main_layout) def load_credentials(self): credentials = {} try: with open("user.txt", "r", encoding="utf-8") as file: for line in file: # 分割每一行来获取用户名和密码 username, password = line.strip().split("=") credentials[username] = password return credentials except FileNotFoundError: logging.error("未找到凭证文件") return {} except Exception as e: logging.error(f"加载凭证时出错: {str(e)}") return {} def handle_login(self): credentials = self.load_credentials() username = self.username_input.text().strip() password = self.password_input.text().strip() if username in credentials and credentials[username] == password: logging.info(f"用户 {username} 登录成功") QMessageBox.information(self, "成功", "登录成功!") self.accept() else: logging.warning(f"用户 {username} 登录失败") QMessageBox.warning(self, "错误", "用户名或密码错误!") self.password_input.clear() def handle_register(self): credentials = self.load_credentials() username = self.username_input.text().strip() password = self.password_input.text().strip() if not username or not password: logging.warning("注册失败:用户名或密码为空") QMessageBox.warning(self, "错误", "用户名和密码不能为空!") return if username in credentials: logging.warning(f"注册失败:用户名 {username} 已存在") QMessageBox.warning(self, "错误", "用户名已存在!") return credentials[username] = password try: with open("user.txt", "w", encoding="utf-8") as file: for uname, pwd in credentials.items(): file.write(f"{uname}={pwd}\n") logging.info(f"用户 {username} 注册成功") QMessageBox.information(self, "成功", "注册成功!请登录。") self.username_input.clear() self.password_input.clear() except Exception as e: logging.error(f"保存凭证时出错: {str(e)}") QMessageBox.critical(self, "错误", f"保存凭证时出错: {str(e)}") if __name__ == '__main__': app = QApplication(sys.argv) login_dialog = LoginDialog() if login_dialog.exec_() == QDialog.Accepted: window = MainWindow() window.show() sys.exit(app.exec_());这个代码的功能是什么
06-28
现在有个问题,我选择账号1进行抖音数据采集视频列表,然后切换平台的小红书的时候,发现账号1在转圈采集,好像显示抖音账号1和小红书账号1显示有点重叠,账号1-账号6都检查一下,切换平台的时候,不是要暂停采集数据,是放到后台采集数据,account_widget.py的from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, QFrame, QGridLayout, QMessageBox, QHeaderView, QAbstractItemView, QApplication, QMenu, QFileDialog, QStyle, QProgressBar,QComboBox ) from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer from PyQt6.QtGui import QFont, QColor from tanchen_v2.core_v1.collector_threads import DouyinDataCollectorThread from tanchen_v2.ui.comment_window import CommentWindow from tanchen_v2.core_v1.loading_indicator import LoadingIndicator from tanchen_v2.core_v1.config_manager import ConfigManager import pandas as pd import json import os from tanchen_v2.core_v1.log_tc import logger class AccountWidget(QWidget): def __init__(self, index, config_manager,platform:str="douyin"): super().__init__() self.index = index self.account_index = index self.data = [] self.current_platform = platform #默认平台 self.platform = platform self.platform_configs=config_manager self.platform_data={}# 按平台存储数据:{"douyin":data,"xiaohongshu":data} self.config_manager = config_manager self.is_editing = True self.collector_thread = None self.comment_window = None self.init_ui() self.load_config() def init_ui(self): main_layout = QVBoxLayout() # 平台选择器 - 移到账号标题上方 # platform_layout = QHBoxLayout() # platform_layout.addWidget(QLabel("选择平台:")) # # self.platform_combo = QComboBox() # self.platform_combo.addItem("抖音", "douyin") # self.platform_combo.addItem("小红书", "xiaohongshu") # self.platform_combo.currentIndexChanged.connect(self.switch_platform) # platform_layout.addWidget(self.platform_combo) # platform_layout.addStretch() # main_layout.addLayout(platform_layout) # 平台选择器放在最上方 # 账号标题 title_layout = self.create_title_layout() main_layout.addLayout(title_layout) # 配置区域 config_layout = self.create_config_layout() main_layout.addLayout(config_layout) # 控制按钮 btn_layout = self.create_button_layout() main_layout.addLayout(btn_layout) # 表格容器 self.table_container = self.create_table_container() main_layout.addWidget(self.table_container, 1) # 状态栏 self.status_label = QLabel("就绪") self.status_label.setStyleSheet(""" QLabel { color: #666; font-size: 12px; padding: 5px; border-top: 1px solid #e0e0e0; } """) main_layout.addWidget(self.status_label) self.setLayout(main_layout) self.set_edit_mode(True) def update_platform(self,platform): """更新账号平台并保留配置""" # 保存当前平台配置 self.platform_configs[self.platform] = self.get_current_config() # 更新平台 self.platform = platform self.platform_config_group.setTitle(f"{platform}配置") # 加载新平台配置 self.update_platform_config_ui() def create_title_layout(self): layout = QHBoxLayout() self.title_label = QLabel(f"账号 {self.index + 1}") self.title_label.setFont(QFont("Arial", 12, QFont.Weight.Bold)) layout.addWidget(self.title_label) layout.addWidget(QLabel("备注:")) self.remark_input = QLineEdit() self.remark_input.setPlaceholderText("账号备注...") self.remark_input.setMaximumWidth(200) layout.addWidget(self.remark_input) self.edit_btn = self.create_icon_button("编辑配置", QStyle.StandardPixmap.SP_FileDialogDetailedView, "#FFC107", "#FFA000", "#FF8F00") self.edit_btn.clicked.connect(self.toggle_edit_mode) layout.addWidget(self.edit_btn) self.save_btn = self.create_icon_button("保存配置", QStyle.StandardPixmap.SP_DialogSaveButton, "#4CAF50", "#388E3C", "#2E7D32") self.save_btn.clicked.connect(self.save_config) layout.addWidget(self.save_btn) layout.addStretch() return layout def create_config_layout(self): layout = QGridLayout() layout.setColumnStretch(1, 1) # 关键字输入 layout.addWidget(QLabel("关键字:"), 0, 0) self.keyword_input = QLineEdit() self.keyword_input.setPlaceholderText("输入搜索关键字...") layout.addWidget(self.keyword_input, 0, 1) # Cookie输入 layout.addWidget(QLabel("Cookie:"), 1, 0) self.cookie_input = QLineEdit() self.cookie_input.setPlaceholderText("输入账号Cookie...") self.cookie_input.setEchoMode(QLineEdit.EchoMode.Password) layout.addWidget(self.cookie_input, 1, 1) # 显示/隐藏Cookie按钮 self.toggle_cookie_btn = QPushButton("显示") self.toggle_cookie_btn.setMaximumWidth(60) self.toggle_cookie_btn.setStyleSheet("QPushButton { padding: 3px; border-radius: 3px; }") self.toggle_cookie_btn.clicked.connect(self.toggle_cookie_visibility) layout.addWidget(self.toggle_cookie_btn, 1, 2) # 评论关键字输入 layout.addWidget(QLabel("评论关键字:"), 2, 0) self.comment_keyword_input = QLineEdit() self.comment_keyword_input.setPlaceholderText("输入评论关键字过滤...支持中文逗号、英文逗号、空格例如:测试1,测试2") layout.addWidget(self.comment_keyword_input, 2, 1) return layout def create_button_layout(self): layout = QHBoxLayout() # 开始采集按钮 self.start_btn = self.create_action_button("开始采集", QStyle.StandardPixmap.SP_MediaPlay, "#4CAF50", "#388E3C", "#2E7D32") self.start_btn.clicked.connect(self.start_collecting) layout.addWidget(self.start_btn) # 停止采集按钮 self.stop_btn = self.create_action_button("停止采集", QStyle.StandardPixmap.SP_MediaStop, "#f44336", "#D32F2F", "#B71C1C") self.stop_btn.clicked.connect(self.stop_collecting) self.stop_btn.setEnabled(False) layout.addWidget(self.stop_btn) # 获取评论按钮 self.get_comments_btn = self.create_action_button("获取评论", QStyle.StandardPixmap.SP_MessageBoxInformation, "#9C27B0", "#7B1FA2", "#4A148C") self.get_comments_btn.clicked.connect(self.get_comments) layout.addWidget(self.get_comments_btn) # 导出数据按钮 self.export_btn = self.create_action_button("导出数据", QStyle.StandardPixmap.SP_DialogSaveButton, "#2196F3", "#1976D2", "#0D47A1") self.export_btn.clicked.connect(self.export_data) layout.addWidget(self.export_btn) # 清空表格按钮 self.clear_table_btn = self.create_action_button("清空表格", QStyle.StandardPixmap.SP_TrashIcon, "#FF9800", "#F57C00", "#EF6C00") self.clear_table_btn.clicked.connect(self.clear_table_data) layout.addWidget(self.clear_table_btn) layout.addStretch() return layout def setup_table_columns(self,platform): """根据平台设置表格列""" if platform == "douyin": self.table.setColumnCount(7) self.table.setHorizontalHeaderLabels( ["序号", "作者昵称", "类型", "视频地址", "评论数量", "发布时间", "视频ID"]) # 设置列宽 self.table.setColumnWidth(0, 60) self.table.setColumnWidth(1, 150) self.table.setColumnWidth(2, 100) self.table.setColumnWidth(3, 300) self.table.setColumnWidth(4, 100) self.table.setColumnWidth(5, 150) self.table.setColumnWidth(6, 200) elif platform == "xiaohongshu": self.table.setColumnCount(8) self.table.setHorizontalHeaderLabels( ["序号", "作者昵称", "类型", "笔记链接", "点赞数", "收藏数", "发布时间", "笔记ID"]) # 设置列宽 self.table.setColumnWidth(0, 60) self.table.setColumnWidth(1, 150) self.table.setColumnWidth(2, 100) self.table.setColumnWidth(3, 300) self.table.setColumnWidth(4, 80) self.table.setColumnWidth(5, 80) self.table.setColumnWidth(6, 150) self.table.setColumnWidth(7, 200) # 添加新平台时只需在这里添加新的列配置 def create_table_container(self): container = QFrame() container.setFrameShape(QFrame.Shape.StyledPanel) container.setStyleSheet("background-color: white; border: 1px solid #E0E0E0; border-radius: 4px;") layout = QVBoxLayout(container) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # v2数据表格 self.table = QTableWidget() self.setup_table_columns(self.current_platform) # 根据平台初始化表格列 self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive) self.table.horizontalHeader().setSectionsMovable(True) self.table.verticalHeader().setVisible(False) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.table.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.table.setSortingEnabled(True) self.table.setStyleSheet(""" QTableWidget { background-color: white; alternate-background-color: #f9f9f9; gridline-color: #e0e0e0; } QHeaderView::section { background-color: #f0f0f0; padding: 4px; border: 1px solid #e0e0e0; font-weight: bold; } """) self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.table.customContextMenuRequested.connect(self.show_context_menu) layout.addWidget(self.table) # 覆盖层用于显示加载指示器 self.overlay = QWidget(container) self.overlay.setGeometry(0, 0, container.width(), container.height()) self.overlay.setStyleSheet("background-color: rgba(255, 255, 255, 0.7);") overlay_layout = QVBoxLayout(self.overlay) overlay_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) self.loading_indicator = LoadingIndicator() overlay_layout.addWidget(self.loading_indicator) self.overlay.hide() return container def create_icon_button(self, text, icon, base_color, hover_color, pressed_color): button = QPushButton(text) button.setIcon(self.style().standardIcon(icon)) text_color = "black" if base_color == "#FFC107" else "white" button.setStyleSheet(f""" QPushButton {{ background-color: {base_color}; color: {text_color}; padding: 5px; border-radius: 4px; }} QPushButton:hover {{ background-color: {hover_color}; }} QPushButton:pressed {{ background-color: {pressed_color}; }} """) return button def create_action_button(self, text, icon, base_color, hover_color, pressed_color): button = QPushButton(text) button.setIcon(self.style().standardIcon(icon)) button.setStyleSheet(f""" QPushButton {{ background-color: {base_color}; color: white; padding: 8px 16px; border-radius: 5px; font-weight: bold; }} QPushButton:hover {{ background-color: {hover_color}; }} QPushButton:pressed {{ background-color: {pressed_color}; }} QPushButton:disabled {{ background-color: {base_color}80; }} """) return button def toggle_cookie_visibility(self): if self.cookie_input.echoMode() == QLineEdit.EchoMode.Password: self.cookie_input.setEchoMode(QLineEdit.EchoMode.Normal) self.toggle_cookie_btn.setText("隐藏") else: self.cookie_input.setEchoMode(QLineEdit.EchoMode.Password) self.toggle_cookie_btn.setText("显示") def toggle_edit_mode(self): self.set_edit_mode(not self.is_editing) def set_edit_mode(self, edit_mode): self.is_editing = edit_mode self.remark_input.setReadOnly(not edit_mode) self.keyword_input.setReadOnly(not edit_mode) self.cookie_input.setReadOnly(not edit_mode) self.edit_btn.setVisible(not edit_mode) self.save_btn.setVisible(edit_mode) bg_color = "#f5f5f5" if not edit_mode else "white" self.remark_input.setStyleSheet(f"background-color: {bg_color}; border: 1px solid #e0e0e0; border-radius: 3px;") self.keyword_input.setStyleSheet( f"background-color: {bg_color}; border: 1px solid #e0e0e0; border-radius: 3px;") self.cookie_input.setStyleSheet(f"background-color: {bg_color}; border: 1px solid #e0e0e0; border-radius: 3px;") if edit_mode: self.status_label.setText("编辑模式:可以修改配置") else: self.status_label.setText("配置已锁定,点击'编辑配置'修改") def switch_platform(self,new_platform): """切换平台时的处理""" # 保存当前平台的数据 self.save_current_data() # 更新当前平台 self.current_platform = new_platform # 更新表格列 self.setup_table_columns(new_platform) # 加载新平台的配置 self.load_config() # 加载新平台的数据 self.load_platform_data() # 更新状态栏 self.status_label.setText(f"已切换到{new_platform}平台") def save_current_data(self): """保存当前平台的数据到内存""" # 从表格获取当前数据 data = [] for row in range(self.table.rowCount()): row_data = {} for col in range(self.table.columnCount()): item = self.table.item(row, col) if item: header = self.table.horizontalHeaderItem(col).text() row_data[header] = item.text() data.append(row_data) # 保存到平台数据字典 self.platform_data[self.current_platform] = data def load_platform_data(self): """加载当前平台的数据到表格""" data = self.platform_data.get(self.current_platform, []) # 清空表格 self.table.setRowCount(0) if not data: self.status_label.setText(f"{self.current_platform}平台无数据") return # 填充表格 self.table.setRowCount(len(data)) for row, item in enumerate(data): for col in range(self.table.columnCount()): header = self.table.horizontalHeaderItem(col).text() value = item.get(header, "") table_item = QTableWidgetItem(value) table_item.setFlags(Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled) self.table.setItem(row, col, table_item) self.status_label.setText(f"已加载{len(data)}条{self.current_platform}数据") def save_config(self): #v2 self.set_edit_mode(False) # 保存配置时按平台存储 config = { "remark": self.remark_input.text(), "keyword": self.keyword_input.text(), "cookie": self.cookie_input.text(), "comment_keyword": self.comment_keyword_input.text(), "platform": self.current_platform # 保存当前平台 } # 使用平台特定的键保存配置 self.config_manager.save_account_config(self.index, config, self.current_platform) self.status_label.setText("配置已保存") self.save_btn.setStyleSheet(""" QPushButton { background-color: #2E7D32; color: white; padding: 5px; border-radius: 4px; } """) QTimer.singleShot(500, self.restore_save_button_style) def restore_save_button_style(self): self.save_btn.setStyleSheet(""" QPushButton { background-color: #4CAF50; color: white; padding: 5px; border-radius: 4px; } QPushButton:hover { background-color: #388E3C; } QPushButton:pressed { background-color: #2E7D32; } """) def load_config(self): # 加载当前平台的配置 config = self.config_manager.load_account_config(self.index, self.current_platform) if config: self.remark_input.setText(config.get("remark", "")) self.keyword_input.setText(config.get("keyword", "")) self.cookie_input.setText(config.get("cookie", "")) self.comment_keyword_input.setText(config.get("comment_keyword", "")) logger.debug(f"平台:{self.current_platform}--账号{self.index + 1}加载配置成功") logger.debug(f"平台:{self.current_platform}--账号{self.index + 1}加载配置:{str(config)}") else: # 如果没有配置,初始化空值 self.remark_input.setText("") self.keyword_input.setText("") self.cookie_input.setText("") self.comment_keyword_input.setText("") logger.debug(f"平台:{ self.current_platform}--账号{self.index + 1}无配置,使用默认值") logger.debug(f"平台:{self.current_platform}--账号{self.index + 1}无配置, 加载配置:{str(config)}") def start_collecting(self): keyword = self.keyword_input.text().strip() cookie = self.cookie_input.text().strip() if not keyword: QMessageBox.warning(self, "输入错误", "请输入搜索关键字") return if not cookie: QMessageBox.warning(self, "输入错误", "请输入账号Cookie") return if self.collector_thread and self.collector_thread.isRunning(): self.collector_thread.stop() self.collector_thread.quit() self.collector_thread.wait(1000) self.overlay.show() self.loading_indicator.start() self.start_btn.setStyleSheet(""" QPushButton { background-color: #2E7D32; color: white; padding: 8px 16px; border-radius: 5px; font-weight: bold; } """) # 使用爬虫工厂创建对应平台的爬虫线程 try: from core.Crawler.crawler_factory import CrawlerFactory self.collector_thread = CrawlerFactory.create_crawler( platform=self.current_platform, account_index=self.index, config={ "keyword": keyword, "cookie": cookie } ) # 设置关键字 self.collector_thread.keyword = keyword except Exception as e: logger.warning(f"创建爬虫失败: {str(e)},{str(self.current_platform)}") self.handle_error(f"创建爬虫失败: {str(e)}") self.overlay.hide() self.loading_indicator.stop() return try: self.collector_thread.data_collected.connect(self.update_data) self.collector_thread.status_updated.connect(self.update_status) self.collector_thread.error_occurred.connect(self.handle_error) self.collector_thread.finished.connect(self.on_collection_finished) self.collector_thread.start(keyword) self.start_btn.setEnabled(False) self.stop_btn.setEnabled(True) self.keyword_input.setEnabled(False) self.cookie_input.setEnabled(False) self.status_label.setText(f"开始采集{self.current_platform}: {keyword}") self.status_label.setStyleSheet("color: #4CAF50; font-weight: bold;") except Exception as e: logger.warning(f"启动爬虫失败: {str(e)}") self.handle_error(f"启动爬虫失败: {str(e)}") def handle_error(self, error_message): self.status_label.setText(error_message) self.status_label.setStyleSheet("color: #f44336; font-weight: bold;") #self.stop_collecting() def update_status(self, message): self.status_label.setText(message) self.status_label.setStyleSheet("color: #4CAF50; font-weight: bold;") def on_collection_finished(self): self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False) self.keyword_input.setEnabled(True) self.cookie_input.setEnabled(True) self.overlay.hide() self.loading_indicator.stop() self.restore_start_button_style() def restore_start_button_style(self): self.start_btn.setStyleSheet(""" QPushButton { background-color: #4CAF50; color: white; padding: 8px 16px; border-radius: 5px; font-weight: bold; } QPushButton:hover { background-color: #388E3C; } QPushButton:pressed { background-color: #2E7D32; } QPushButton:disabled { background-color: #81C784; } """) def stop_collecting(self): if self.collector_thread and self.collector_thread.isRunning(): self.collector_thread.stop() self.collector_thread.quit() self.collector_thread.wait(1000) self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False) self.keyword_input.setEnabled(True) self.cookie_input.setEnabled(True) self.overlay.hide() self.loading_indicator.stop() self.restore_start_button_style() self.status_label.setText("采集已停止") self.status_label.setStyleSheet("color: #f44336; font-weight: bold;") def remove_duplicates(self, data): seen_urls = set() unique_data = [] url_field = "视频地址" if self.current_platform == "douyin" else "笔记链接" for item in data: item_url = item.get(url_field, "") if item_url and item_url not in seen_urls: seen_urls.add(item_url) unique_data.append(item) return unique_data def update_data(self, new_data): logger.debug("new_data",new_data) if not new_data or not isinstance(new_data, list): QMessageBox.warning(self, "数据错误", "接收到无效的数据格式") return if new_data and "状态" in new_data[0] and new_data[0]["状态"] == "错误": error_msg = new_data[0].get("消息", "未知错误") QMessageBox.warning(self, "采集错误", error_msg) self.stop_collecting() return unique_data = self.remove_duplicates(new_data) # 保存到当前平台的数据 self.platform_data[self.current_platform] = unique_data # 更新表格 self.table.setSortingEnabled(False) self.table.setRowCount(len(unique_data)) flags = Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled for row, item in enumerate(unique_data): # 根据平台动态填充表格 if self.current_platform == "douyin": # 抖音数据处理 self.table.setItem(row, 0, QTableWidgetItem(str(item.get("序号", "")))) self.table.setItem(row, 1, QTableWidgetItem(item.get("作者昵称", ""))) self.table.setItem(row, 2, QTableWidgetItem(item.get("类型", ""))) video_url = item.get("视频地址", "") url_item = QTableWidgetItem(video_url) url_item.setForeground(QColor("#0000FF")) url_item.setToolTip(video_url) self.table.setItem(row, 3, url_item) self.table.setItem(row, 4, QTableWidgetItem(str(item.get("评论数量", 0)))) self.table.setItem(row, 5, QTableWidgetItem(item.get("发布时间", ""))) self.table.setItem(row, 6, QTableWidgetItem(item.get("视频ID", ""))) elif self.current_platform == "xiaohongshu": # 小红书数据处理 self.table.setItem(row, 0, QTableWidgetItem(str(item.get("序号", "")))) self.table.setItem(row, 1, QTableWidgetItem(item.get("作者昵称", ""))) self.table.setItem(row, 2, QTableWidgetItem(item.get("类型", ""))) note_url = item.get("笔记链接", "") url_item = QTableWidgetItem(note_url) url_item.setForeground(QColor("#0000FF")) url_item.setToolTip(note_url) self.table.setItem(row, 3, url_item) self.table.setItem(row, 4, QTableWidgetItem(str(item.get("点赞数", 0)))) self.table.setItem(row, 5, QTableWidgetItem(str(item.get("收藏数", 0)))) self.table.setItem(row, 6, QTableWidgetItem(item.get("发布时间", ""))) self.table.setItem(row, 7, QTableWidgetItem(item.get("笔记ID", ""))) # 添加新平台时在这里添加处理逻辑 self.table.setSortingEnabled(True) self.table.scrollToBottom() self.status_label.setText(f"已采集 {len(unique_data)} 条{self.current_platform}数据") self.status_label.setStyleSheet("color: #4CAF50; font-weight: bold;") def export_data(self): # 获取当前平台的数据 current_data = self.platform_data.get(self.current_platform, []) if not current_data: QMessageBox.warning(self, "导出失败", f"没有可导出的{self.current_platform}数据") return try: remark = self.remark_input.text().strip() platform_name = self.current_platform default_name = f"{platform_name}数据_账号{self.index + 1}_{remark}.xlsx" if remark else f"{platform_name}数据_账号{self.index + 1}.xlsx" filename, _ = QFileDialog.getSaveFileName( self, "导出数据", default_name, "Excel文件 (*.xlsx)" ) if not filename: return if not filename.endswith('.xlsx'): filename += '.xlsx' df = pd.DataFrame(current_data) df.to_excel(filename, index=False) self.export_btn.setStyleSheet(""" QPushButton { background-color: #0D47A1; color: white; padding: 8px 16px; border-radius: 5px; font-weight: bold; } """) QTimer.singleShot(500, self.restore_export_button_style) QMessageBox.information(self, "导出成功", f"数据已导出到: {filename}") except Exception as e: QMessageBox.critical(self, "导出错误", f"导出失败: {str(e)}") def restore_export_button_style(self): self.export_btn.setStyleSheet(""" QPushButton { background-color: #2196F3; color: white; padding: 8px 16px; border-radius: 5px; font-weight: bold; } QPushButton:hover { background-color: #1976D2; } QPushButton:pressed { background-color: #0D47A1; } """) def clear_table_data(self): # 获取当前平台的数据 current_data = self.platform_data.get(self.current_platform, []) if not current_data: QMessageBox.information(self, "提示", f"{self.current_platform}平台无数据") return reply = QMessageBox.question( self, "确认清空", f"确定要清空{self.current_platform}的表格数据吗? (共{len(current_data)}条)", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: self.platform_data[self.current_platform] = [] self.table.setRowCount(0) self.status_label.setText(f"{self.current_platform}数据已清空") def get_comments(self): """修改:处理多个关键字""" # 获取当前平台的数据 current_data = self.platform_data.get(self.current_platform, []) if not current_data: QMessageBox.warning(self, "操作失败", f"没有可用的{self.current_platform}数据") return # 获取关键字并分割 keyword_input = self.comment_keyword_input.text().strip() if keyword_input: # 使用 | 作为分隔符,支持中文逗号、英文逗号、空格 keywords = keyword_input.replace(',', ',').replace(' ', ',') keyword_filter = '|'.join([kw.strip() for kw in keywords.split(',') if kw.strip()]) else: keyword_filter = "" # 构建有效的视频数据 video_data = [] for idx, item in enumerate(current_data): try: # 根据平台获取不同的字段 if self.current_platform == "douyin": video_id = self.extract_douyin_video_id(item.get("视频地址", "")) comment_count = int(item.get("评论数量", 0)) video_url = item.get("视频地址", "") elif self.current_platform == "xiaohongshu": video_id = self.extract_xiaohongshu_note_id(item.get("笔记链接", "")) comment_count = int(item.get("评论数量", 0)) # 假设小红书也有评论数量字段 video_url = item.get("笔记链接", "") else: continue if video_id and comment_count > 0: video_data.append({ "序号": idx + 1, "视频地址": video_url, "视频ID": video_id, "评论数量": comment_count }) except (ValueError, TypeError, KeyError): continue if not video_data: QMessageBox.warning(self, "操作失败", "没有包含评论的视频数据") return try: # 获取当前账号的cookie cookie = self.cookie_input.text().strip() # 如果评论窗口已存在,先关闭它 if hasattr(self, 'comment_window') and self.comment_window: self.comment_window.close() self.comment_window = None self.comment_window = CommentWindow( parent=self, account_index=self.index, video_data=video_data, keyword_filter=keyword_filter, # 传递处理后的关键字 cookie=cookie, # 传递cookie use_proxy=False, # 默认不开启代理 account_keywords=keyword_filter, # 传递账号关键字 platform=self.current_platform # 传递当前平台 ) # 使用独立窗口模式 self.comment_window.setWindowFlag(Qt.WindowType.Window) self.comment_window.finished.connect(self.on_comment_window_closed) self.comment_window.show() except Exception as e: QMessageBox.critical(self, "错误", f"无法打开评论窗口: {str(e)}") def extract_douyin_video_id(self, video_url): """从抖音视频地址中提取视频ID""" # 示例URL: https://www.douyin.com/video/1234567890123456789 parts = video_url.split('/') if len(parts) >= 5: return parts[-1] # 返回最后一部分作为视频ID return "" def extract_xiaohongshu_note_id(self, note_url): """从小红书笔记地址中提取笔记ID""" # 示例URL: https://www.xiaohongshu.com/explore/1234567890abcdef12345678 parts = note_url.split('/') if len(parts) >= 5: return parts[-1] # 返回最后一部分作为笔记ID return "" def extract_video_id(self, video_url): """从视频地址中提取视频ID""" # 示例URL: https://www.douyin.com/video/1234567890123456789 parts = video_url.split('/') if len(parts) >= 5: return parts[-1] # 返回最后一部分作为视频ID return "" def on_comment_window_closed(self): # 安全地关闭评论窗口 if hasattr(self, 'comment_window') and self.comment_window: # 确保停止线程 if hasattr(self.comment_window, 'comment_thread') and self.comment_window.comment_thread.isRunning(): self.comment_window.comment_thread.stop() self.comment_window.comment_thread.quit() self.comment_window.comment_thread.wait(2000) self.comment_window = None def show_context_menu(self, position): menu = QMenu(self) selected_rows = set(item.row() for item in self.table.selectedItems()) if not selected_rows: return copy_row_action = menu.addAction("复制整行") copy_row_action.triggered.connect(lambda: self.copy_selected_row(selected_rows)) copy_cell_action = menu.addAction("复制单元格") copy_cell_action.triggered.connect(self.copy_selected_cell) menu.addSeparator() # 根据平台显示不同的复制选项 if self.current_platform == "douyin": copy_url_action = menu.addAction("复制视频地址") copy_url_action.triggered.connect(lambda: self.copy_selected_column(3)) copy_video_id_action = menu.addAction("复制视频ID") copy_video_id_action.triggered.connect(lambda: self.copy_selected_column(6)) elif self.current_platform == "xiaohongshu": copy_url_action = menu.addAction("复制笔记链接") copy_url_action.triggered.connect(lambda: self.copy_selected_column(3)) copy_note_id_action = menu.addAction("复制笔记ID") copy_note_id_action.triggered.connect(lambda: self.copy_selected_column(7)) copy_author_action = menu.addAction("复制作者昵称") copy_author_action.triggered.connect(lambda: self.copy_selected_column(1)) menu.exec(self.table.viewport().mapToGlobal(position)) def copy_selected_row(self, rows): text = "" for row in sorted(rows): row_data = [] for col in range(self.table.columnCount()): item = self.table.item(row, col) if item: row_data.append(item.text()) text += "\t".join(row_data) + "\n" clipboard = QApplication.clipboard() clipboard.setText(text.strip()) def copy_selected_cell(self): selected_items = self.table.selectedItems() if not selected_items: return text = "\n".join(item.text() for item in selected_items) clipboard = QApplication.clipboard() clipboard.setText(text) def copy_selected_column(self, column): selected_items = [item for item in self.table.selectedItems() if item.column() == column] if not selected_items: return text = "\n".join(item.text() for item in selected_items) clipboard = QApplication.clipboard() clipboard.setText(text) def resizeEvent(self, event): super().resizeEvent(event) if self.table_container: self.overlay.setGeometry(0, 0, self.table_container.width(), self.table_container.height())和main_window.py的import pandas as pd from PyQt6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QLabel, QTabWidget, QHBoxLayout, QPushButton, QStatusBar, QStyle, QFileDialog, QMessageBox, QDialog, QFormLayout, QLineEdit, QComboBox, QCheckBox ) from PyQt6.QtGui import QFont from PyQt6.QtCore import Qt, QTimer from tanchen_v2.ui.account_widget import AccountWidget from tanchen_v2.core_v1.config_manager import ConfigManager class ProxyConfigDialog(QDialog): def __init__(self, config_manager, parent=None): super().__init__(parent) self.setWindowTitle("天启代理配置") self.setGeometry(200, 200, 500, 300) self.config_manager = config_manager layout = QVBoxLayout() layout.setContentsMargins(15, 15, 15, 15) layout.setSpacing(10) # 加载配置 self.config = self.config_manager.load_proxy_config() or {} form_layout = QFormLayout() form_layout.setSpacing(10) # 创建控件 self.secret_edit = QLineEdit() self.secret_edit.setText(self.config.get("secret", "")) self.type_combo = QComboBox() self.type_combo.addItems(["json", "txt"]) if "type" in self.config: self.type_combo.setCurrentText(self.config["type"]) else: self.type_combo.setCurrentText("json") self.time_combo = QComboBox() self.time_combo.addItems(["3", "5", "10", "15"]) if "time" in self.config: self.time_combo.setCurrentText(self.config["time"]) else: self.time_combo.setCurrentText("5") self.mr_check = QCheckBox("IP去重") self.mr_check.setChecked(self.config.get("mr", "1") == "1") self.sign_edit = QLineEdit() self.sign_edit.setText(self.config.get("sign", "")) # 添加表单行 form_layout.addRow("提取秘钥 (secret):", self.secret_edit) form_layout.addRow("返回类型 (type):", self.type_combo) form_layout.addRow("IP使用时长 (time):", self.time_combo) form_layout.addRow("", self.mr_check) # 单独一行显示复选框 form_layout.addRow("用户签名 (sign):", self.sign_edit) layout.addLayout(form_layout) # 按钮 btn_layout = QHBoxLayout() save_btn = QPushButton("保存") save_btn.setStyleSheet(""" QPushButton { background-color: #4CAF50; color: white; padding: 8px 16px; border-radius: 4px; } QPushButton:hover { background-color: #45a049; } """) save_btn.clicked.connect(self.save_config) cancel_btn = QPushButton("取消") cancel_btn.setStyleSheet(""" QPushButton { background-color: #f44336; color: white; padding: 8px 16px; border-radius: 4px; } QPushButton:hover { background-color: #d32f2f; } """) cancel_btn.clicked.connect(self.reject) btn_layout.addStretch() btn_layout.addWidget(save_btn) btn_layout.addWidget(cancel_btn) btn_layout.addStretch() layout.addLayout(btn_layout) self.setLayout(layout) def save_config(self): self.config = { "secret": self.secret_edit.text(), "type": self.type_combo.currentText(), "time": self.time_combo.currentText(), "mr": "1" if self.mr_check.isChecked() else "0", "sign": self.sign_edit.text() } self.config_manager.save_proxy_config(self.config) self.accept() class DouyinDataCollector(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("多平台数据采集工具") self.setGeometry(100, 100, 1400, 800) self.setStyleSheet(""" QMainWindow { background-color: #f5f5f5; } QTabWidget::pane { border: 1px solid #e0e0e0; background: white; } QTabBar::tab { background: #e0e0e0; border: 1px solid #e0e0e0; padding: 8px 16px; border-top-left-radius: 4px; border-top-right-radius: 4px; margin-right: 2px; } QTabBar::tab:selected { background: white; border-bottom: none; } QTabBar::tab:!selected { margin-top: 4px; } """) self.accounts = [] self.config_manager = ConfigManager() self.current_platform = "douyin" self.init_ui() def init_ui(self): main_widget = QWidget() main_layout = QVBoxLayout() main_layout.setContentsMargins(15, 15, 15, 15) main_layout.setSpacing(15) # 标题 title_label = QLabel("多平台数据采集系统") title_label.setFont(QFont("Arial", 18, QFont.Weight.Bold)) title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) title_label.setStyleSheet(""" QLabel { color: #2196F3; padding: 10px; background-color: white; border-radius: 8px; border: 1px solid #e0e0e0; } """) main_layout.addWidget(title_label) ####################### # 平台选择和代理配置按钮 top_control_layout = QHBoxLayout() # 平台选择器 platform_layout = QHBoxLayout() platform_layout.addWidget(QLabel("选择平台:")) self.platform_combo = QComboBox() self.platform_combo.addItem("抖音", "douyin") self.platform_combo.addItem("小红书", "xiaohongshu") self.platform_combo.currentIndexChanged.connect(self.switch_platform) platform_layout.addWidget(self.platform_combo) top_control_layout.addLayout(platform_layout) # 代理配置按钮 top_control_layout.addStretch() self.proxy_config_btn = self.create_button( "天启代理配置", "#FF9800", "#F57C00", "#E65100", QStyle.StandardPixmap.SP_ComputerIcon) self.proxy_config_btn.clicked.connect(self.show_proxy_config) top_control_layout.addWidget(self.proxy_config_btn) main_layout.addLayout(top_control_layout) # 账号标签页 self.tabs = QTabWidget() self.tabs.setTabPosition(QTabWidget.TabPosition.North) self.tabs.setDocumentMode(True) for i in range(6): account_widget = AccountWidget(i, self.config_manager, self.current_platform) self.accounts.append(account_widget) tab_name = self.get_tab_name(i) # 添加平台参数 self.tabs.addTab(account_widget, tab_name) self.tabs.currentChanged.connect(self.update_tab_names) main_layout.addWidget(self.tabs, 1) # 状态栏 self.status_bar = self.statusBar() self.status_bar.setStyleSheet("background-color: #e0e0e0; color: #666; padding: 5px;") self.status_bar.showMessage("就绪 | 数据采集工具 v1.1 | 配置已自动保存") main_widget.setLayout(main_layout) self.setCentralWidget(main_widget) def switch_platform(self): """切换平台时的处理""" new_platform = self.platform_combo.currentData() self.current_platform = new_platform # 更新所有账号的平台 for account in self.accounts: account.switch_platform(new_platform) self.status_bar.showMessage(f"已切换到{self.platform_combo.currentText()}平台") def create_button(self, text, bg_color, hover_color, pressed_color, icon): button = QPushButton(text) button.setIcon(self.style().standardIcon(icon)) button.setStyleSheet(f""" QPushButton {{ background-color: {bg_color}; color: white; padding: 10px 20px; border-radius: 6px; font-weight: bold; font-size: 14px; }} QPushButton:hover {{ background-color: {hover_color}; }} QPushButton:pressed {{ background-color: {pressed_color}; }} """) # 存储基础颜色用于动画 button.setProperty("base_color", bg_color) return button def get_tab_name(self, index): config = self.config_manager.load_account_config(index,self.current_platform) tab_name = f"账号 {index + 1}" if config and config.get("remark"): tab_name = f"{tab_name}: {config['remark']}" return tab_name def update_tab_names(self): for i in range(self.tabs.count()): self.tabs.setTabText(i, self.get_tab_name(i)) def show_proxy_config(self): try: dialog = ProxyConfigDialog(self.config_manager, self) dialog.setWindowModality(Qt.WindowModality.ApplicationModal) if dialog.exec() == QDialog.DialogCode.Accepted: self.status_bar.showMessage("代理配置已保存") self.animate_button(self.proxy_config_btn, "#E65100") except Exception as e: QMessageBox.critical(self, "错误", f"打开代理配置时出错: {str(e)}") def animate_button(self, button, color): original_style = button.styleSheet() button.setStyleSheet(original_style.replace( f"background-color: {button.property('base_color')}", f"background-color: {color}") ) QTimer.singleShot(500, lambda: button.setStyleSheet(original_style))
07-23
import cv2 import os import time import shutil import threading import queue import re import logging import signal import sys from pathlib import Path from collections import defaultdict from ultralytics import YOLO from logging.handlers import RotatingFileHandler # 配置参数 class Config: # 模型路径 MODEL_PATH = r"F:\yolov8\ultralytics-main\best.pt" # 初始模型路径 NEW_MODEL_DIR = r"F:\yolov8\ultralytics-main\retrained_models" # 新模型存储目录 # 图片路径 SOURCE_DIR = r"F:\yolov8\ultralytics-main\datasets\bvn\images\train" # 图片输入目录 OUTPUT_DIR = r"F:\yolov8\ultralytics-main\datasets\bvn\images\done" # 检测结果输出路径 NO_DETECTION_DIR = os.path.join(OUTPUT_DIR, "no_detection") # 未检测到目标的文件夹 LOG_DIR = os.path.join(OUTPUT_DIR, "logs") # 日志目录 BACKUP_DIR = os.path.join(OUTPUT_DIR, "backup") # 原始图片备份目录 # 训练参数 TRAINING_THRESHOLD = 600 # 收集一定新图片后开始训练 EPOCHS = 50 # 训练轮数 IMGSZ = 640 # 训练图像尺寸 BATCH = 8 # 训练批次大小 # 检测参数 CONFIDENCE_THRESHOLD = 0.5 # 检测置信度阈值 POLL_INTERVAL = 1 # 文件夹轮询间隔(秒) STABLE_TIME = 0.5 # 文件稳定时间(秒) IMAGE_EXTS = {&#39;.jpg&#39;, &#39;.jpeg&#39;, &#39;.png&#39;, &#39;.bmp&#39;} # 支持的图片格式 # 日志参数 MAX_LOG_SIZE = 10 * 1024 * 1024 # 单个日志文件最大大小 (10MB) BACKUP_COUNT = 5 # 保留的备份日志文件数量 # 确保目录存在 Path(Config.OUTPUT_DIR).mkdir(parents=True, exist_ok=True) Path(Config.NO_DETECTION_DIR).mkdir(parents=True, exist_ok=True) Path(Config.NEW_MODEL_DIR).mkdir(parents=True, exist_ok=True) Path(Config.LOG_DIR).mkdir(parents=True, exist_ok=True) Path(Config.BACKUP_DIR).mkdir(parents=True, exist_ok=True) # 全局变量 training_queue = queue.Queue() # 训练任务队列 new_images_count = 0 # 新图片计数器 model_lock = threading.Lock() # 模型更新锁 log_lock = threading.Lock() # 日志文件锁 exit_flag = threading.Event() # 程序退出标志 # 加载初始模型 model = YOLO(Config.MODEL_PATH) # 日志管理器(使用阿拉伯数字序号) class SequentialLogManager: def __init__(self, log_dir): self.log_dir = Path(log_dir) self.loggers = {} self.entry_counts = defaultdict(int) # 每个类别的条目计数 self.setup_logging() def setup_logging(self): """配置日志系统""" # 创建根日志记录器 root_logger = logging.getLogger() root_logger.setLevel(logging.INFO) # 控制台处理器 console_handler = logging.StreamHandler() console_handler.setFormatter(logging.Formatter(&#39;%(asctime)s - %(levelname)s - %(message)s&#39;)) root_logger.addHandler(console_handler) def get_logger(self, cls_name): """获取或创建特定类别的日志记录器""" with log_lock: if cls_name not in self.loggers: # 创建新的日志记录器 logger = logging.getLogger(f"detection.{cls_name}") logger.propagate = False # 防止传播到根日志 # 创建日志文件路径 log_file = self.log_dir / f"{cls_name}.log" # 创建旋转文件处理器 file_handler = RotatingFileHandler( log_file, maxBytes=Config.MAX_LOG_SIZE, backupCount=Config.BACKUP_COUNT ) # 设置日志格式 formatter = logging.Formatter(&#39;%(asctime)s - %(message)s&#39;) file_handler.setFormatter(formatter) # 添加处理器 logger.addHandler(file_handler) # 记录初始信息 logger.info(f"{&#39;=&#39; * 60}") logger.info(f"Detection Log - Class: {cls_name}") logger.info(f"Log Start Time: {time.strftime(&#39;%Y-%m-%d %H:%M:%S&#39;)}") logger.info(f"{&#39;=&#39; * 60}") # 初始化计数 self.entry_counts[cls_name] = 0 # 保存记录器 self.loggers[cls_name] = logger return self.loggers[cls_name] def parse_filename(self, filename): """解析文件名,提取日期、时间和长度信息""" # 示例文件名: "Err2025-06-28 040014922L14397382_0_result" pattern = r&#39;Err(\d{4}-\d{2}-\d{2}) (\d{9})L(\d+)&#39; match = re.search(pattern, filename) if match: date = match.group(1) # 2025-06-28 time_str = match.group(2) # 040014922 length_mm = int(match.group(3)) # 14397382 (mm) # 格式化时间: HH:mm:ss.fff formatted_time = f"{time_str[0:2]}:{time_str[2:4]}:{time_str[4:6]}.{time_str[6:9]}" # 转换毫米为米,保留3位小数 length_m = round(length_mm / 1000, 3) return date, formatted_time, length_m return None, None, None def log_detection(self, cls_name, img_name): """记录检测日志并更新计数(使用阿拉伯数字)""" date, time_str, length_m = self.parse_filename(img_name) if date is None or time_str is None or length_m is None: logging.warning(f"Cannot parse filename: {img_name}") return # 获取或创建日志记录器 logger = self.get_logger(cls_name) # 更新计数 with log_lock: self.entry_counts[cls_name] += 1 entry_num = self.entry_counts[cls_name] # 创建日志条目(使用阿拉伯数字) log_entry = f"{entry_num}. 日期: {date}, 时间: {time_str}, 长度: {length_m} 原编码: {img_name}" # 记录日志 logger.info(log_entry) def close_all(self): """关闭所有日志记录器并添加最终统计信息""" for cls_name, logger in self.loggers.items(): # 获取日志处理器 for handler in logger.handlers: if isinstance(handler, RotatingFileHandler): # 记录最终统计信息 total_entries = self.entry_counts[cls_name] logger.info(f"{&#39;=&#39; * 60}") logger.info(f"Detection Class: {cls_name}") logger.info(f"Total Entries: {total_entries}") logger.info(f"Log End Time: {time.strftime(&#39;%Y-%m-%d %H:%M:%S&#39;)}") logger.info(f"{&#39;=&#39; * 60}") # 关闭处理器 handler.close() # 移除处理器 logger.handlers = [] # 重置计数 self.entry_counts.clear() self.loggers.clear() def get_entry_counts(self): """获取所有类别的条目计数""" with log_lock: return dict(self.entry_counts) # 创建全局日志管理器 log_manager = SequentialLogManager(Config.LOG_DIR) def signal_handler(sig, frame): """处理Ctrl+C信号""" print("\nCtrl+C pressed. Exiting gracefully...") exit_flag.set() # 设置退出标志 # 注册信号处理器 signal.signal(signal.SIGINT, signal_handler) def backup_original_image(image_path): """将原始图片移动到备份目录""" try: # 确保备份目录存在 Path(Config.BACKUP_DIR).mkdir(parents=True, exist_ok=True) # 创建目标路径 img_name = Path(image_path).name backup_path = Path(Config.BACKUP_DIR) / img_name # 移动文件(如果目标已存在,则添加时间戳) if backup_path.exists(): timestamp = time.strftime("%Y%m%d-%H%M%S") new_name = f"{backup_path.stem}_{timestamp}{backup_path.suffix}" backup_path = backup_path.with_name(new_name) # 移动文件 shutil.move(image_path, backup_path) return backup_path except Exception as e: logging.error(f"Failed to backup original image: {image_path} - {str(e)}") return None def process_image(image_path): """处理单张图片并保存结果,返回检测结果统计""" global new_images_count # 检查退出标志 if exit_flag.is_set(): return {} # 读取原始图片 original_img = cv2.imread(image_path) if original_img is None: logging.error(f"Failed to read image: {image_path}") return {} # 获取图片名称用于日志记录 img_name = Path(image_path).stem # 执行目标检测 results = model(image_path, conf=Config.CONFIDENCE_THRESHOLD) # 检测结果计数器 detection_count = 0 class_files = defaultdict(list) # 按类别保存文件路径 # 处理检测结果 for result in results: # 检查退出标志 if exit_flag.is_set(): return {} # 统计检测到的目标数量 detection_count += len(result.boxes) if result.boxes is not None else 0 # 如果有检测结果 if result.boxes is not None and len(result.boxes) > 0: # 绘制检测结果 plotted_img = result.plot() # 获取检测到的类别 class_indices = result.boxes.cls.int().tolist() # 为每个检测到的类别保存结果 for cls_idx in set(class_indices): # 使用set去重 cls_name = model.names[cls_idx] cls_dir = Path(Config.OUTPUT_DIR) / cls_name cls_dir.mkdir(parents=True, exist_ok=True) # 保存路径 save_path = str(cls_dir / f"{Path(image_path).stem}_result{Path(image_path).suffix}") cv2.imwrite(save_path, plotted_img) class_files[cls_name].append(save_path) # 记录日志 log_manager.log_detection(cls_name, img_name) # 如果没有检测到任何目标 if detection_count == 0: # 在图片上添加"未检测到目标"文本 annotated_img = original_img.copy() text = "No Detection" font = cv2.FONT_HERSHEY_SIMPLEX font_scale = 1 thickness = 2 color = (0, 0, 255) # 红色文本 # 获取文本尺寸并居中 text_size = cv2.getTextSize(text, font, font_scale, thickness)[0] text_x = (annotated_img.shape[1] - text_size[0]) // 2 text_y = (annotated_img.shape[0] + text_size[1]) // 2 # 添加文本 cv2.putText(annotated_img, text, (text_x, text_y), font, font_scale, color, thickness) # 保存到未检测文件夹 save_path = str(Path(Config.NO_DETECTION_DIR) / f"{Path(image_path).name}") cv2.imwrite(save_path, annotated_img) class_files["no_detection"] = [save_path] # 记录未检测日志 log_manager.log_detection("no_detection", img_name) # 备份原始图片 backup_path = backup_original_image(image_path) if backup_path: logging.info(f"Original image backed up to: {backup_path}") else: logging.warning(f"Failed to backup original image: {image_path}") # 将处理后的图片添加到训练队列 training_queue.put(backup_path if backup_path else image_path) new_images_count += 1 return class_files def auto_retrain_model(): """自动训练新模型并替换旧模型""" global model, new_images_count # 检查退出标志 if exit_flag.is_set(): logging.warning("Training canceled due to exit signal") return False # 创建数据集目录 dataset_dir = os.path.join(Config.NEW_MODEL_DIR, "auto_dataset") images_dir = os.path.join(dataset_dir, "images", "train") labels_dir = os.path.join(dataset_dir, "labels", "train") os.makedirs(images_dir, exist_ok=True) os.makedirs(labels_dir, exist_ok=True) logging.info(f"Preparing training data, collected {new_images_count} new images") # 处理训练队列中的所有图片 processed_count = 0 while not training_queue.empty() and not exit_flag.is_set(): try: img_path = training_queue.get_nowait() base_name = Path(img_path).name # 复制图片到训练集 shutil.copy(img_path, os.path.join(images_dir, base_name)) # 生成对应的标签文件路径 label_path = os.path.join(labels_dir, f"{Path(img_path).stem}.txt") # 如果标签文件不存在,创建一个空文件 if not os.path.exists(label_path): open(label_path, &#39;w&#39;).close() processed_count += 1 if processed_count % 10 == 0: logging.info(f"Processed {processed_count}/{new_images_count} images") except queue.Empty: break # 检查退出标志 if exit_flag.is_set(): logging.warning("Training canceled during dataset preparation") return False # 创建数据集配置文件 dataset_yaml_path = os.path.join(dataset_dir, "dataset.yaml") with open(dataset_yaml_path, &#39;w&#39;) as f: f.write(f"path: {dataset_dir}\n") f.write("train: images/train\n") f.write("val: images/train # Using same data for validation\n") f.write("names:\n") for idx, name in model.names.items(): f.write(f" {idx}: {name}\n") logging.info(f"Training dataset prepared: {dataset_dir}") logging.info(f"Starting model training (epochs={Config.EPOCHS}, batch={Config.BATCH})") # 训练新模型 try: with model_lock: # 锁定模型以防止在训练时被使用 if exit_flag.is_set(): logging.warning("Training canceled before starting") return False new_model = YOLO("yolov8n.yaml") # 使用YOLOv8n架构 results = new_model.train( data=dataset_yaml_path, epochs=Config.EPOCHS, imgsz=Config.IMGSZ, batch=Config.BATCH, project=Config.NEW_MODEL_DIR, name="auto_retrain", exist_ok=True ) except KeyboardInterrupt: logging.warning("Training interrupted by user") return False # 定位最佳模型 best_model_path = os.path.join(Config.NEW_MODEL_DIR, "auto_retrain", "weights", "best.pt") if os.path.exists(best_model_path): # 备份旧模型 timestamp = time.strftime("%Y%m%d-%H%M%S") backup_path = f"{Config.MODEL_PATH}.bak.{timestamp}" shutil.copy(Config.MODEL_PATH, backup_path) # 替换模型 shutil.copy(best_model_path, Config.MODEL_PATH) # 重新加载新模型 with model_lock: model = YOLO(Config.MODEL_PATH) logging.info(f"Model successfully replaced: {Config.MODEL_PATH}") logging.info(f"Old model backed up to: {backup_path}") # 记录类别统计信息 entry_counts = log_manager.get_entry_counts() logging.info("Class detection statistics:") for cls, count in entry_counts.items(): logging.info(f" {cls}: {count} detections") return True else: logging.error("Training failed, best model not found!!!") return False def monitor_folder(): """监控文件夹并处理新图片""" global new_images_count logging.info(f"Monitoring folder: {Config.SOURCE_DIR}") logging.info(f"Confidence threshold: {Config.CONFIDENCE_THRESHOLD}") logging.info(f"Undetected images will be saved to: {Config.NO_DETECTION_DIR}") logging.info(f"Original images will be backed up to: {Config.BACKUP_DIR}") logging.info(f"Auto-training will start after collecting {Config.TRAINING_THRESHOLD} new images") logging.info(f"Log files stored in: {Config.LOG_DIR}") logging.info("Press Ctrl+C to exit gracefully...") try: while not exit_flag.is_set(): # 检查是否需要启动训练 if new_images_count >= Config.TRAINING_THRESHOLD and not exit_flag.is_set(): logging.warning("Reached training threshold, starting auto-training process...") if auto_retrain_model(): # 重置计数器 new_images_count = 0 # 清空训练队列 while not training_queue.empty() and not exit_flag.is_set(): training_queue.get_nowait() # 获取文件夹中的所有文件 for entry in Path(Config.SOURCE_DIR).iterdir(): if exit_flag.is_set(): break if not entry.is_file(): continue file_path = str(entry.resolve()) file_ext = entry.suffix.lower() # 检查是否为支持的图片文件 if file_ext not in Config.IMAGE_EXTS: continue # 检查文件是否稳定 file_age = time.time() - entry.stat().st_mtime if file_age < Config.STABLE_TIME: continue # 处理新图片 logging.info(f"Processing new image: {entry.name}") result_files = process_image(file_path) # 打印处理结果 if "no_detection" in result_files: logging.info(f"No targets detected: {result_files[&#39;no_detection&#39;][0]}") else: for cls_name, paths in result_files.items(): logging.info(f"Detected {cls_name}: {len(paths)} images") # 显示当前新图片计数 logging.info(f"Current new images count: {new_images_count}/{Config.TRAINING_THRESHOLD}") # 检查退出标志 if exit_flag.is_set(): break time.sleep(Config.POLL_INTERVAL) except Exception as e: logging.error(f"Unexpected error: {str(e)}") finally: # 在退出前检查是否有待处理的训练任务 if new_images_count > 0 and not exit_flag.is_set(): logging.warning(f"Detected {new_images_count} unprocessed images. Train new model? (y/n)") if input().lower() == &#39;y&#39;: auto_retrain_model() # 关闭所有日志文件 log_manager.close_all() logging.info("Log files closed with final statistics") # 打印最终类别统计 entry_counts = log_manager.get_entry_counts() if entry_counts: logging.info("Final class detection statistics:") for cls, count in entry_counts.items(): logging.info(f" {cls}: {count} detections") else: logging.info("No detection records") logging.info("Program exited gracefully") if __name__ == "__main__": monitor_folder() sys.exit(0)
08-06
class SCLMultiProcessor: def __init__(self, root): self.root = root # 创建日志目录 self.log_dir = "logs" self.root.title("SCL文件处理系统") self.root.geometry("1100x750") # 增加窗口尺寸以适应新控件 # 初始化变量 self.color_detector = EnhancedColorDetector() self.stats_processor = SCLRuleProcessor(self.color_detector) self.empty_cell_detector = EnhancedEmptyCellDetector(self.color_detector) self.progress_var = tk.DoubleVar() os.makedirs(self.log_dir, exist_ok=True) # 初始化日志系统 self.current_log_file = None self.setup_logger() # 创建主框架 self.main_frame = ttk.Frame(root, padding="10") self.main_frame.pack(fill=tk.BOTH, expand=True) # 创建UI self.create_ui() # 记录UI初始化完成 logger.info("用户界面初始化完成") # 先定义 toggle_config_fields 方法 def toggle_config_fields(self): """根据操作模式显示/隐藏相关配置字段""" mode = self.operation_mode.get() # 统计模式:显示统计表路径,隐藏CheckSheet路径 if mode == "stats": self.input_frame.pack(fill=tk.X, pady=5) # 显示统计表路径 self.checksheet_frame.pack_forget() # 隐藏CheckSheet路径 logger.info("切换到统计模式,显示统计表路径") # SCL格式检查模式:隐藏统计表路径,显示CheckSheet路径 elif mode == "empty_check": self.input_frame.pack_forget() # 隐藏统计表路径 self.checksheet_frame.pack(fill=tk.X, pady=5) # 显示CheckSheet路径 logger.info("切换到SCL格式检查模式,显示CheckSheet路径") def create_ui(self): """创建用户界面""" # 操作模式选择区域 - 放在最前面 mode_frame = ttk.LabelFrame(self.main_frame, text="操作模式", padding="10") mode_frame.pack(fill=tk.X, pady=5) # 添加操作模式单选按钮 self.operation_mode = tk.StringVar(value="stats") # 默认选择统计模式 ttk.Radiobutton(mode_frame, text="统计功能", variable=self.operation_mode, value="stats", command=self.toggle_config_fields).pack(side=tk.LEFT, padx=10) ttk.Radiobutton(mode_frame, text="SCL格式检查", variable=self.operation_mode, value="empty_check", command=self.toggle_config_fields).pack(side=tk.LEFT, padx=10) # 文件选择区域 - 放在操作模式后面 file_frame = ttk.LabelFrame(self.main_frame, text="文件选择", padding="10") file_frame.pack(fill=tk.X, pady=5) # 输入文件选择 - 统计表路径(统计模式需要) self.input_frame = ttk.Frame(file_frame) ttk.Label(self.input_frame, text="统计表:").pack(side=tk.LEFT, padx=5) self.input_path_var = tk.StringVar() input_entry = ttk.Entry(self.input_frame, textvariable=self.input_path_var, width=70) input_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) ttk.Button(self.input_frame, text="浏览...", command=self.browse_input_file).pack(side=tk.LEFT, padx=5) # 配置区域 config_frame = ttk.LabelFrame(self.main_frame, text="处理配置", padding="10") config_frame.pack(fill=tk.X, pady=5) # 添加SCL文件夹路径输入 scl_folder_frame = ttk.Frame(config_frame) scl_folder_frame.pack(fill=tk.X, pady=5) ttk.Label(scl_folder_frame, text="SCL文件夹路径:").grid(row=0, column=0, padx=5, sticky=tk.W) self.scl_folder_var = tk.StringVar() scl_folder_entry = ttk.Entry(scl_folder_frame, textvariable=self.scl_folder_var, width=60) scl_folder_entry.grid(row=0, column=1, padx=5, sticky=tk.W) ttk.Button(scl_folder_frame, text="浏览...", command=self.browse_scl_folder).grid(row=0, column=2, padx=5, sticky=tk.W) # 搜索选项 search_frame = ttk.Frame(config_frame) search_frame.pack(fill=tk.X, pady=5) ttk.Label(search_frame, text="文件前缀:").grid(row=0, column=0, padx=5, sticky=tk.W) self.prefix_var = tk.StringVar(value="SCL_") ttk.Entry(search_frame, textvariable=self.prefix_var, width=10).grid(row=0, column=1, padx=5, sticky=tk.W) # 添加CheckSheet路径输入(SCL格式检查模式需要) self.checksheet_frame = ttk.Frame(config_frame) ttk.Label(self.checksheet_frame, text="CheckSheet路径:").grid(row=0, column=0, padx=5, sticky=tk.W) self.checksheet_path_var = tk.StringVar() checksheet_entry = ttk.Entry(self.checksheet_frame, textvariable=self.checksheet_path_var, width=60) checksheet_entry.grid(row=0, column=1, padx=5, sticky=tk.W) ttk.Button(self.checksheet_frame, text="浏览...", command=self.browse_checksheet_path).grid(row=0, column=2, padx=5, sticky=tk.W) # 添加性能提示 ttk.Label(config_frame, text="(表头固定在第3行,数据从第4行开始)").pack(anchor=tk.W, padx=5, pady=2) # 日志选项 log_frame = ttk.Frame(config_frame) log_frame.pack(fill=tk.X, pady=5) ttk.Label(log_frame, text="日志级别:").grid(row=0, column=0, padx=5, sticky=tk.W) self.log_level_var = tk.StringVar(value="INFO") log_level_combo = ttk.Combobox( log_frame, textvariable=self.log_level_var, width=10, state="readonly" ) log_level_combo[&#39;values&#39;] = (&#39;DEBUG&#39;, &#39;INFO&#39;, &#39;WARNING&#39;, &#39;ERROR&#39;, &#39;CRITICAL&#39;) log_level_combo.grid(row=0, column=1, padx=5, sticky=tk.W) log_level_combo.bind("<<ComboboxSelected>>", self.change_log_level) # 根据初始模式显示/隐藏相关字段 self.toggle_config_fields() # 处理按钮 btn_frame = ttk.Frame(self.main_frame) btn_frame.pack(fill=tk.X, pady=10) ttk.Button(btn_frame, text="开始处理", command=self.process_file).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="查看日志", command=self.view_log).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="导出配置", command=self.export_config).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="加载配置", command=self.load_config).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="退出", command=self.root.destroy).pack(side=tk.RIGHT, padx=5) # 进度条 progress_frame = ttk.Frame(self.main_frame) progress_frame.pack(fill=tk.X, pady=5) ttk.Label(progress_frame, text="处理进度:").pack(side=tk.LEFT, padx=5) self.progress_bar = ttk.Progressbar( progress_frame, variable=self.progress_var, maximum=100, length=700 ) self.progress_bar.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) self.progress_label = ttk.Label(progress_frame, text="0%") self.progress_label.pack(side=tk.LEFT, padx=5) # 结果展示区域 result_frame = ttk.LabelFrame(self.main_frame, text="处理结果", padding="10") result_frame.pack(fill=tk.BOTH, expand=True, pady=5) # 结果文本框 self.result_text = scrolledtext.ScrolledText( result_frame, wrap=tk.WORD, height=20 ) self.result_text.pack(fill=tk.BOTH, expand=True) self.result_text.config(state=tk.DISABLED) # 状态栏 self.status_var = tk.StringVar(value="就绪") status_bar = ttk.Label(self.main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) status_bar.pack(fill=tk.X, pady=5) logger.info("UI创建完成") def browse_checksheet_path(self): """浏览CheckSheet文件夹""" folder_path = filedialog.askdirectory(title="选择CheckSheet文件夹") if folder_path: self.checksheet_path_var.set(folder_path) logger.info(f"已选择CheckSheet文件夹: {folder_path}") def setup_logger(self): """配置日志记录器""" # 创建唯一日志文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") self.current_log_file = os.path.join(self.log_dir, f"scl_processor_{timestamp}.log") # 创建或获取日志记录器 self.logger = logging.getLogger("SCLProcessor") self.logger.setLevel(logging.INFO) # 移除所有现有处理器 for handler in self.logger.handlers[:]: self.logger.removeHandler(handler) # 创建文件处理器 file_handler = logging.FileHandler(self.current_log_file, encoding=&#39;utf-8&#39;) file_handler.setLevel(logging.INFO) # 创建控制台处理器 console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) # 创建日志格式 formatter = logging.Formatter( &#39;%(asctime)s - %(name)s - %(levelname)s - %(message)s&#39;, datefmt=&#39;%Y-%m-%d %H:%M:%S&#39; ) file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) # 添加处理器 self.logger.addHandler(file_handler) self.logger.addHandler(console_handler) # 记录日志初始化信息 self.logger.info(f"日志系统已初始化,日志文件: {self.current_log_file}") self.logger.info(f"日志目录: {os.path.abspath(self.log_dir)}") def change_log_level(self, event=None): """动态更改日志级别并更新所有处理器(会话感知版本)""" try: # 获取选择的日志级别 level_str = self.log_level_var.get() log_level = getattr(logging, level_str.upper()) # 更新全局日志级别设置 self.current_log_level = log_level logger.info(f"请求更改日志级别为: {level_str}") # 设置根日志记录器级别 logger.setLevel(log_level) # 更新所有现有处理器的级别 for handler in logger.handlers: # 仅更新文件和控制台处理器 if isinstance(handler, (logging.FileHandler, logging.StreamHandler)): handler.setLevel(log_level) # 记录级别变更确认 logger.info(f"日志级别已成功更改为: {level_str}") # 添加调试信息显示当前处理器级别 handler_levels = [ f"{type(h).__name__}: {logging.getLevelName(h.level)}" for h in logger.handlers ] logger.debug(f"当前处理器级别: {&#39;, &#39;.join(handler_levels)}") # 更新UI状态显示 self.status_var.set(f"日志级别: {level_str}") except AttributeError: logger.error(f"无效的日志级别: {level_str}") messagebox.showerror("错误", f"无效的日志级别: {level_str}") except Exception as e: logger.exception("更改日志级别时发生错误") messagebox.showerror("错误", f"更改日志级别失败: {str(e)}") def browse_input_file(self): """浏览输入文件""" file_path = filedialog.askopenfilename( filetypes=[("Excel 文件", "*.xlsx *.xls"), ("所有文件", "*.*")] ) if file_path: self.input_path_var.set(file_path) self.input_file = file_path logger.info(f"已选择输入文件: {file_path}") def browse_scl_folder(self): """浏览SCL文件夹""" folder_path = filedialog.askdirectory(title="选择SCL文件夹") if folder_path: self.scl_folder_var.set(folder_path) logger.info(f"已选择SCL文件夹: {folder_path}") def highlight_cell(self, sheet, row, col, color="FFFF0000"): """为单元格设置背景色""" try: fill = PatternFill(start_color=color, end_color=color, fill_type="solid") sheet.cell(row=row, column=col).fill = fill return True except Exception as e: logger.error(f"设置单元格颜色失败: {str(e)}") return False def process_file(self): """处理文件 - 每次处理保存数据,下次运行重新开始""" operation_mode = self.operation_mode.get() # 统计模式需要统计表路径 if operation_mode == "stats" and not self.input_path_var.get(): messagebox.showwarning("警告", "请先选择统计表") logger.warning("未选择统计表") return # SCL格式检查模式需要CheckSheet路径 if operation_mode == "empty_check" and not self.checksheet_path_var.get(): messagebox.showwarning("警告", "请先选择CheckSheet路径") logger.warning("未选择CheckSheet路径") return try: # 重置结果 self.result_text.config(state=tk.NORMAL) self.result_text.delete(1.0, tk.END) self.result_text.insert(tk.END, "开始处理...\n") self.result_text.see(tk.END) self.result_text.config(state=tk.DISABLED) self.status_var.set("开始处理文件...") self.root.update() # 每次处理前重新初始化日志系统 self.setup_logger() # 记录处理开始信息 self.logger.info("=" * 50) self.logger.info(f"开始新处理会话: {datetime.now().strftime(&#39;%Y-%m-%d %H:%M:%S&#39;)}") self.logger.info("=" * 50) # 更新UI显示当前日志文件 self.status_var.set(f"当前日志: {os.path.basename(self.current_log_file)}") # 获取输入文件目录 input_file = self.input_path_var.get() input_dir = os.path.dirname(input_file) logger.info(f"开始处理文件: {input_file}") logger.info(f"文件目录: {input_dir}") # 获取SCL文件夹路径 scl_folder = self.scl_folder_var.get() if not scl_folder: # 如果没有指定SCL文件夹,则使用输入文件所在目录 scl_folder = input_dir logger.info(f"未指定SCL文件夹,使用输入文件目录: {scl_folder}") # 使用openpyxl加载工作簿(保留格式) wb = openpyxl.load_workbook(input_file) sheet = wb.active logger.info(f"工作簿加载成功, 工作表: {sheet.title}") # 获取配置参数 prefix = self.prefix_var.get() operation_mode = self.operation_mode.get() logger.info(f"配置参数: 文件前缀={prefix}, 操作模式={operation_mode}") # 扫描E列(第5列) total_rows = sheet.max_row processed_count = 0 found_files = 0 problem_files = 0 logger.info(f"开始扫描E列, 总行数: {total_rows}") start_time = time.time() for row_idx in range(1, total_rows + 1): # 更新进度 progress = (row_idx / total_rows) * 100 self.progress_var.set(progress) self.progress_label.config(text=f"{progress:.1f}%") self.root.update() cell = sheet.cell(row=row_idx, column=5) cell_value = str(cell.value) if cell.value else "" # 检查是否包含前缀的文件名 if prefix in cell_value: # 提取文件名(可能有多个以空格分隔) file_names = re.findall(fr&#39;{prefix}[^\s]+&#39;, cell_value) logger.info(f"行 {row_idx}: 找到文件: {&#39;, &#39;.join(file_names)}") result_lines = [] file_has_problems = False # 标记当前行是否有问题文件 for file_name in file_names: # 构建文件路径 - 使用SCL文件夹 file_path = os.path.join(scl_folder, file_name) # 检查文件是否存在 if not os.path.exists(file_path): result_lines.append(f"{file_name}: 文件不存在") logger.warning(f"文件不存在: {file_path}") # 标记文件不存在的单元格为紫色 self.highlight_cell(sheet, row_idx, 5, "FF800080") file_has_problems = True problem_files += 1 continue # 根据操作模式执行不同处理 if operation_mode == "stats": # 统计模式 results, color_report, missing_data = self.stats_processor.process_file(file_path) # 如果有数据缺失 if missing_data: file_has_problems = True problem_files += 1 result_lines.append(f"{file_name}: 数据缺失!") for item in missing_data: result_lines.append(f" - {item[&#39;message&#39;]}") logger.warning(item[&#39;message&#39;]) else: result_lines.append(f"{file_name}: 处理完成") # 将结果写入主Excel文件的不同列 for rule_name, result_str in results.items(): target_col = self.stats_processor.RULE_MAPPING.get(rule_name) if target_col: target_cell = sheet.cell(row=row_idx, column=target_col) target_cell.value = result_str elif operation_mode == "empty_check": # SCL格式检查模式 checksheet_path = self.checksheet_path_var.get() missing_data, marked_file_path = self.empty_cell_detector.detect_empty_cells( file_path, checksheet_path ) if missing_data: file_has_problems = True problem_files += 1 result_lines.append(f"{file_name}: 发现空单元格!") for item in missing_data: result_lines.append(f" - {item[&#39;message&#39;]}") logger.warning(item[&#39;message&#39;]) # 添加标记文件路径信息 if marked_file_path: result_lines.append(f"已生成标记文件: {marked_file_path}") else: result_lines.append(f"{file_name}: 无空单元格问题") found_files += 1 # 如果该行有文件存在问题,将E列单元格标红 if file_has_problems: self.highlight_cell(sheet, row_idx, 5) logger.info(f"行 {row_idx} E列单元格标记为红色(存在问题)") # 更新结果文本框 self.result_text.config(state=tk.NORMAL) self.result_text.insert( tk.END, f"行 {row_idx} 处理结果:\n" + "\n".join(result_lines) + "\n\n" ) self.result_text.see(tk.END) self.result_text.config(state=tk.DISABLED) processed_count += 1 # 保存修改后的Excel文件 - 每次处理保存数据 output_path = input_file.replace(".xlsx", "_processed.xlsx") wb.save(output_path) logger.info(f"结果已保存到: {output_path}") elapsed_time = time.time() - start_time status_msg = f"处理完成! 处理了 {processed_count} 个文件项, 耗时 {elapsed_time:.2f} 秒" if problem_files > 0: status_msg += f", {problem_files} 个文件存在问题" self.status_var.set(status_msg) logger.info(status_msg) # 更新结果文本框 self.result_text.config(state=tk.NORMAL) self.result_text.insert( tk.END, f"\n{status_msg}\n" f"结果已保存到: {output_path}\n" ) self.result_text.see(tk.END) self.result_text.config(state=tk.DISABLED) messagebox.showinfo("完成", status_msg) except Exception as e: error_msg = f"处理文件时出错: {str(e)}" logger.exception(f"处理文件时出错: {str(e)}") messagebox.showerror("错误", error_msg) self.status_var.set(f"错误: {str(e)}") # 更新结果文本框 self.result_text.config(state=tk.NORMAL) self.result_text.insert(tk.END, f"\n错误: {error_msg}\n") self.result_text.see(tk.END) self.result_text.config(state=tk.DISABLED) def view_log(self): """查看日志""" log_window = tk.Toplevel(self.root) log_window.title("处理日志") log_window.geometry("800x600") log_frame = ttk.Frame(log_window, padding="10") log_frame.pack(fill=tk.BOTH, expand=True) # 日志文本框 log_text = scrolledtext.ScrolledText( log_frame, wrap=tk.WORD, height=30 ) log_text.pack(fill=tk.BOTH, expand=True) # 读取日志文件 log_file = &#39;scl_processor.log&#39; try: if not os.path.exists(log_file): with open(log_file, &#39;w&#39;, encoding=&#39;utf-8&#39;) as f: f.write("日志文件已创建,暂无记录\n") with open(log_file, &#39;r&#39;, encoding=&#39;utf-8&#39;) as f: log_content = f.read() log_text.insert(tk.END, log_content) except Exception as e: log_text.insert(tk.END, f"无法读取日志文件: {str(e)}") # 设置为只读 log_text.config(state=tk.DISABLED) # 添加刷新按钮 refresh_btn = ttk.Button(log_frame, text="刷新日志", command=lambda: self.refresh_log(log_text)) refresh_btn.pack(pady=5) logger.info("日志查看窗口已打开") def refresh_log(self, log_text): """刷新日志内容""" log_text.config(state=tk.NORMAL) log_text.delete(1.0, tk.END) try: with open(&#39;scl_processor.log&#39;, &#39;r&#39;, encoding=&#39;utf-8&#39;) as f: log_content = f.read() log_text.insert(tk.END, log_content) except Exception as e: log_text.insert(tk.END, f"刷新日志失败: {str(e)}") log_text.config(state=tk.DISABLED) log_text.see(tk.END) logger.info("日志已刷新") def export_config(self): """导出配置到文件""" config = { "prefix": self.prefix_var.get(), "log_level": self.log_level_var.get(), "operation_mode": self.operation_mode.get(), "tolerance": self.tolerance_var.get(), "checksheet_path": self.checksheet_path_var.get(), "scl_folder": self.scl_folder_var.get() # 添加SCL文件夹路径 } file_path = filedialog.asksaveasfilename( defaultextension=".json", filetypes=[("JSON 文件", "*.json"), ("所有文件", "*.*")] ) if file_path: try: with open(file_path, &#39;w&#39;, encoding=&#39;utf-8&#39;) as f: f.write(str(config)) messagebox.showinfo("成功", f"配置已导出到: {file_path}") logger.info(f"配置已导出到: {file_path}") except Exception as e: messagebox.showerror("错误", f"导出配置失败: {str(e)}") logger.error(f"导出配置失败: {str(e)}") def load_config(self): """从文件加载配置""" file_path = filedialog.askopenfilename( filetypes=[("JSON 文件", "*.json"), ("所有文件", "*.*")] ) if file_path: try: with open(file_path, &#39;r&#39;, encoding=&#39;utf-8&#39;) as f: config = eval(f.read()) self.prefix_var.set(config.get("prefix", "SCL_")) self.log_level_var.set(config.get("log_level", "INFO")) self.operation_mode.set(config.get("operation_mode", "stats")) self.tolerance_var.set(config.get("tolerance", 30)) self.checksheet_path_var.set(config.get("checksheet_path", "")) self.scl_folder_var.set(config.get("scl_folder", "")) # 加载SCL文件夹路径 self.change_log_level() messagebox.showinfo("成功", "配置已加载") logger.info(f"配置已从 {file_path} 加载") except Exception as e: messagebox.showerror("错误", f"加载配置失败: {str(e)}") logger.error(f"加载配置失败: {str(e)}") 隔离出SCL格式检查模式,检查SCL文件夹路径下的所有的SCL_前缀的文件,不依赖统计表
最新发布
08-12
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值