OnlineGPT项目分析
創建於:2024年12月21日
創建於:2024年12月21日
分析这个项目
文件结构:
OnlineGPT_7.1
├── gui_components.py
├── language_manager.py
├── LICENSE
├── main.exe
├── main.py
├── resources
│ ├── icon.ico
│ └── icon.png
├── search_app.log
├── search_app.py
├── search_engines.py
├── translations.py
├── utils.py
└── worker.py
import logging
from PyQt5.QtWidgets import (
QLineEdit, QTextEdit, QHeaderView, QStyleOptionButton,
QStyledItemDelegate, QApplication, QStyle
)
from PyQt5.QtCore import QObject, pyqtSignal, Qt, QRect
from PyQt5.QtGui import QTextCursor, QPainter, QFont
class GuiLogHandler(QObject, logging.Handler):
"""
自定义日志处理器,将日志写入GUI的QTextEdit控件。
"""
log_signal = pyqtSignal(str, str)
textdef __init__(self, text_edit): QObject.__init__(self) logging.Handler.__init__(self) self.text_edit = text_edit self.log_signal.connect(self.append_log) def emit(self, record): msg = self.format(record) # 根据日志级别设置不同的颜色 if record.levelno == logging.ERROR: color = '#ff4c4c' # 红色 elif record.levelno == logging.WARNING: color = '#ffae42' # 橙色 elif record.levelno == logging.INFO: color = '#4caf50' # 绿色 else: color = '#dcdcdc' # 默认颜色 # 发射信号,将日志信息传递到主线程 self.log_signal.emit(msg, color) def append_log(self, msg, color): """ 将日志消息以指定颜色添加到QTextEdit,并自动滚动到底部。 """ # 使用HTML格式插入带颜色的文本 self.text_edit.moveCursor(QTextCursor.End) self.text_edit.insertHtml(f'<span style="color:{color};">{msg}</span><br>') self.text_edit.moveCursor(QTextCursor.End) self.text_edit.ensureCursorVisible()
class MyLineEdit(QLineEdit):
"""
自定义的输入控件,增加特定的键盘事件处理。
"""
def init(self, parent=None):
super().init(parent)
self.setFont(QFont("微软雅黑", 12))
self.setStyleSheet("""
QLineEdit {
padding: 5px;
border: 1px solid #ccc;
border-radius: 4px;
}
QLineEdit:focus {
border: 1px solid #4caf50;
}
""")
textdef keyPressEvent(self, event): if event.key() == Qt.Key_Up: self.setCursorPosition(0) elif event.key() == Qt.Key_Down: self.setCursorPosition(len(self.text())) elif event.key() == Qt.Key_Delete: self.clear() else: super(MyLineEdit, self).keyPressEvent(event)
class MyTextEdit(QTextEdit):
"""
自定义的文本编辑控件,增加特定的键盘事件处理。
通过重写paste方法确保粘贴纯文本。
"""
def init(self, parent=None):
super().init(parent)
self.setFont(QFont("微软雅黑", 12))
self.setStyleSheet("""
QTextEdit {
padding: 5px;
border: 1px solid #ccc;
border-radius: 4px;
}
QTextEdit:focus {
border: 1px solid #4caf50;
}
""")
textdef keyPressEvent(self, event): # 检测是否为粘贴操作(Ctrl+V) if (event.modifiers() & Qt.ControlModifier) and (event.key() == Qt.Key_V): self.paste_plain_text() return elif event.key() == Qt.Key_Up: cursor = self.textCursor() cursor.movePosition(QTextCursor.Start) self.setTextCursor(cursor) elif event.key() == Qt.Key_Down: cursor = self.textCursor() cursor.movePosition(QTextCursor.End) self.setTextCursor(cursor) elif event.key() == Qt.Key_Delete: self.clear() else: super(MyTextEdit, self).keyPressEvent(event) def paste_plain_text(self): """ 仅粘贴纯文本,去除格式。 """ plain_text = QApplication.clipboard().text() self.insertPlainText(plain_text) def paste(self): """ 重写粘贴方法,确保仅粘贴纯文本。 """ self.paste_plain_text()
class CheckBoxHeader(QHeaderView):
"""
自定义QHeaderView,在第一列的表头添加一个复选框。
"""
checkBoxClicked = pyqtSignal(bool)
textdef __init__(self, orientation=Qt.Horizontal, parent=None): super(CheckBoxHeader, self).__init__(orientation, parent) self.isOn = False self.setSectionsClickable(True) self.sectionClicked.connect(self.onSectionClicked) def paintSection(self, painter, rect, logicalIndex): super(CheckBoxHeader, self).paintSection(painter, rect, logicalIndex) if logicalIndex == 0: option = QStyleOptionButton() option.rect = QRect( rect.x() + 5, rect.y() + (rect.height() - 20) // 2, 20, 20 ) option.state = QStyle.State_Enabled | QStyle.State_Active if self.isOn: option.state |= QStyle.State_On else: option.state |= QStyle.State_Off self.style().drawControl(QStyle.CE_CheckBox, option, painter) def onSectionClicked(self, logicalIndex): if logicalIndex == 0: self.isOn = not self.isOn self.checkBoxClicked.emit(self.isOn) self.updateSection(0)
class CenteredCheckBoxDelegate(QStyledItemDelegate):
"""
自定义委托,用于在单元格中居中显示复选框。
"""
def init(self, parent=None):
super(CenteredCheckBoxDelegate, self).init(parent)
textdef paint(self, painter, option, index): if index.column() == 0: # 获取复选框的状态 checked = index.data(Qt.CheckStateRole) == Qt.Checked check_box_style_option = QStyleOptionButton() if checked: check_box_style_option.state |= QStyle.State_On else: check_box_style_option.state |= QStyle.State_Off check_box_style_option.state |= QStyle.State_Enabled # 计算复选框的位置,使其居中 check_box_rect = self.getCheckBoxRect(option) check_box_style_option.rect = check_box_rect # 绘制复选框 QApplication.style().drawControl(QStyle.CE_CheckBox, check_box_style_option, painter) else: super(CenteredCheckBoxDelegate, self).paint(painter, option, index) def getCheckBoxRect(self, option): # 获取复选框的尺寸 check_box_style_option = QStyleOptionButton() check_box_rect = QApplication.style().subElementRect( QStyle.SE_CheckBoxIndicator, check_box_style_option, None ) # 计算复选框的位置,使其居中 x = option.rect.x() + (option.rect.width() - check_box_rect.width()) / 2 y = option.rect.y() + (option.rect.height() - check_box_rect.height()) / 2 return QRect(int(x), int(y), check_box_rect.width(), check_box_rect.height())
import logging
from translations import translations
class LanguageManager:
"""
语言管理器,负责管理当前语言和提供翻译功能。
"""
def init(self):
self.current_language = 'zh' # 默认语言为中文
textdef set_language(self, language_code): if language_code in translations: self.current_language = language_code else: logging.warning(f"尝试设置未知语言: {language_code}") def tr(self, key): return translations[self.current_language].get(key, key)
MIT License
Copyright (c) 2024 yeahhe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
import sys
import logging
from PyQt5.QtWidgets import QApplication
from search_app import SearchApp
def main():
"""
程序主入口。
"""
app = QApplication(sys.argv)
window = SearchApp()
window.show()
sys.exit(app.exec())
if name == "main":
# 配置日志
logging.basicConfig(
filename='search_app.log',
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
main()
2024-12-17 21:41:22,842 - ERROR - ͼļ: C:\Users\admin\AppData\Local\Temp_MEI190202\resources\icon.png
2024-12-17 21:45:03,930 - ERROR - ͼļ: C:\Users\admin\AppData\Local\Temp_MEI203602\resources\icon.png
2024-12-17 22:17:58,602 - ERROR - ͼļ: C:\Users\admin\AppData\Local\Temp_MEI102162\resources\icon.png
2024-12-17 22:19:41,428 - ERROR - ͼļ: C:\Users\admin\AppData\Local\Temp_MEI77242\resources\icon.png
2024-12-17 22:23:50,901 - ERROR - ͼļ: C:\Users\admin\AppData\Local\Temp_MEI103922\resources\icon.png
2024-12-17 22:23:55,058 - INFO - ٵ 4
2024-12-17 22:23:56,222 - INFO - ӵ 5
2024-12-17 22:28:15,961 - ERROR - ͼļ: C:\Users\admin\AppData\Local\Temp_MEI156722\resources\icon.png
2024-12-17 23:30:23,869 - ERROR - ͼļ: C:\Users\admin\AppData\Local\Temp_MEI195442\resources\icon.png
2024-12-17 23:45:05,257 - ERROR - ͼļ: C:\Users\admin\AppData\Local\Temp_MEI103162\resources\icon.png
2024-12-17 23:47:10,962 - ERROR - ͼļ: C:\Users\admin\AppData\Local\Temp_MEI29762\resources\icon.png
2024-12-17 23:47:27,540 - ERROR - ͼļ: C:\Users\admin\AppData\Local\Temp_MEI117722\resources\icon.png
2024-12-17 23:50:57,523 - ERROR - ͼļ: C:\Users\admin\AppData\Local\Temp_MEI39442\resources\icon.png
2024-12-17 23:51:28,674 - ERROR - ͼļ: C:\Users\admin\AppData\Local\Temp_MEI203282\resources\icon.png
2024-12-18 00:01:56,480 - ERROR - ͼļ: C:\Users\admin\AppData\Local\Temp_MEI23362\resources\icon.png
2024-12-18 00:16:10,434 - ERROR - ͼļ: C:\Users\admin\AppData\Local\Temp_MEI67322\resources\icon.png
import sys
import logging
import os
from PyQt5.QtWidgets import (
QApplication, QWidget, QLabel, QPushButton, QTextEdit, QVBoxLayout,
QHBoxLayout, QMessageBox, QFileDialog, QProgressBar, QTableWidget,
QTableWidgetItem, QGroupBox, QHeaderView, QComboBox, QCheckBox,
QGridLayout, QSplitter, QShortcut, QFrame, QAction, QMenuBar
)
from PyQt5.QtCore import Qt, QThread, QUrl
from PyQt5.QtGui import QFont, QIcon, QDesktopServices, QKeySequence
from worker import Worker
from gui_components import GuiLogHandler, MyLineEdit, MyTextEdit, CheckBoxHeader, CenteredCheckBoxDelegate
from utils import save_results_to_txt, generate_txt_content
from language_manager import LanguageManager # 引入语言管理器
class SearchApp(QWidget):
def init(self):
super().init()
self.language_manager = LanguageManager()
self.thread = None
self.saved_file = None
self.all_results = []
self.current_content = ""
self.init_ui()
textdef init_ui(self): """ 初始化用户界面。 """ label_font = QFont("微软雅黑", 12) input_font = QFont("微软雅黑", 12) # 将按钮的字体从12改为10 button_font = QFont("微软雅黑", 10) status_font = QFont("微软雅黑", 10) table_font = QFont("微软雅黑", 10) log_font = QFont("Consolas", 10) main_layout = QVBoxLayout() main_layout.setContentsMargins(20, 20, 20, 20) main_layout.setSpacing(15) # 创建菜单栏 self.menu_bar = QMenuBar(self) language_menu_title = "Language" if self.language_manager.current_language == 'en' else "语言" language_menu = self.menu_bar.addMenu(language_menu_title) english_action = QAction("English", self) chinese_action = QAction("中文", self) language_menu.addAction(english_action) language_menu.addAction(chinese_action) english_action.triggered.connect(lambda: self.change_language('en')) chinese_action.triggered.connect(lambda: self.change_language('zh')) # 帮助菜单 help_menu_title = self.language_manager.tr('help') help_menu = self.menu_bar.addMenu(help_menu_title) about_action = QAction(self.language_manager.tr('about'), self) help_menu.addAction(about_action) about_action.triggered.connect(self.show_about_dialog) main_layout.setMenuBar(self.menu_bar) # 搜索设置分组框 search_group = QGroupBox(self.language_manager.tr('search_settings')) search_group.setObjectName("search_group") search_layout = QGridLayout() search_layout.setSpacing(10) search_group.setLayout(search_layout) # 进阶模式复选框 self.advanced_mode_checkbox = QCheckBox(self.language_manager.tr('advanced_mode')) self.advanced_mode_checkbox.setFont(label_font) self.advanced_mode_checkbox.setToolTip(self.language_manager.tr('advanced_mode')) self.advanced_mode_checkbox.stateChanged.connect(self.on_advanced_mode_changed) # 搜索引擎选择 self.engine_label = QLabel(self.language_manager.tr('search_engine')) self.engine_label.setFont(label_font) self.engine_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.engine_combo = QComboBox() self.engine_combo.setFont(input_font) self.engines = { "Google": "Google", "Bing": "Bing", "百度": "百度" } for engine in self.engines.keys(): self.engine_combo.addItem(engine) self.engine_combo.setCurrentText("Google") self.engine_combo.setToolTip(self.language_manager.tr('search_engine')) # 搜索结果数量 self.result_num_label = QLabel(self.language_manager.tr('search_number')) self.result_num_label.setFont(label_font) self.result_num_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.decrement_button = QPushButton(self.language_manager.tr('decrement')) self.decrement_button.setFont(button_font) self.decrement_button.setFixedWidth(30) self.decrement_button.setStyleSheet(""" QPushButton { background-color: #ff5722; color: white; border: none; border-radius: 4px; } QPushButton:hover { background-color: #e64a19; } QPushButton:pressed { background-color: #d84315; } """) self.decrement_button.setToolTip(self.language_manager.tr('decrement')) self.decrement_button.clicked.connect(self.on_decrement) self.result_num_value = 5 # 默认5 self.result_num_display = QLabel(str(self.result_num_value)) self.result_num_display.setFont(input_font) self.result_num_display.setAlignment(Qt.AlignCenter) self.result_num_display.setFixedWidth(30) self.result_num_display.setStyleSheet(""" QLabel { border: 1px solid #ccc; border-radius: 4px; padding: 5px; background-color: #f0f0f0; } """) self.increment_button = QPushButton(self.language_manager.tr('increment')) self.increment_button.setFont(button_font) self.increment_button.setFixedWidth(30) self.increment_button.setStyleSheet(""" QPushButton { background-color: #4caf50; color: white; border: none; border-radius: 4px; } QPushButton:hover { background-color: #43a047; } QPushButton:pressed { background-color: #388e3c; } """) self.increment_button.setToolTip(self.language_manager.tr('increment')) self.increment_button.clicked.connect(self.on_increment) # 布局搜索数量控件 result_num_layout = QHBoxLayout() result_num_layout.addWidget(self.decrement_button) result_num_layout.addWidget(self.result_num_display) result_num_layout.addWidget(self.increment_button) result_num_layout.setSpacing(5) # 复制按钮 self.copy_button = QPushButton(self.language_manager.tr('copy')) self.copy_button.setFont(button_font) self.copy_button.setFixedWidth(80) # 原为80,不变 self.copy_button.setStyleSheet(""" QPushButton { background-color: #ff9800; color: white; border: none; border-radius: 4px; padding: 8px 16px; } QPushButton:hover { background-color: #fb8c00; } QPushButton:pressed { background-color: #f57c00; } """) self.copy_button.setToolTip(self.language_manager.tr('copy')) self.copy_button.clicked.connect(self.on_copy_click) self.copy_button.setEnabled(False) # 清空按钮 self.clear_button = QPushButton(self.language_manager.tr('clear')) self.clear_button.setFont(button_font) self.clear_button.setFixedWidth(60) # 减小为60,不变 self.clear_button.setStyleSheet(""" QPushButton { background-color: #9e9e9e; color: white; border: none; border-radius: 4px; padding: 8px 16px; } QPushButton:hover { background-color: #757575; } QPushButton:pressed { background-color: #616161; } """) self.clear_button.setToolTip(self.language_manager.tr('clear')) self.clear_button.clicked.connect(self.on_clear_click) copy_and_clear_layout = QHBoxLayout() copy_and_clear_layout.addWidget(self.copy_button) copy_and_clear_layout.addWidget(self.clear_button) copy_and_clear_layout.setSpacing(10) search_num_layout = QHBoxLayout() search_num_layout.addWidget(self.result_num_label) search_num_layout.addLayout(result_num_layout) search_num_layout.addLayout(copy_and_clear_layout) search_num_layout.setSpacing(20) # 添加到搜索布局(第一行) search_layout.addWidget(self.advanced_mode_checkbox, 0, 0) engine_layout = QHBoxLayout() engine_layout.addWidget(self.engine_label) engine_layout.addWidget(self.engine_combo) engine_layout.setSpacing(5) search_layout.addLayout(engine_layout, 0, 1) search_layout.addLayout(search_num_layout, 0, 2, 1, 2) # 普通模式搜索输入框 self.search_label = QLabel(self.language_manager.tr('search_keywords')) self.search_label.setFont(label_font) self.search_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.search_input = MyLineEdit() self.search_input.setFont(input_font) self.search_input.setPlaceholderText(self.language_manager.tr('search_placeholder')) self.search_input.setToolTip(self.language_manager.tr('search_keywords')) self.search_input.setMinimumWidth(360) # 缩小为360px(原400px) # 进阶模式搜索输入框 self.search_input_advanced = MyTextEdit() self.search_input_advanced.setFont(input_font) self.search_input_advanced.setPlaceholderText(self.language_manager.tr('search_placeholder_advanced')) self.search_input_advanced.setVisible(False) self.search_input_advanced.setToolTip(self.language_manager.tr('search_keywords')) self.search_input_advanced.setMinimumWidth(360) # 同步缩小为360px # 自定义问题(仅进阶模式) self.question_label = QLabel(self.language_manager.tr('custom_question')) self.question_label.setFont(label_font) self.question_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.question_input = QTextEdit() self.question_input.setFont(input_font) self.question_input.setPlaceholderText(self.language_manager.tr('question_placeholder')) self.question_label.setVisible(False) self.question_input.setVisible(False) self.question_input.setToolTip(self.language_manager.tr('custom_question')) # 搜索按钮 self.search_button = QPushButton(self.language_manager.tr('search')) self.search_button.setFont(button_font) self.search_button.setFixedWidth(90) # 原80增加到90 self.search_button.setStyleSheet(""" QPushButton { background-color: #4caf50; color: white; border: none; border-radius: 4px; padding: 8px 16px; } QPushButton:hover { background-color: #45a049; } QPushButton:pressed { background-color: #3e8e41; } """) self.search_button.setToolTip(self.language_manager.tr('search')) self.search_button.clicked.connect(self.on_search_click) # 中断按钮 self.interrupt_button = QPushButton(self.language_manager.tr('interrupt')) self.interrupt_button.setFont(button_font) self.interrupt_button.setFixedWidth(90) # 原80增加到90 self.interrupt_button.setStyleSheet(""" QPushButton { background-color: #f44336; color: white; border: none; border-radius: 4px; padding: 8px 16px; } QPushButton:hover { background-color: #da190b; } QPushButton:pressed { background-color: #b71c1c; } """) self.interrupt_button.setToolTip(self.language_manager.tr('interrupt')) self.interrupt_button.clicked.connect(self.on_interrupt_click) self.interrupt_button.setEnabled(False) self.search_input.returnPressed.connect(self.on_search_click) # 添加 Ctrl+C 快捷键 self.interrupt_shortcut = QShortcut(QKeySequence("Ctrl+C"), self) self.interrupt_shortcut.setContext(Qt.ApplicationShortcut) self.interrupt_shortcut.activated.connect(self.on_interrupt_click) # 保存按钮 self.save_button = QPushButton(self.language_manager.tr('save_results')) self.save_button.setFont(button_font) self.save_button.setFixedWidth(90) # 原80增加到90 self.save_button.setStyleSheet(""" QPushButton { background-color: #2196F3; color: white; border: none; border-radius: 4px; padding: 8px 16px; } QPushButton:hover { background-color: #0b7dda; } QPushButton:pressed { background-color: #095a9d; } """) self.save_button.setToolTip(self.language_manager.tr('save_results')) self.save_button.clicked.connect(self.on_save_click) self.save_button.setEnabled(False) # 打开结果按钮 self.open_button = QPushButton(self.language_manager.tr('open_results')) self.open_button.setFont(button_font) self.open_button.setFixedWidth(90) # 原80增加到90 self.open_button.setStyleSheet(""" QPushButton { background-color: #9c27b0; color: white; border: none; border-radius: 4px; padding: 8px 16px; } QPushButton:hover { background-color: #7b1fa2; } QPushButton:pressed { background-color: #4a0072; } """) self.open_button.setToolTip(self.language_manager.tr('open_results')) self.open_button.clicked.connect(self.on_open_click) self.open_button.setEnabled(False) self.input_button_layout = QHBoxLayout() self.input_button_layout.addWidget(self.search_input) self.input_button_layout.addWidget(self.search_input_advanced) self.button_layout = QHBoxLayout() self.button_layout.addWidget(self.search_button) self.button_layout.addWidget(self.interrupt_button) self.button_layout.addStretch() self.button_layout.addWidget(self.save_button) self.button_layout.addWidget(self.open_button) self.input_button_layout.addStretch(1) self.input_button_layout.addLayout(self.button_layout) # 将搜索关键词和问题添加到布局 search_layout.addWidget(self.search_label, 1, 0) search_layout.addLayout(self.input_button_layout, 1, 1, 1, 3) search_layout.addWidget(self.question_label, 2, 0) search_layout.addWidget(self.question_input, 2, 1, 1, 3) search_layout.setColumnStretch(0, 0) search_layout.setColumnStretch(1, 1) search_layout.setColumnStretch(2, 1) search_layout.setColumnStretch(3, 1) self.status_label = QLabel(self.language_manager.tr('status_waiting')) self.status_label.setFont(status_font) self.status_label.setAlignment(Qt.AlignLeft) self.status_label.setStyleSheet("color: #555;") self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 0) self.progress_bar.setVisible(False) self.progress_bar.setFixedHeight(15) self.progress_bar.setStyleSheet(""" QProgressBar { border: 1px solid #bbb; background-color: #eee; height: 15px; border-radius: 7px; } QProgressBar::chunk { background-color: #4caf50; border-radius: 7px; } """) # 结果显示区 self.result_table = QTableWidget() self.result_table.setColumnCount(5) self.result_table.setHorizontalHeaderLabels([ self.language_manager.tr('copy'), "URL", "Title", "Snippet", "Content" ]) self.checkbox_header = CheckBoxHeader() self.result_table.setHorizontalHeader(self.checkbox_header) self.checkbox_header.checkBoxClicked.connect(self.on_header_checkbox_clicked) self.result_table.verticalHeader().setVisible(False) self.result_table.setEditTriggers(QTableWidget.NoEditTriggers) self.result_table.setSelectionBehavior(QTableWidget.SelectRows) self.result_table.setSelectionMode(QTableWidget.SingleSelection) self.result_table.setAlternatingRowColors(True) self.result_table.setFont(table_font) self.result_table.setStyleSheet(""" QTableWidget { background-color: #fff; alternate-background-color: #f9f9f9; } QHeaderView::section { background-color: #f0f0f0; padding: 6px; border: 1px solid #d6d6d6; font-weight: bold; } QTableWidget::item:selected { background-color: #cce5ff; } """) self.result_table.setColumnWidth(0, 40) self.result_table.setColumnWidth(1, 250) self.result_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Fixed) self.result_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Fixed) for i in range(2, 5): self.result_table.horizontalHeader().setSectionResizeMode(i, QHeaderView.Stretch) self.result_table.setItemDelegateForColumn(0, CenteredCheckBoxDelegate()) self.result_table.cellClicked.connect(self.on_result_cell_clicked) log_group = QGroupBox("日志" if self.language_manager.current_language == 'zh' else "Logs") log_group.setObjectName("log_group") log_layout = QVBoxLayout() self.log_text = QTextEdit() self.log_text.setReadOnly(True) self.log_text.setFont(log_font) self.log_text.setStyleSheet(""" QTextEdit { background-color: #1e1e1e; color: #dcdcdc; border: 1px solid #555; border-radius: 4px; } """) log_layout.addWidget(self.log_text) log_group.setLayout(log_layout) splitter = QSplitter(Qt.Vertical) splitter.addWidget(self.result_table) splitter.addWidget(log_group) splitter.setSizes([600, 300]) main_layout.addWidget(search_group) main_layout.addWidget(self.status_label) main_layout.addWidget(self.progress_bar) main_layout.addWidget(splitter) self.setLayout(main_layout) self.setup_logging() self.setWindowTitle(self.language_manager.tr('window_title')) # 使用 __file__ 定位资源文件 script_dir = os.path.dirname(os.path.abspath(__file__)) icon_path = os.path.join(script_dir, 'resources', 'icon.png') print(f"尝试加载图标路径: {icon_path}") if not os.path.exists(icon_path): logging.error(f"图标文件不存在: {icon_path}") QMessageBox.critical(self, "错误", f"图标文件不存在: {icon_path}") else: try: self.setWindowIcon(QIcon(icon_path)) logging.info(f"已设置窗口图标: {icon_path}") print(f"已设置窗口图标: {icon_path}") except Exception as e: logging.error(f"设置窗口图标时出错: {e}") QMessageBox.critical(self, "错误", f"设置窗口图标时出错: {e}") def setup_logging(self): gui_handler = GuiLogHandler(self.log_text) gui_handler.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') gui_handler.setFormatter(formatter) logging.getLogger().addHandler(gui_handler) logging.getLogger().setLevel(logging.DEBUG) def change_language(self, language_code): if language_code in ['en', 'zh']: if language_code == self.language_manager.current_language: return self.language_manager.set_language(language_code) logging.info(f"语言更改为: {'English' if language_code == 'en' else '中文'}") self.update_ui_texts() else: logging.warning(f"尝试设置未知语言: {language_code}") def update_ui_texts(self): self.setWindowTitle(self.language_manager.tr('window_title')) language_menu = self.menu_bar.actions()[0].menu() language_menu.setTitle("Language" if self.language_manager.current_language == 'en' else "语言") language_menu.actions()[0].setText("English" if self.language_manager.current_language == 'en' else "英文") language_menu.actions()[1].setText("中文" if self.language_manager.current_language == 'en' else "中文") help_menu = self.menu_bar.actions()[1].menu() help_menu.setTitle(self.language_manager.tr('help')) help_menu.actions()[0].setText(self.language_manager.tr('about')) search_group = self.findChild(QGroupBox, "search_group") if search_group: search_group.setTitle(self.language_manager.tr('search_settings')) self.advanced_mode_checkbox.setText(self.language_manager.tr('advanced_mode')) self.advanced_mode_checkbox.setToolTip(self.language_manager.tr('advanced_mode')) self.engine_label.setText(self.language_manager.tr('search_engine')) self.engine_combo.setToolTip(self.language_manager.tr('search_engine')) self.result_num_label.setText(self.language_manager.tr('search_number')) self.decrement_button.setText(self.language_manager.tr('decrement')) self.decrement_button.setToolTip(self.language_manager.tr('decrement')) self.increment_button.setText(self.language_manager.tr('increment')) self.increment_button.setToolTip(self.language_manager.tr('increment')) self.copy_button.setText(self.language_manager.tr('copy')) self.copy_button.setToolTip(self.language_manager.tr('copy')) self.clear_button.setText(self.language_manager.tr('clear')) self.clear_button.setToolTip(self.language_manager.tr('clear')) self.search_button.setText(self.language_manager.tr('search')) self.search_button.setToolTip(self.language_manager.tr('search')) self.interrupt_button.setText(self.language_manager.tr('interrupt')) self.interrupt_button.setToolTip(self.language_manager.tr('interrupt')) self.save_button.setText(self.language_manager.tr('save_results')) self.save_button.setToolTip(self.language_manager.tr('save_results')) self.open_button.setText(self.language_manager.tr('open_results')) self.open_button.setToolTip(self.language_manager.tr('open_results')) self.search_label.setText(self.language_manager.tr('search_keywords')) if self.advanced_mode_checkbox.isChecked(): self.search_input_advanced.setPlaceholderText(self.language_manager.tr('search_placeholder_advanced')) self.search_input_advanced.setToolTip(self.language_manager.tr('search_keywords')) else: self.search_input.setPlaceholderText(self.language_manager.tr('search_placeholder')) self.search_input.setToolTip(self.language_manager.tr('search_keywords')) self.question_label.setText(self.language_manager.tr('custom_question')) self.question_input.setPlaceholderText(self.language_manager.tr('question_placeholder')) self.question_input.setToolTip(self.language_manager.tr('custom_question')) self.status_label.setText(self.language_manager.tr('status_waiting')) self.result_table.setHorizontalHeaderLabels([ self.language_manager.tr('copy'), "URL", "Title", "Snippet", "Content" ]) log_group = self.findChild(QGroupBox, "log_group") if log_group: log_group.setTitle("Logs" if self.language_manager.current_language == 'en' else "日志") def show_about_dialog(self): about_title = self.language_manager.tr('about_title') about_message = self.language_manager.tr('about_message') QMessageBox.about(self, about_title, about_message) def on_advanced_mode_changed(self, state): is_advanced = (state == Qt.Checked) logging.info(f"进阶模式 {'开启' if is_advanced else '关闭'}") if is_advanced: self.search_input.setVisible(False) self.search_input_advanced.setVisible(True) self.question_label.setVisible(True) self.question_input.setVisible(True) self.search_input_advanced.setFocus() else: self.search_input.setVisible(True) self.search_input_advanced.setVisible(False) self.question_label.setVisible(False) self.question_input.setVisible(False) self.search_input.setFocus() def on_increment(self): if self.result_num_value < 20: self.result_num_value += 1 self.result_num_display.setText(str(self.result_num_value)) logging.info(f"搜索数量增加到 {self.result_num_value}") def on_decrement(self): if self.result_num_value > 1: self.result_num_value -= 1 self.result_num_display.setText(str(self.result_num_value)) logging.info(f"搜索数量减少到 {self.result_num_value}") def on_clear_click(self): if self.advanced_mode_checkbox.isChecked(): self.search_input_advanced.clear() else: self.search_input.clear() logging.info("已清空搜索关键词输入框。") def on_search_click(self): is_advanced = self.advanced_mode_checkbox.isChecked() if is_advanced: keywords_text = self.search_input_advanced.toPlainText().strip() if not keywords_text: QMessageBox.warning(self, self.language_manager.tr('input_error'), self.language_manager.tr('input_error_empty_keyword')) logging.warning("空关键词搜索(进阶模式)。") return queries = [line.strip() for line in keywords_text.splitlines() if line.strip()] if not queries: QMessageBox.warning(self, self.language_manager.tr('input_error'), self.language_manager.tr('input_error_empty_keyword')) logging.warning("空关键词搜索(进阶模式)。") return custom_question = self.question_input.toPlainText().strip() if not custom_question: QMessageBox.warning(self, self.language_manager.tr('input_error'), self.language_manager.tr('input_error_empty_question')) logging.warning("进阶模式下无自定义问题输入。") return else: query = self.search_input.text().strip() if not query: QMessageBox.warning(self, self.language_manager.tr('input_error'), self.language_manager.tr('input_error_empty_keyword')) logging.warning("空关键词搜索。") return queries = [query] custom_question = None num_results = self.result_num_value engine_display = self.engine_combo.currentText() engine = self.engines.get(engine_display, 'Google') logging.info(f"开始搜索,关键词: {queries}, 数量: {num_results}, 引擎: {engine}") self.search_button.setEnabled(False) self.open_button.setEnabled(False) self.save_button.setEnabled(False) self.copy_button.setEnabled(False) self.interrupt_button.setEnabled(True) self.search_input.setEnabled(False) self.search_input_advanced.setEnabled(False) self.question_input.setEnabled(False) self.increment_button.setEnabled(False) self.decrement_button.setEnabled(False) self.engine_combo.setEnabled(False) self.result_table.setRowCount(0) self.status_label.setText(self.language_manager.tr('status_searching')) self.progress_bar.setVisible(True) self.thread = QThread() self.worker = Worker(queries, num_results, engine, custom_question) self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.on_search_complete) self.worker.error.connect(self.on_search_error) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.thread.start() def on_interrupt_click(self): if self.thread and self.thread.isRunning(): logging.info("用户中断搜索任务。") self.worker.stop() self.thread.quit() self.thread.wait() self.interrupt_button.setEnabled(False) self.status_label.setText(self.language_manager.tr('interrupt_info_task_interrupted')) self.search_button.setEnabled(True) self.open_button.setEnabled(False) self.save_button.setEnabled(False) self.copy_button.setEnabled(False) self.search_input.setEnabled(True) self.search_input_advanced.setEnabled(True) self.question_input.setEnabled(True) self.increment_button.setEnabled(True) self.decrement_button.setEnabled(True) self.engine_combo.setEnabled(True) else: logging.warning("无正在运行的搜索任务可中断。") QMessageBox.information(self, self.language_manager.tr('input_error'), self.language_manager.tr('interrupt_info_no_task')) def on_search_complete(self, results, filename): self.progress_bar.setVisible(False) self.interrupt_button.setEnabled(False) if results: try: self.saved_file = filename self.all_results = results logging.info("搜索结果已成功获取。") except Exception as e: logging.error(f"保存文件时出错:{e}") QMessageBox.critical(self, self.language_manager.tr('save_failure'), f"{self.language_manager.tr('save_failure').format(e)}") self.status_label.setText(self.language_manager.tr('status_search_failed')) self.reset_ui_after_search_failure() return self.result_table.setRowCount(0) for idx, result in enumerate(results): self.result_table.insertRow(idx) checkbox_item = QTableWidgetItem() checkbox_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) checkbox_item.setCheckState(Qt.Checked) url_item = QTableWidgetItem(result['link']) url_item.setFont(QFont("微软雅黑", 10)) title_item = QTableWidgetItem(result['title']) title_item.setFont(QFont("微软雅黑", 10)) snippet_item = QTableWidgetItem(result['snippet']) snippet_item.setFont(QFont("微软雅黑", 9)) content_item = QTableWidgetItem(result['content']) content_item.setFont(QFont("微软雅黑", 9)) self.result_table.setItem(idx, 0, checkbox_item) self.result_table.setItem(idx, 1, url_item) self.result_table.setItem(idx, 2, title_item) self.result_table.setItem(idx, 3, snippet_item) self.result_table.setItem(idx, 4, content_item) self.result_table.itemChanged.connect(self.on_checkbox_state_changed) self.update_saved_content() self.status_label.setText(self.language_manager.tr('status_search_complete')) self.save_button.setEnabled(True) self.open_button.setEnabled(True) self.copy_button.setEnabled(True) self.search_button.setEnabled(True) self.search_input.setEnabled(True) self.search_input_advanced.setEnabled(True) self.question_input.setEnabled(True) self.increment_button.setEnabled(True) self.decrement_button.setEnabled(True) self.engine_combo.setEnabled(True) if self.advanced_mode_checkbox.isChecked(): self.search_input_advanced.setFocus() else: self.search_input.setFocus() logging.info("搜索完成,结果已展示。") self.copy_results_silently() else: self.result_table.setRowCount(0) self.status_label.setText(self.language_manager.tr('status_search_failed')) QMessageBox.information(self, self.language_manager.tr('input_error'), self.language_manager.tr('status_search_failed')) logging.info("搜索完成但无结果。") self.reset_ui_after_search_failure() def on_search_error(self, error_message): self.progress_bar.setVisible(False) self.result_table.setRowCount(0) self.status_label.setText(self.language_manager.tr('status_search_failed')) QMessageBox.critical(self, self.language_manager.tr('input_error'), f"{self.language_manager.tr('status_search_failed')}\n{error_message}") logging.error(f"搜索错误:{error_message}") self.reset_ui_after_search_failure() def reset_ui_after_search_failure(self): self.search_button.setEnabled(True) self.open_button.setEnabled(False) self.save_button.setEnabled(False) self.copy_button.setEnabled(False) self.search_input.setEnabled(True) self.search_input_advanced.setEnabled(True) self.question_input.setEnabled(True) self.increment_button.setEnabled(True) self.decrement_button.setEnabled(True) self.engine_combo.setEnabled(True) self.interrupt_button.setEnabled(False) if self.advanced_mode_checkbox.isChecked(): self.search_input_advanced.setFocus() else: self.search_input.setFocus() def on_save_click(self): is_advanced = self.advanced_mode_checkbox.isChecked() if is_advanced: queries = [line.strip() for line in self.search_input_advanced.toPlainText().strip().splitlines() if line.strip()] custom_question = self.question_input.toPlainText().strip() else: query = self.search_input.text().strip() queries = [query] custom_question = None if not queries: QMessageBox.warning(self, self.language_manager.tr('input_error'), self.language_manager.tr('save_error_no_keyword')) logging.warning("试图保存结果但无关键词。") return downloads_path = os.path.join(os.path.expanduser('~'), 'Downloads') default_filename = os.path.join(downloads_path, "search_results.txt") options = QFileDialog.Options() filename, _ = QFileDialog.getSaveFileName( self, self.language_manager.tr('save_results'), default_filename, "Text Files (*.txt);;All Files (*)", options=options ) if filename: logging.info(f"用户选择保存文件路径:{filename}") try: selected_results = self.get_selected_results() save_results_to_txt( selected_results, ', '.join(queries), filename=filename, engine=self.engine_combo.currentText(), custom_question=custom_question, language=self.language_manager.current_language ) QMessageBox.information(self, self.language_manager.tr('save_success').format(filename), self.language_manager.tr('save_success').format(filename)) logging.info(f"结果成功保存到 {filename}") self.saved_file = filename self.open_button.setEnabled(True) except Exception as e: QMessageBox.critical(self, self.language_manager.tr('save_failure'), f"{self.language_manager.tr('save_failure').format(e)}") logging.error(f"保存文件时出错:{e}") def on_open_click(self): if self.saved_file and os.path.exists(self.saved_file): logging.info(f"打开文件:{self.saved_file}") QDesktopServices.openUrl(QUrl.fromLocalFile(self.saved_file)) else: QMessageBox.warning(self, self.language_manager.tr('input_error'), self.language_manager.tr('open_error_no_file')) logging.warning("尝试打开文件但不存在或未保存。") def on_copy_click(self): selected_results = self.get_selected_results() if selected_results: is_advanced = self.advanced_mode_checkbox.isChecked() if is_advanced: queries = [line.strip() for line in self.search_input_advanced.toPlainText().strip().splitlines() if line.strip()] custom_question = self.question_input.toPlainText().strip() else: query = self.search_input.text().strip() queries = [query] custom_question = None try: content = generate_txt_content( selected_results, ', '.join(queries), engine=self.engine_combo.currentText(), custom_question=custom_question, language=self.language_manager.current_language ) clipboard = QApplication.clipboard() clipboard.setText(content) logging.info("选中内容已复制到剪贴板。") QMessageBox.information(self, self.language_manager.tr('copy_success'), self.language_manager.tr('copy_success')) except Exception as e: QMessageBox.warning(self, self.language_manager.tr('copy_failure').format(e), self.language_manager.tr('copy_failure').format(e)) logging.error(f"复制出错:{e}") else: QMessageBox.warning(self, self.language_manager.tr('copy_failure_no_selection'), self.language_manager.tr('copy_failure_no_selection')) logging.warning("尝试复制但无选择内容。") def copy_results_silently(self): selected_results = self.get_selected_results() if selected_results: is_advanced = self.advanced_mode_checkbox.isChecked() if is_advanced: queries = [line.strip() for line in self.search_input_advanced.toPlainText().strip().splitlines() if line.strip()] custom_question = self.question_input.toPlainText().strip() else: query = self.search_input.text().strip() queries = [query] custom_question = None try: content = generate_txt_content( selected_results, ', '.join(queries), engine=self.engine_combo.currentText(), custom_question=custom_question, language=self.language_manager.current_language ) clipboard = QApplication.clipboard() clipboard.setText(content) logging.info("选中内容已自动复制到剪贴板。") except Exception as e: logging.error(f"自动复制出错:{e}") else: logging.warning("自动复制时无选择内容。") def on_result_cell_clicked(self, row, column): if column == 1: # URL列 item = self.result_table.item(row, column) if item: url = item.text() logging.info(f"点击URL:{url}") QDesktopServices.openUrl(QUrl(url)) else: logging.warning("点击的URL单元格为空。") def on_checkbox_state_changed(self, item): if item.column() == 0: self.update_saved_content() all_checked = True for row in range(self.result_table.rowCount()): checkbox_item = self.result_table.item(row, 0) if checkbox_item is None or checkbox_item.checkState() != Qt.Checked: all_checked = False break self.checkbox_header.isOn = all_checked self.checkbox_header.updateSection(0) def on_header_checkbox_clicked(self, checked): for row in range(self.result_table.rowCount()): checkbox_item = self.result_table.item(row, 0) if checkbox_item is not None: checkbox_item.setCheckState(Qt.Checked if checked else Qt.Unchecked) def get_selected_results(self): selected_results = [] for row in range(self.result_table.rowCount()): checkbox_item = self.result_table.item(row, 0) if checkbox_item is not None and checkbox_item.checkState() == Qt.Checked: title_item = self.result_table.item(row, 2) link_item = self.result_table.item(row, 1) snippet_item = self.result_table.item(row, 3) content_item = self.result_table.item(row, 4) if title_item and link_item and snippet_item and content_item: result = { 'title': title_item.text(), 'link': link_item.text(), 'snippet': snippet_item.text(), 'content': content_item.text(), 'engine': self.engine_combo.currentText() } selected_results.append(result) else: logging.warning(f"行 {row} 存在空数据项。") return selected_results def update_saved_content(self): selected_results = self.get_selected_results() is_advanced = self.advanced_mode_checkbox.isChecked() if is_advanced: queries = [line.strip() for line in self.search_input_advanced.toPlainText().strip().splitlines() if line.strip()] custom_question = self.question_input.toPlainText().strip() else: query = self.search_input.text().strip() queries = [query] custom_question = None try: content = generate_txt_content( selected_results, ', '.join(queries), engine=self.engine_combo.currentText(), custom_question=custom_question, language=self.language_manager.current_language ) self.current_content = content except Exception as e: logging.error(f"更新内容时出错:{e}") def closeEvent(self, event): if self.thread and self.thread.isRunning(): self.worker.stop() self.thread.quit() event.accept()
if name == "main":
app = QApplication(sys.argv)
window = SearchApp()
window.show()
sys.exit(app.exec_())
import logging
import urllib.parse
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
from bs4 import BeautifulSoup
from utils import get_page_content
import charset_normalizer
def get_google_search_results(query, num_results=5, worker=None):
"""
获取Google搜索结果,并爬取每个结果页面的内容。
"""
query_encoded = urllib.parse.quote_plus(query)
url = f"https://www.google.com/search?q={query_encoded}&num={num_results}"
textheaders = { "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/113.0.0.0 Safari/537.36" ), "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } logging.info(f"发送请求到Google URL: {url}") try: response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() # 获取Content-Type并检查是否为HTML content_type = response.headers.get('Content-Type', '') if 'text/html' not in content_type: logging.error(f"搜索结果页面非HTML内容: {url},Content-Type: {content_type}") raise Exception("搜索结果页面非HTML内容") # 使用charset-normalizer检测编码 detected = charset_normalizer.from_bytes(response.content).best() encoding = detected.encoding if detected and detected.encoding else 'utf-8' # 使用检测到的编码解码内容 text = response.content.decode(encoding, errors='replace') logging.info(f"检测到编码: {encoding},Google搜索结果页面URL: {url}") except requests.RequestException as e: logging.error(f"请求Google失败:{e}") raise Exception(f"请求Google失败:{e}") except Exception as e: logging.error(f"解码Google搜索结果页面失败:{e}") raise Exception(f"解码Google搜索结果页面失败:{e}") soup = BeautifulSoup(text, 'html.parser') results = [] # 根据Google当前的HTML结构进行解析 for g in soup.find_all('div', class_='tF2Cxc'): # 提取标题 title_tag = g.find('h3') title = title_tag.get_text(separator=' ', strip=True) if title_tag else "No title" # 提取URL link_tag = g.find('a') link = link_tag['href'] if link_tag and 'href' in link_tag.attrs else "No link" # 提取摘要内容 snippet = "" possible_snippet_classes = ['VwiC3b', 'IsZvec', 'aCOpRe'] for cls in possible_snippet_classes: snippet_tag = g.find('div', class_=cls) if snippet_tag: snippet = snippet_tag.get_text(separator=' ', strip=True) break if not snippet: snippet_tag = g.find('span', class_='aCOpRe') if snippet_tag: snippet = snippet_tag.get_text(separator=' ', strip=True) if not snippet: snippet = "No content" if snippet == "No content": logging.debug(f"未能提取到Google摘要内容,尝试其他方法。") results.append({ 'title': title, 'link': link, 'snippet': snippet, 'content': "正在获取内容...", 'engine': 'Google' # 添加搜索引擎标识 }) if len(results) >= num_results: break logging.info(f"解析出 {len(results)} 个Google搜索结果。") # 使用线程池并行抓取每个链接的内容 with ThreadPoolExecutor(max_workers=5) as executor: future_to_result = {} for result in results: if worker and not worker.is_running: logging.info("抓取内容任务被中断。") break future = executor.submit(get_page_content, result['link'], worker) future_to_result[future] = result for future in as_completed(future_to_result): if worker and not worker.is_running: logging.info("抓取内容任务被中断。") break result = future_to_result[future] try: content = future.result() result['content'] = content except Exception as e: logging.error(f"抓取内容时出错 ({result['link']}): {e}") result['content'] = "无法获取内容" return results
def get_bing_search_results(query, num_results=5, worker=None):
"""
获取Bing搜索结果,并爬取每个结果页面的内容。
"""
query_encoded = urllib.parse.quote_plus(query)
url = f"https://www.bing.com/search?q={query_encoded}&count={num_results}"
textheaders = { "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/113.0.0.0 Safari/537.36" ), "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } logging.info(f"发送请求到Bing URL: {url}") try: response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() # 获取Content-Type并检查是否为HTML content_type = response.headers.get('Content-Type', '') if 'text/html' not in content_type: logging.error(f"搜索结果页面非HTML内容: {url},Content-Type: {content_type}") raise Exception("搜索结果页面非HTML内容") # 使用charset-normalizer检测编码 detected = charset_normalizer.from_bytes(response.content).best() encoding = detected.encoding if detected and detected.encoding else 'utf-8' # 使用检测到的编码解码内容 text = response.content.decode(encoding, errors='replace') logging.info(f"检测到编码: {encoding},Bing搜索结果页面URL: {url}") except requests.RequestException as e: logging.error(f"请求Bing失败:{e}") raise Exception(f"请求Bing失败:{e}") except Exception as e: logging.error(f"解码Bing搜索结果页面失败:{e}") raise Exception(f"解码Bing搜索结果页面失败:{e}") soup = BeautifulSoup(text, 'html.parser') results = [] # 根据Bing当前的HTML结构进行解析 for li in soup.find_all('li', class_='b_algo'): # 提取标题和链接 h2 = li.find('h2') if h2 and h2.find('a'): a_tag = h2.find('a') title = a_tag.get_text(separator=' ', strip=True) link = a_tag['href'] else: title = "No title" link = "No link" # 提取摘要 snippet_tag = li.find('p') snippet = snippet_tag.get_text(separator=' ', strip=True) if snippet_tag else "No content" results.append({ 'title': title, 'link': link, 'snippet': snippet, 'content': "正在获取内容...", 'engine': 'Bing' # 添加搜索引擎标识 }) if len(results) >= num_results: break logging.info(f"解析出 {len(results)} 个Bing搜索结果。") # 使用线程池并行抓取每个链接的内容 with ThreadPoolExecutor(max_workers=5) as executor: future_to_result = {} for result in results: if worker and not worker.is_running: logging.info("抓取内容任务被中断。") break future = executor.submit(get_page_content, result['link'], worker) future_to_result[future] = result for future in as_completed(future_to_result): if worker and not worker.is_running: logging.info("抓取内容任务被中断。") break result = future_to_result[future] try: content = future.result() result['content'] = content except Exception as e: logging.error(f"抓取内容时出错 ({result['link']}): {e}") result['content'] = "无法获取内容" return results
def get_baidu_search_results(query, num_results=5, worker=None):
"""
获取百度搜索结果,并爬取每个结果页面的内容。
"""
query_encoded = urllib.parse.quote_plus(query)
url = f"https://www.baidu.com/s?wd={query_encoded}&rn={num_results}&ie=utf-8"
textheaders = { "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/113.0.0.0 Safari/537.36" ), "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } logging.info(f"发送请求到百度 URL: {url}") try: response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() # 获取Content-Type并检查是否为HTML content_type = response.headers.get('Content-Type', '') if 'text/html' not in content_type: logging.error(f"搜索结果页面非HTML内容: {url},Content-Type: {content_type}") raise Exception("搜索结果页面非HTML内容") # 使用charset-normalizer检测编码 detected = charset_normalizer.from_bytes(response.content).best() encoding = detected.encoding if detected and detected.encoding else 'utf-8' # 使用检测到的编码解码内容 text = response.content.decode(encoding, errors='replace') logging.info(f"检测到编码: {encoding},百度搜索结果页面URL: {url}") except requests.RequestException as e: logging.error(f"请求百度失败:{e}") raise Exception(f"请求百度失败:{e}") except Exception as e: logging.error(f"解码百度搜索结果页面失败:{e}") raise Exception(f"解码百度搜索结果页面失败:{e}") soup = BeautifulSoup(text, 'html.parser') results = [] # 根据百度当前的HTML结构进行解析 for div in soup.find_all('div', class_='result'): h3 = div.find('h3') if h3 and h3.find('a'): a_tag = h3.find('a') title = a_tag.get_text(separator=' ', strip=True) link = a_tag['href'] else: title = "No title" link = "No link" # 提取摘要 snippet_tag = div.find('div', class_='c-abstract') if not snippet_tag: snippet_tag = div.find('div', class_='c-span18 c-span-last') snippet = snippet_tag.get_text(separator=' ', strip=True) if snippet_tag else "No content" results.append({ 'title': title, 'link': link, 'snippet': snippet, 'content': "正在获取内容...", 'engine': '百度' # 添加搜索引擎标识 }) if len(results) >= num_results: break logging.info(f"解析出 {len(results)} 个百度搜索结果。") # 使用线程池并行抓取每个链接的内容 with ThreadPoolExecutor(max_workers=5) as executor: future_to_result = {} for result in results: if worker and not worker.is_running: logging.info("抓取内容任务被中断。") break future = executor.submit(get_page_content, result['link'], worker) future_to_result[future] = result for future in as_completed(future_to_result): if worker and not worker.is_running: logging.info("抓取内容任务被中断。") break result = future_to_result[future] try: content = future.result() result['content'] = content except Exception as e: logging.error(f"抓取内容时出错 ({result['link']}): {e}") result['content'] = "无法获取内容" return results
translations = {
'en': {
'window_title': "OnlineGPT 7.1",
'search_settings': "Search Settings",
'advanced_mode': "Advanced Mode",
'search_engine': "Search Engine:",
'search_number': "Number of Results:",
'decrement': "-",
'increment': "+",
'copy': "Copy",
'clear': "Clear",
'search_keywords': "Search Keywords:",
'search_placeholder': "e.g., What's the weather like tomorrow",
'search_placeholder_advanced': "Enter one keyword per line, e.g.,\nWhat's the weather like tomorrow\nShanghai travel guide",
'custom_question': "Please enter your question:",
'question_placeholder': "Enter your question here",
'search': "Search",
'interrupt': "Interrupt",
'save_results': "Save Results",
'open_results': "Open Results",
'status_waiting': "Waiting for input...",
'status_searching': "Searching, please wait...",
'status_search_complete': "Search complete, results saved and copied.",
'status_search_failed': "Search failed.",
'input_error': "Input Error",
'input_error_empty_keyword': "Please enter at least one search keyword!",
'input_error_empty_question': "Please enter a custom question!",
'save_error_no_keyword': "No search keywords, cannot save results.",
'save_success': "Search results have been saved to {}",
'save_failure': "Error saving file: {}",
'open_error_no_file': "No result file to open.",
'copy_success': "Selected content has been copied to the clipboard.",
'copy_failure': "Failed to copy content: {}",
'copy_failure_no_selection': "No content selected.",
'interrupt_info_no_task': "There is no ongoing search task to interrupt.",
'interrupt_info_task_interrupted': "Search has been interrupted.",
'help': "Help",
'about': "About",
'about_title': "About OnlineGPT 7.1",
'about_message': (
"<h2>About OnlineGPT 7.1</h2>"
"<p>OnlineGPT 7.1 is a powerful tool for searching and managing online information.</p>"
"<h3>Usage Instructions:</h3>"
"<ol>"
"<li><strong>Enter Search Keywords:</strong> Input your search terms in the designated field. For advanced searches, enable Advanced Mode.</li>"
"<li><strong>Select Search Engine:</strong> Choose your preferred search engine from the dropdown menu (Google, Bing, 百度).</li>"
"<li><strong>Set Number of Results:</strong> Adjust the number of search results you wish to retrieve.</li>"
"<li><strong>Start Search:</strong> Click the 'Search' button to begin the search process.</li>"
"<li><strong>View Results:</strong> Once the search is complete, results will be displayed in the table below.</li>"
"<li><strong>Manage Results:</strong> You can copy, save, or open the search results as needed.</li>"
"<li><strong>Interrupt Search:</strong> If needed, you can interrupt an ongoing search by clicking the 'Interrupt' button or pressing Ctrl+C.</li>"
"</ol>"
"<h3>Features:</h3>"
"<ul>"
"<li><strong>Advanced Mode:</strong> Provides additional options for more refined searches, including custom questions.</li>"
"<li><strong>Multiple Search Engines:</strong> Supports Google, Bing, and 百度 for diverse search needs.</li>"
"<li><strong>Result Management:</strong> Easily copy, save, or open search results directly from the application.</li>"
"<li><strong>Logging:</strong> View detailed logs of your search activities within the application.</li>"
"</ul>"
"<h3>License:</h3>"
"<p>This project is licensed under the <a href='https://opensource.org/licenses/MIT'>MIT License</a>. "
"You are free to use, modify, and distribute this software in accordance with the terms of the license.</p>"
"<h3>Additional Resources:</h3>"
"<p><strong>Open Source:</strong> "
"<a href='https://github.com/yeahhe365/OnlineGPT'>https://github.com/yeahhe365/OnlineGPT</a></p>"
"<p><strong>LINUXDO Forum:</strong> "
"<a href='https://linux.do/t/topic/211975'>https://linux.do/t/topic/211975</a></p>"
),
},
'zh': {
'window_title': "OnlineGPT 7.1",
'search_settings': "搜索设置",
'advanced_mode': "进阶模式",
'search_engine': "搜索引擎:",
'search_number': "搜索数量:",
'decrement': "-",
'increment': "+",
'copy': "复制",
'clear': "清空",
'search_keywords': "搜索关键词:",
'search_placeholder': "例如:明天天气怎么样",
'search_placeholder_advanced': "每行一个关键词,如:\n明天天气怎么样\n上海旅游攻略",
'custom_question': "请输入问题:",
'question_placeholder': "在此输入您的问题",
'search': "搜索",
'interrupt': "中断",
'save_results': "保存结果",
'open_results': "打开结果",
'status_waiting': "等待输入...",
'status_searching': "正在搜索,请稍候...",
'status_search_complete': "搜索完成,结果已保存并已自动复制。",
'status_search_failed': "搜索失败。",
'input_error': "输入错误",
'input_error_empty_keyword': "请输入至少一个搜索关键词!",
'input_error_empty_question': "请输入自定义问题!",
'save_error_no_keyword': "没有搜索关键词,无法保存结果。",
'save_success': "搜索结果已保存到 {}",
'save_failure': "保存文件时出错:{}",
'open_error_no_file': "没有可打开的结果文件。",
'copy_success': "已复制选中的内容到剪贴板。",
'copy_failure': "复制内容时出错:{}",
'copy_failure_no_selection': "未选择任何内容。",
'interrupt_info_no_task': "当前没有正在运行的搜索任务。",
'interrupt_info_task_interrupted': "搜索已被中断。",
'help': "帮助",
'about': "关于",
'about_title': "关于 OnlineGPT 7.1",
'about_message': (
"<h2>关于 OnlineGPT 7.1</h2>"
"<p>OnlineGPT 7.1 是一个强大的在线信息搜索和管理工具。</p>"
"<h3>使用说明:</h3>"
"<ol>"
"<li><strong>输入搜索关键词:</strong> 在指定的输入框中输入您的搜索词。对于高级搜索,请启用进阶模式。</li>"
"<li><strong>选择搜索引擎:</strong> 从下拉菜单中选择您偏好的搜索引擎(Google、Bing、百度)。</li>"
"<li><strong>设置搜索数量:</strong> 调整您希望获取的搜索结果数量。</li>"
"<li><strong>开始搜索:</strong> 点击“搜索”按钮开始搜索过程。</li>"
"<li><strong>查看结果:</strong> 搜索完成后,结果将显示在下方的表格中。</li>"
"<li><strong>管理结果:</strong> 您可以根据需要复制、保存或打开搜索结果。</li>"
"<li><strong>中断搜索:</strong> 如有需要,您可以通过点击“中断”按钮或按下Ctrl+C来中断正在进行的搜索。</li>"
"</ol>"
"<h3>功能特点:</h3>"
"<ul>"
"<li><strong>进阶模式:</strong> 提供额外的选项以进行更精细的搜索,包括自定义问题。</li>"
"<li><strong>多搜索引擎支持:</strong> 支持Google、Bing和百度,满足多样化的搜索需求。</li>"
"<li><strong>结果管理:</strong> 轻松复制、保存或直接从应用程序中打开搜索结果。</li>"
"<li><strong>日志记录:</strong> 在应用程序内查看详细的搜索活动日志。</li>"
"</ul>"
"<h3>License:</h3>"
"<p>本项目采用 <a href='https://opensource.org/licenses/MIT'>MIT 许可证</a> 进行许可。您可以根据许可证的条款自由使用、修改和分发该软件。</p>"
"<h3>额外资源:</h3>"
"<p><strong>开源地址:</strong> "
"<a href='https://github.com/yeahhe365/OnlineGPT'>https://github.com/yeahhe365/OnlineGPT</a></p>"
"<p><strong>LINUXDO 论坛:</strong> "
"<a href='https://linux.do/t/topic/211975'>https://linux.do/t/topic/211975</a></p>"
),
}
}
import re
import logging
import os
from datetime import datetime
import requests
from bs4 import BeautifulSoup
import charset_normalizer
def clean_text(text):
"""
清洗文本,移除控制字符和非打印字符。
"""
# 移除控制字符
return re.sub(r'[\x00-\x1F\x7F]', '', text)
def get_page_content(url, worker=None):
"""
获取指定URL页面的所有文本内容,处理编码并过滤非HTML内容,同时尽量保留原网页的文本格式。
"""
if worker and not worker.is_running:
logging.info(f"中断获取页面内容:{url}")
return "任务已中断,无法获取内容"
textheaders = { "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/113.0.0.0 Safari/537.36" ), "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } try: response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() # 获取Content-Type并检查是否为HTML content_type = response.headers.get('Content-Type', '') if 'text/html' not in content_type: logging.warning(f"非HTML内容,跳过: {url},Content-Type: {content_type}") return "非HTML内容,无法提取" # 使用charset-normalizer检测编码 detected = charset_normalizer.from_bytes(response.content).best() encoding = detected.encoding if detected and detected.encoding else 'utf-8' # 使用检测到的编码解码内容 text = response.content.decode(encoding, errors='replace') logging.info(f"检测到编码: {encoding},URL: {url}") except requests.RequestException as e: logging.error(f"获取页面内容失败 ({url}): {e}") return "无法获取内容" except Exception as e: logging.error(f"解码页面内容失败 ({url}): {e}") return "无法提取内容" soup = BeautifulSoup(text, 'html.parser') # 尝试提取主要内容,首先寻找<article>标签 article = soup.find('article') if article: # 使用换行符分隔段落,保留基本格式 extracted_text = '\n\n'.join([ p.get_text(separator='\n', strip=True) for p in article.find_all(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li']) ]) else: # 如果没有<article>标签,则提取所有<p>和其他块级标签的内容 paragraphs = soup.find_all(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li']) extracted_text = '\n\n'.join([ p.get_text(separator='\n', strip=True) for p in paragraphs ]) # 清洗文本,移除控制字符等 extracted_text = clean_text(extracted_text) # 如果提取的文本过短,可能需要进一步处理 if len(extracted_text) < 200: logging.debug(f"提取内容过短 ({len(extracted_text)} 字符), 使用备用方法。") extracted_text = '\n\n'.join([ p.get_text(separator='\n', strip=True) for p in soup.find_all(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li']) ]) extracted_text = clean_text(extracted_text) return extracted_text if extracted_text else "无法提取内容"
def generate_txt_content(all_results, query, engine='Google', custom_question=None, language='zh'):
"""
生成要保存或复制的文本内容,仅包含选中的结果。
根据语言生成不同的指令内容。
"""
try:
current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if language == 'en':
content = (
"Ignore all previous instructions. You are a knowledgeable and helpful person that can answer any questions. Your task is to answer the following question delimited by triple backticks. Please answer in English.\n\n"
"Question:\n"
"\n" ) if custom_question: content += f"{custom_question}\n" else: content += f"{query}\n" content += "
\n\n"
content += "It's possible that the question, or just a portion of it, requires relevant information from the internet to give a satisfactory answer. The relevant search results provided below, delimited by triple quotes, are the necessary information already obtained from the internet. The search results set the context for addressing the question, so you don't need to access the internet to answer the question.\n\n"
content += "Write a comprehensive answer to the question in the best way you can. If necessary, use the provided search results.\n\n"
content += f"For your reference, today's date is {current_datetime}.\n\n"
content += "---\n\n"
content += "If you use any of the search results in your answer, always cite the sources at the end of the corresponding line, similar to how Wikipedia.org cites information. Use the citation format [NUMBER], where both the NUMBER and URL correspond to the provided search results below, delimited by triple quotes.\n\n"
content += "Present the answer in a clear format.\n"
content += "Use a numbered list if it clarifies things\n"
content += "---\n\n"
content += "If you can't find enough information in the search results and you're not sure about the answer, try your best to give a helpful response by using all the information you have from the search results.\n\n"
else:
content = (
"忽略之前的所有指示。你是一个知识渊博且乐于助人的人,可以回答任何问题。你的任务是回答以下被三个反引号分隔的问题。请用中文回答。\n\n"
"问题:\n"
"\n" ) if custom_question: content += f"{custom_question}\n" else: content += f"{query}\n" content += "
\n\n"
content += "问题可能需要互联网相关的信息来给出满意的答案。下面提供的被三个引号分隔的相关搜索结果是已经从互联网获取的必要信息。这些搜索结果为回答问题提供了上下文,因此你不需要访问互联网来回答问题。\n\n"
content += "请用你能做到的最佳方式写出对问题的全面回答。如果有必要,使用提供的搜索结果。\n\n"
content += f"供参考,今天的日期是 {current_datetime}。\n\n"
content += "---\n\n"
content += "如果你在回答中使用了任何搜索结果,请始终在相应行的末尾引用来源,类似于Wikipedia.org引用信息的方式。使用引用格式[编号],其中编号和URL对应于下面被三个引号分隔的提供的搜索结果。\n\n"
content += "以清晰的格式呈现答案。\n"
content += "如果有助于澄清,请使用编号列表。\n"
content += "---\n\n"
content += "如果你在搜索结果中找不到足够的信息,并且不确定答案,请尽力利用所有来自搜索结果的信息提供有帮助的回答。\n\n"
textcontent += "Search results:\n" content += '"""\n' idx = 1 for result in all_results: content += f"NUMBER:{idx}\n" # 移除搜索引擎信息 # content += f"Engine: {result['engine']}\n" content += f"URL: {result['link']}\n" content += f"TITLE: {result['title']}\n" content += f"SNIPPET: {result['snippet']}\n" content += f"CONTENT: {result['content']}\n\n" idx += 1 content += '"""\n' return content except Exception as e: logging.error(f"生成内容时出错:{e}") raise Exception(f"生成内容时出错:{e}")
def save_results_to_txt(all_results, query, filename=None, engine='Google', custom_question=None, language='zh'):
"""
将搜索结果保存到文本文件中,按照指定的格式。
默认保存到系统的“下载”文件夹。
"""
if not filename:
downloads_path = os.path.join(os.path.expanduser('~'), 'Downloads')
filename = os.path.join(downloads_path, "search_results.txt")
textlogging.info(f"尝试将搜索结果保存到文件: {filename}") try: content = generate_txt_content(all_results, query, engine, custom_question, language) with open(filename, 'w', encoding='utf-8') as f: f.write(content) logging.info(f"搜索结果成功保存到 {filename}") return filename # 返回保存的文件路径 except Exception as e: logging.error(f"保存文件时出错:{e}") raise e
import logging
from PyQt5.QtCore import QObject, pyqtSignal
from search_engines import (
get_google_search_results,
get_bing_search_results,
get_baidu_search_results
)
from utils import save_results_to_txt
class Worker(QObject):
"""
工作线程,用于执行搜索任务。
"""
finished = pyqtSignal(list, str) # 发送结果和文件路径
error = pyqtSignal(str)
textdef __init__(self, queries, num_results=5, engine='Google', custom_question=None): super().__init__() self.queries = queries # 接受多个关键词 self.num_results = num_results self.engine = engine # 搜索引擎 self.custom_question = custom_question # 自定义问题 self._is_running = True # 添加运行状态标志 @property def is_running(self): return self._is_running def stop(self): """ 停止工作线程。 """ self._is_running = False def run(self): """ 执行搜索任务。 """ try: all_results = [] logging.info( f"工作线程开始执行搜索任务,关键词: {self.queries}, " f"结果数量: {self.num_results}, 搜索引擎: {self.engine}" ) for query in self.queries: if not self.is_running: logging.info("搜索任务被中断。") break if self.engine == 'Google': results = get_google_search_results( query, self.num_results, self ) elif self.engine == 'Bing': results = get_bing_search_results( query, self.num_results, self ) elif self.engine == '百度': results = get_baidu_search_results( query, self.num_results, self ) else: raise Exception("不支持的搜索引擎。") # 添加查询词到结果中 for result in results: result['query'] = query all_results.append(results) if not self.is_running: logging.info("搜索任务已被用户中断,停止后续操作。") return # 展平结果列表 flat_results = [item for sublist in all_results for item in sublist] filename = save_results_to_txt( flat_results, ', '.join(self.queries), engine=self.engine, custom_question=self.custom_question ) self.finished.emit(flat_results, filename) logging.info("工作线程搜索任务完成。") except Exception as e: self.error.emit(str(e)) logging.error(f"工作线程搜索任务失败:{e}")
OnlineGPT_7.1 是一个基于 PyQt5 的桌面应用程序,旨在通过集成多个搜索引擎(如 Google、Bing 和 百度)进行信息检索,并提供搜索结果的管理和处理功能。用户可以输入搜索关键词,选择搜索引擎,设定结果数量,执行搜索任务,并对结果进行复制、保存或打开等操作。此外,应用程序支持中英文界面切换,具备日志记录功能,以便用户监控应用行为和调试。
textOnlineGPT_7.1 ├── gui_components.py ├── language_manager.py ├── LICENSE ├── main.exe ├── main.py ├── resources │ ├── icon.ico │ └── icon.png ├── search_app.log ├── search_app.py ├── search_engines.py ├── translations.py ├── utils.py └── worker.py
gui_components.py
GuiLogHandler
,自定义的输入框 MyLineEdit
和文本编辑框 MyTextEdit
,以及带复选框的表头 CheckBoxHeader
和复选框委托 CenteredCheckBoxDelegate
。language_manager.py
translations.py
提供的翻译字典,实现界面文本的动态更新。main.py
SearchApp
窗口。search_app.py
search_engines.py
ThreadPoolExecutor
实现并发抓取每个搜索结果页面的内容。translations.py
utils.py
generate_txt_content
函数生成结构化的文本内容,可能用于进一步处理或与语言模型交互。worker.py
Worker
类,负责在独立线程中执行搜索任务,防止阻塞主线程。LICENSE
main.exe
resources
icon.ico
和 icon.png
)。search_app.log
用户界面
搜索功能
search_engines.py
中的函数与不同搜索引擎交互,获取搜索结果。日志记录
logging
模块,结合自定义的 GuiLogHandler
,将日志信息实时显示在 GUI 中。search_app.log
记录了应用程序的详细运行信息,便于开发者和用户进行问题排查。线程管理
模块化设计
用户体验
功能强大
错误处理和日志
资源文件加载问题
search_app.log
中多次记录到图标文件 icon.png
在临时目录中不存在的错误。这可能是由于应用程序打包后资源文件路径未正确配置导致的。--add-data
选项)引用资源。sys._MEIPASS
(针对 PyInstaller)动态获取资源路径,确保在运行时能够正确加载资源文件。多重主入口
main.py
和 search_app.py
均包含 if __name__ == "__main__":
主入口判断,可能导致冲突或重复启动应用。main.py
),并确保其他模块不包含主入口判断,避免混淆和潜在错误。搜索引擎爬虫合规性
异常信息泄露
翻译更新不完全
update_ui_texts
函数中可能未覆盖所有界面元素的文本更新,导致部分文本在语言切换后未正确显示。.qm
文件或第三方库,提升翻译管理的效率和可靠性。用户体验优化
测试覆盖
OnlineGPT_7.1 是一个功能全面、界面友好的搜索管理工具,集成了多种搜索引擎,提供了丰富的结果管理功能。其模块化设计和多语言支持提升了应用的可维护性和用户覆盖面。然而,项目在资源管理、爬虫合规性、异常处理和用户体验等方面仍有改进空间。通过优化这些方面,项目可以进一步提升其稳定性、合法性和用户满意度。