python笔记 基础语法·第16课 【模块调用,import,form...import..., if __name__ ==,系统内置模块,random,csv模块,writer写入】

本文详细介绍了Python中的模块概念,包括如何使用自己的模块和他人模块。重点讲解了import语句、from...import...语句以及if name == 'main'的作用。此外,还探讨了系统内置模块如random的使用,并提供了csv模块读写文件的示例。

模块是什么

定义模块:模块是最高级别的程序组织单元。
模块是【.py】为后缀的文件,模块可以存放变量、函数、类。
区别:定义变量需要“赋值”语句,封装函数需要“def”语句,封装类需要“class”语句,封装模块不需要任何语句。

如何使用自己的模块

如何使用:用“import”语句,可导入其他【.py】文件。

模块常用语句

import语句

注意:我们使用import语句导入一个模块,最主要的目的并不是运行模块中的执行语句,而是为了利用模块中已经封装好的变量、函数、类。
示例1【拆分导入模块并使用】
#原始模块

a = '我是模块中的变量a'

def hi():
    a = '我是函数里的变量a'
    print('函数“hi”已经运行!')

class Go1:  # 如果没有继承的类,class语句中可以省略括号,但定义函数的def语句括号不能省
    a = '我是类1中的变量a'
    @classmethod
    def do1(cls):
        print('函数“do1”已经运行!')

class Go2:
    a = '我是类2中的变量a'
    def do2(self):
        print('函数“do2”已经运行!')

print(a)  # 打印变量“a”

hi()  # 调用函数“hi”

print(Go1.a)  # 打印类属性“a”
Go1.do1()  # 调用类方法“Go1”

A = Go2()  # 实例化“Go2”类
print(A.a)  # 打印实例属性“a”
A.do2()  # 调用实例方法“do2”

#拆分模块
··变量、函数、类,放到test.py模块中··
··执行相关的语句放到main.py模块中··


                
现在有个问题,我选择账号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
<think>我们正在开发一个多平台数据采集工具,使用PyQt6作为GUI框架。问题描述:在切换平台(如从抖音切换到小红书)时,出现了账号数据重叠(即抖音的账号数据出现在小红书界面中)和后台采集任务未正确停止/切换的问题。 分析: 1. 账号数据重叠:可能是由于不同平台的账号数据没有完全隔离,切换平台时未清除前一个平台的数据或未加载新平台的数据。 2. 后台采集未正确处理:当切换平台时,原平台的后台采集任务可能还在运行,没有停止或切换为新平台的任务。 解决方案: 一、账号数据隔离 目标:确保每个平台的账号数据独立存储和显示,切换平台时加载对应平台的账号数据。 步骤: 1. 为每个平台创建独立的数据存储结构(例如,使用不同的配置文件、数据库表或数据类实例)。 例如: ```python class AccountManager: def __init__(self): # 平台名称到账号列表的映射 self.accounts = { 'douyin': [], 'xiaohongshu': [] } def get_accounts(self, platform): return self.accounts.get(platform, []) ``` 2. 在切换平台时,更新界面显示的账号列表: 在切换平台的信号(如QComboBox的currentIndexChanged信号)触发时,调用加载账号的方法。 ```python # 假设有一个平台选择的下拉框 self.platform_combo = QComboBox() self.platform_combo.addItems(['抖音', '小红书']) self.platform_combo.currentTextChanged.connect(self.switch_platform) def switch_platform(self, platform_name): # 将平台名称转换为内部标识 platform_id = self.get_platform_id(platform_name) # 从AccountManager获取该平台的账号列表 accounts = self.account_manager.get_accounts(platform_id) # 更新界面上的账号列表(例如QListWidget) self.account_list_widget.clear() for account in accounts: self.account_list_widget.addItem(account) ``` 3. 保存账号数据时,也要根据当前平台保存到对应的位置。 二、后台任务管理 目标:在切换平台时,确保原平台的后台采集任务停止,并可以启动新平台的任务(如果需要)。 步骤: 1. 设计后台任务管理器,能够管理多个平台的采集任务。 ```python class TaskManager: def __init__(self): self.tasks = {} # 存储平台对应的任务对象(或线程) def start_task(self, platform, task_func): # 停止该平台之前的任务(如果存在) self.stop_task(platform) # 创建并启动新任务(这里用线程示例) thread = threading.Thread(target=task_func) thread.daemon = True thread.start() self.tasks[platform] = thread def stop_task(self, platform): if platform in self.tasks: # 实际项目中可能需要更复杂的停止逻辑(如设置停止标志) # 这里简单演示 thread = self.tasks.pop(platform) # 如何停止线程需要根据具体任务设计,这里只是示例 # 通常需要线程内部有停止标志,这里无法直接停止,所以需要设计线程可停止 # 因此,我们通常使用一个Event来通知线程停止 # 下面会给出改进版本 pass # 改进的停止方法:要求任务线程使用Event来停止 def stop_task_safely(self, platform): if platform in self.tasks: thread, stop_event = self.tasks[platform] stop_event.set() # 设置停止事件 thread.join() # 等待线程结束 del self.tasks[platform] ``` 2. 修改任务函数,使其可以响应停止事件: ```python def data_collection_task(stop_event, platform): while not stop_event.is_set(): # 执行采集任务 time.sleep(1) # 模拟工作 ``` 3. 在任务管理器中启动任务时传入停止事件: ```python class TaskManager: def __init__(self): self.tasks = {} # 存储平台对应的任务对象(线程和停止事件) def start_task(self, platform, task_func): # 停止该平台之前的任务(如果存在) self.stop_task(platform) stop_event = threading.Event() thread = threading.Thread(target=task_func, args=(stop_event, platform)) thread.daemon = True thread.start() self.tasks[platform] = (thread, stop_event) def stop_task(self, platform): if platform in self.tasks: thread, stop_event = self.tasks.pop(platform) stop_event.set() thread.join(timeout=1.0) # 等待1秒 ``` 4. 切换平台时,停止当前平台的任务(如果有): ```python def switch_platform(self, platform_name): # 停止当前运行的任务(如果有) current_platform = self.get_current_platform_id() # 获取当前平台的标识 self.task_manager.stop_task(current_platform) # ... 然后切换账号数据等 ... # 如果需要,可以启动新平台的任务(比如自动采集) # 但通常切换平台后,用户需要手动启动采集,所以这里不自动启动 ``` 三、界面与逻辑分离 建议采用Model-View-Controller(MVC)或类似模式,将数据管理(Model)、界面(View)和逻辑控制(Controller)分离,这样切换平台时只需更新View和对应的Model。 四、注意事项 1. 线程安全:PyQt的界面操作必须在主线程,因此后台任务采集到的数据需要通过信号(Signal)传递给主线程更新界面。 2. 数据持久化:账号数据应保存到文件或数据库,每次启动时加载。 3. 异常处理:任务线程中应捕获异常,避免线程崩溃。 示例代码整合: 在PyQt6的主窗口中,我们可能这样组织: ```python class MainWindow(QMainWindow): def __init__(self): super().__init__() # 初始化账号管理器 self.account_manager = AccountManager() # 初始化任务管理器 self.task_manager = TaskManager() # 初始化界面 self.init_ui() def init_ui(self): # 创建平台选择下拉框 self.platform_combo = QComboBox() self.platform_combo.addItems(['抖音', '小红书']) self.platform_combo.currentTextChanged.connect(self.switch_platform) # ... 其他界面组件 ... def switch_platform(self, platform_name): # 将平台名称转换为内部标识(例如:'douyin', 'xiaohongshu') platform_id = self.translate_platform_name(platform_name) # 停止当前平台的任务(如果有) self.task_manager.stop_task(self.current_platform_id) # 需要记录当前平台ID # 更新当前平台ID self.current_platform_id = platform_id # 加载新平台的账号数据 accounts = self.account_manager.get_accounts(platform_id) self.update_account_list(accounts) def update_account_list(self, accounts): # 更新账号列表控件 pass ``` 通过以上措施,可以解决账号数据重叠和后台任务管理的问题。 引用说明:PyQt6的跨平台特性和强大的信号槽机制为多线程任务管理提供了便利[^1]。在开发数据采集工具时,合理的数据隔离和任务管理是保证稳定性的关键[^2]。 §§相关问题§§ 1. PyQt6中如何安全地进行多线程编程? 2. 如何设计跨平台的数据采集系统? 3. 在PyQt6中,如何实现不同界面之间的数据传递? 4. 如何为PyQt6应用设计持久化存储方案?</think># PyQt6多平台数据采集工具的账号隔离与后台任务管理解决方案 在PyQt6开发的多平台数据采集工具中,切换平台时出现账号数据重叠和后台采集未正确处理的问题,主要是由于账号管理机制和任务调度机制设计不当导致的。下面我将提供系统化的解决方案。 ## 问题核心分析 1. **账号数据重叠问题**:不同平台的账号数据未正确隔离 2. **后台采集未正确处理**:平台切换时任务未正确停止/启动 3. **数据持久化问题**:账号数据和采集状态未正确保存和恢复 ## 解决方案架构 ```mermaid graph TD A[用户界面] --> B[平台管理器] B --> C[账号隔离系统] B --> D[任务调度器] C --> E[平台1账号存储] C --> F[平台2账号存储] D --> G[平台1任务队列] D --> H[平台2任务队列] ``` ## 一、账号数据隔离方案 ### 1. 账号存储设计 ```python import json from PyQt6.QtCore import QStandardPaths, QDir class AccountManager: def __init__(self): self.app_data_dir = self._get_app_data_dir() self.accounts = {} # 存储格式:{'douyin': [账号1, 账号2], 'xiaohongshu': [...]} def _get_app_data_dir(self): """获取应用数据存储目录""" path = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation) if not QDir(path).exists(): QDir().mkpath(path) return path def load_accounts(self, platform): """加载指定平台的账号""" file_path = f"{self.app_data_dir}/{platform}_accounts.json" try: with open(file_path, 'r', encoding='utf-8') as f: return json.load(f) except FileNotFoundError: return [] def save_accounts(self, platform, accounts): """保存指定平台的账号""" file_path = f"{self.app_data_dir}/{platform}_accounts.json" with open(file_path, 'w', encoding='utf-8') as f: json.dump(accounts, f, ensure_ascii=False, indent=2) def switch_platform(self, platform): """切换平台时调用""" # 保存当前平台账号状态 if self.current_platform: self.save_accounts(self.current_platform, self.current_accounts) # 加载新平台账号 self.current_platform = platform self.current_accounts = self.load_accounts(platform) return self.current_accounts ``` ### 2. 界面账号隔离实现 ```python from PyQt6.QtWidgets import QComboBox, QListWidget class PlatformSwitcher(QComboBox): def __init__(self, account_manager, account_list_widget): super().__init__() self.account_manager = account_manager self.account_list_widget = account_list_widget # 添加平台选项 self.addItem("抖音", "douyin") self.addItem("小红书", "xiaohongshu") self.currentIndexChanged.connect(self.on_platform_changed) def on_platform_changed(self, index): platform = self.itemData(index) accounts = self.account_manager.switch_platform(platform) # 更新界面账号列表 self.account_list_widget.clear() for account in accounts: self.account_list_widget.addItem(account['username']) ``` ## 二、后台任务管理方案 ### 1. 任务调度器设计 ```python from PyQt6.QtCore import QObject, pyqtSignal, QThreadPool, QRunnable import time class DataCollectionTask(QRunnable): def __init__(self, platform, task_id): super().__init__() self.platform = platform self.task_id = task_id self.is_running = True def run(self): """任务执行逻辑""" while self.is_running: print(f"采集任务 {self.task_id} 在 {self.platform} 运行中...") time.sleep(1) # 模拟采集工作 def stop(self): """停止任务""" self.is_running = False class TaskScheduler(QObject): task_status_changed = pyqtSignal(str, str) # 信号:任务ID, 状态 def __init__(self): super().__init__() self.thread_pool = QThreadPool() self.thread_pool.setMaxThreadCount(5) # 最大并发任务数 self.active_tasks = {} # 当前活动任务 {平台: {任务ID: 任务对象}} def start_task(self, platform, task_id): """启动新任务""" # 停止同平台的所有任务 self.stop_platform_tasks(platform) # 创建并启动新任务 task = DataCollectionTask(platform, task_id) self.thread_pool.start(task) # 保存任务引用 if platform not in self.active_tasks: self.active_tasks[platform] = {} self.active_tasks[platform][task_id] = task self.task_status_changed.emit(task_id, "运行中") def stop_platform_tasks(self, platform): """停止指定平台的所有任务""" if platform in self.active_tasks: for task_id, task in self.active_tasks[platform].items(): task.stop() self.task_status_changed.emit(task_id, "已停止") del self.active_tasks[platform] def switch_platform(self, new_platform): """切换平台时调用""" # 停止所有非新平台的任务 for platform in list(self.active_tasks.keys()): if platform != new_platform: self.stop_platform_tasks(platform) ``` ### 2. 任务状态持久化 ```python class TaskScheduler(TaskScheduler): def __init__(self): super().__init__() self.task_history = [] # 任务历史记录 def save_task_state(self): """保存任务状态到文件""" state = { 'active_tasks': {}, 'task_history': self.task_history } # 序列化活动任务 for platform, tasks in self.active_tasks.items(): state['active_tasks'][platform] = [] for task_id, task in tasks.items(): state['active_tasks'][platform].append({ 'task_id': task_id, 'status': 'running' if task.is_running else 'stopped' }) # 保存到文件 with open('task_state.json', 'w') as f: json.dump(state, f) def load_task_state(self): """从文件加载任务状态""" try: with open('task_state.json', 'r') as f: state = json.load(f) self.task_history = state.get('task_history', []) # 恢复活动任务 for platform, tasks in state.get('active_tasks', {}).items(): for task_info in tasks: if task_info['status'] == 'running': self.start_task(platform, task_info['task_id']) except FileNotFoundError: pass ``` ## 三、系统集成与使用示例 ```python from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QListWidget, QPushButton class DataCollectionApp(QMainWindow): def __init__(self): super().__init__() # 初始化管理器 self.account_manager = AccountManager() self.task_scheduler = TaskScheduler() # 加载上次状态 self.task_scheduler.load_task_state() # 创建UI self.setWindowTitle("多平台数据采集工具") self.setGeometry(100, 100, 800, 600) layout = QVBoxLayout() # 平台切换器 self.platform_switcher = PlatformSwitcher( self.account_manager, self.account_list_widget ) layout.addWidget(self.platform_switcher) # 账号列表 self.account_list_widget = QListWidget() layout.addWidget(self.account_list_widget) # 任务控制按钮 self.start_button = QPushButton("开始采集") self.start_button.clicked.connect(self.start_collection) layout.addWidget(self.start_button) self.stop_button = QPushButton("停止采集") self.stop_button.clicked.connect(self.stop_collection) layout.addWidget(self.stop_button) container = QWidget() container.setLayout(layout) self.setCentralWidget(container) def start_collection(self): """开始采集任务""" current_platform = self.platform_switcher.currentData() task_id = f"task_{int(time.time())}" # 生成唯一任务ID self.task_scheduler.start_task(current_platform, task_id) def stop_collection(self): """停止当前平台采集任务""" current_platform = self.platform_switcher.currentData() self.task_scheduler.stop_platform_tasks(current_platform) def closeEvent(self, event): """应用关闭时保存状态""" self.account_manager.save_current_state() self.task_scheduler.save_task_state() super().closeEvent(event) if __name__ == "__main__": app = QApplication([]) window = DataCollectionApp() window.show() app.exec() ``` ## 四、最佳实践与优化建议 1. **数据加密存储**: ```python from cryptography.fernet import Fernet class SecureAccountManager(AccountManager): def __init__(self, encryption_key): super().__init__() self.cipher = Fernet(encryption_key) def save_accounts(self, platform, accounts): data = json.dumps(accounts).encode('utf-8') encrypted = self.cipher.encrypt(data) super().save_accounts(platform, encrypted) def load_accounts(self, platform): encrypted = super().load_accounts(platform) if encrypted: decrypted = self.cipher.decrypt(encrypted) return json.loads(decrypted.decode('utf-8')) return [] ``` 2. **任务优先级管理**: ```python class PriorityTaskScheduler(TaskScheduler): def start_task(self, platform, task_id, priority=0): # 根据优先级调整任务队列 pass ``` 3. **平台插件化架构**: ```python class PlatformPlugin: def __init__(self, platform_id): self.platform_id = platform_id def collect_data(self): """平台特定的采集逻辑""" pass def format_data(self, raw_data): """平台特定的数据格式化""" pass class DouyinPlugin(PlatformPlugin): def collect_data(self): # 抖音特定的采集逻辑 pass ``` 4. **资源监控与限制**: ```python import psutil class ResourceAwareTask(DataCollectionTask): def run(self): while self.is_running: # 检查CPU使用率 if psutil.cpu_percent() > 80: self.pause_task() # 采集逻辑... ``` ## 五、常见问题排查 1. **账号数据仍然重叠**: - 检查是否在切换平台时正确调用了`switch_platform`方法 - 验证文件存储路径是否正确隔离 - 确保没有全局变量存储账号数据 2. **后台任务未正确停止**: - 确认任务停止标志`is_running`被正确设置 - 检查线程池管理是否正常工作 - 添加任务日志以便追踪任务状态 3. **跨平台数据污染**: - 实现数据沙盒机制,每个平台在独立环境中运行 - 使用进程隔离代替线程隔离(通过`QProcess`) ```python from PyQt6.QtCore import QProcess class ProcessBasedTask: def __init__(self, platform, task_id): self.process = QProcess() self.process.start("python", ["platform_worker.py", platform, task_id]) def stop(self): self.process.terminate() self.process.waitForFinished(5000) ``` 通过以上方案,您的多平台数据采集工具将实现: 1. 严格的账号隔离机制 2. 可靠的后台任务管理 3. 跨平台数据持久化 4. 资源感知的任务调度 PyQt6的跨平台特性和强大的信号槽机制为这类复杂应用提供了坚实基础[^1],而合理的架构设计是保证数据隔离和任务调度的关键[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值