现在有个问题,我选择账号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))
最新发布