[Python] 纯文本查看 复制代码import sys
import os
import time
from urllib.parse import quote
import requests
import re
import json
from bs4 import BeautifulSoup
from requests import Session
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QLineEdit, QPushButton,
QListWidget, QTextEdit, QStackedWidget, QFrame,
QScrollArea, QScrollBar, QProgressBar, QMessageBox,
QDialog, QTextBrowser, QFileDialog, QSplitter, QInputDialog, QColorDialog, QFontDialog)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QPropertyAnimation, QEasingCurve, QRect, QPoint
from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QCursor, QMouseEvent
# 爬虫部分(保持不变)
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
}
session = Session()
def session_ask():
try:
url = 'http://m.xqishuta.net'
session.get(url=url, headers=headers, timeout=30)
except Exception as e:
print(f"Session初始化失败: {e}")
def novel(name):
novels_list = []
try:
url = 'http://m.xqishuta.net/search.html'
data = {"searchkey": name, "type": "articlename"}
response = session.post(url=url, headers=headers, data=data, timeout=30)
htmls = BeautifulSoup(response.text, 'html.parser').find('div', class_='searchresult').find_all('p')
for i, html in enumerate(htmls, 1):
novel_name = html.a.text
href = 'http://m.xqishuta.net' + html.a['href']
author = html.span.text
novel_list = [i, novel_name, author, href]
novels_list.append(novel_list)
except Exception as e:
print(f"搜索小说失败: {e}")
return novels_list
def single_novel(url):
chapters_list = []
try:
response = session.get(url=url, headers=headers)
response.encoding = 'utf-8'
options = BeautifulSoup(response.text, 'html.parser').find('select').find_all('option')
i = 1
for option in options:
value = 'http://m.xqishuta.net/' + option.get('value')
response = session.get(url=value, headers=headers)
response.encoding = 'utf-8'
chapters = BeautifulSoup(response.text, 'html.parser').find_all('div', class_='info_menu1')
if len(chapters) > 1:
chapters = chapters[1].find('div', class_='list_xm').find_all('li')
for chapter in chapters:
chapter_name = chapter.a.text
url = 'http://m.xqishuta.net' + chapter.a['href']
chapter_list = [i, chapter_name, url]
chapters_list.append(chapter_list)
i += 1
except Exception as e:
print(f"获取章节列表失败: {e}")
return chapters_list
def text(url):
all_text = ""
try:
while True:
response = session.get(url=url, headers=headers)
response.encoding = 'utf-8'
all_text += clear_text(response)
soup = BeautifulSoup(response.text, 'html.parser').find('p', class_='p1 p3')
if soup and soup.text == '下一页' and soup.a:
url = 'http://m.xqishuta.net' + soup.a['href']
else:
break
except Exception as e:
print(f"获取小说内容失败: {e}")
all_text = f"获取内容失败: {e}"
return all_text
def clear_text(response):
try:
soup = BeautifulSoup(response.text, 'html.parser').find('div', class_='novelcontent').find('p')
text = str(soup.text)
text = re.sub(r'最新网址:\S*\s*', '', text)
text = re.sub(r'第[^章]+章\s*[^(]*\s*\(第\d+/\d+页\)\s*', '', text)
text = re.sub(r'(本章未完,请点击下一页继续阅读)', '', text)
return text
except Exception as e:
return f"内容解析失败: {e}"
# 多线程处理网络请求
class SearchThread(QThread):
finished = pyqtSignal(list)
error = pyqtSignal(str)
def __init__(self, novel_name):
super().__init__()
self.novel_name = novel_name
def run(self):
try:
result = novel(self.novel_name)
self.finished.emit(result)
except Exception as e:
self.error.emit(str(e))
class ChapterThread(QThread):
finished = pyqtSignal(list)
error = pyqtSignal(str)
def __init__(self, url):
super().__init__()
self.url = url
def run(self):
try:
result = single_novel(self.url)
self.finished.emit(result)
except Exception as e:
self.error.emit(str(e))
class ContentThread(QThread):
finished = pyqtSignal(str)
error = pyqtSignal(str)
def __init__(self, url):
super().__init__()
self.url = url
def run(self):
try:
result = text(self.url)
self.finished.emit(result)
except Exception as e:
self.error.emit(str(e))
# 自定义多功能按钮类
class MultiFunctionButton(QPushButton):
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.saved_file_path = "" # 保存的文件路径
self.config_file = "saved_file_path.txt" # 配置文件路径
# 尝试读取之前保存的文件路径
self.load_saved_path()
def load_saved_path(self):
"""加载之前保存的文件路径"""
try:
if os.path.exists(self.config_file):
with open(self.config_file, 'r', encoding='utf-8') as file:
self.saved_file_path = file.read().strip()
except Exception as e:
print(f"加载保存的文件路径失败: {e}")
def mousePressEvent(self, event):
if event.button() == Qt.RightButton:
# 右键:浏览文件并保存路径
self.browse_and_save_file()
else:
# 左键:打开保存的文件
super().mousePressEvent(event)
self.open_saved_file()
def browse_and_save_file(self):
"""右键功能:浏览文件并保存路径到txt"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择文件", "", "所有文件 (*.*)"
)
if file_path:
try:
# 保存文件路径到txt文件
with open(self.config_file, 'w', encoding='utf-8') as file:
file.write(file_path)
self.saved_file_path = file_path
QMessageBox.information(self, "成功", f"文件路径已保存: {file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存文件路径失败: {str(e)}")
def open_saved_file(self):
"""左键功能:打开保存的文件"""
if not self.saved_file_path or not os.path.exists(self.saved_file_path):
QMessageBox.warning(self, "提示", "请先右键点击按钮选择文件")
return
try:
# 读取文件内容
with open(self.saved_file_path, 'r', encoding='utf-8') as file:
content = file.read()
# 获取父组件(ReadingWidget)并显示文件内容
parent_widget = self.parent()
while parent_widget and not isinstance(parent_widget, ReadingWidget):
parent_widget = parent_widget.parent()
if parent_widget and hasattr(parent_widget, 'content_text'):
parent_widget.content_text.setPlainText(content)
parent_widget.novel_title_label.setText(f"本地文件: {os.path.basename(self.saved_file_path)}")
QMessageBox.information(self, "成功", f"已打开文件: {os.path.basename(self.saved_file_path)}")
except Exception as e:
QMessageBox.critical(self, "错误", f"打开文件失败: {str(e)}")
# 阅读界面 - 增强极简模式功能
class ReadingWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
self.chapter_list_visible = True
self.buttons_visible = True
self.minimal_mode = False # 极简模式标志
self.drag_position = None
self.resize_border = 8 # 调整大小的边界宽度
self.resize_direction = None
self.custom_bg_color = None
self.original_window_flags = None
self.original_geometry = None
self.current_font = QFont("微软雅黑", 12) # 默认字体
self.initUI()
def initUI(self):
main_layout = QHBoxLayout()
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# 使用QSplitter来管理左右布局
self.splitter = QSplitter(Qt.Horizontal)
# 左侧章节列表(可隐藏)
self.left_frame = QFrame()
self.left_frame.setFrameStyle(QFrame.StyledPanel)
left_layout = QVBoxLayout(self.left_frame)
left_layout.setContentsMargins(10, 10, 10, 10)
chapter_label = QLabel("章节列表")
chapter_label.setFont(QFont("微软雅黑", 12, QFont.Bold))
chapter_label.setStyleSheet("color: #2c3e50; margin-bottom: 10px;")
left_layout.addWidget(chapter_label)
self.chapter_list = QListWidget()
self.chapter_list.setFont(QFont("微软雅黑", 10))
self.chapter_list.setStyleSheet("""
QListWidget {
border: 1px solid #ced4da;
border-radius: 4px;
background-color: white;
}
QListWidget::item {
padding: 8px;
border-bottom: 1px solid #e9ecef;
}
QListWidget::item:selected {
background-color: #3498db;
color: white;
}
QListWidget::item:hover {
background-color: #e9ecef;
}
""")
self.chapter_list.itemClicked.connect(self.on_chapter_clicked)
left_layout.addWidget(self.chapter_list)
# 隐藏/显示章节列表按钮
self.toggle_chapter_btn = QPushButton("隐藏列表")
self.toggle_chapter_btn.setFont(QFont("微软雅黑", 10))
self.toggle_chapter_btn.setStyleSheet("""
QPushButton {
background-color: #6c757d;
color: white;
padding: 6px 12px;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #5a6268;
}
""")
self.toggle_chapter_btn.clicked.connect(self.toggle_chapter_list)
left_layout.addWidget(self.toggle_chapter_btn)
self.splitter.addWidget(self.left_frame)
# 右侧内容区域
self.right_frame = QFrame()
self.right_frame.setFrameStyle(QFrame.StyledPanel)
right_layout = QVBoxLayout(self.right_frame)
right_layout.setContentsMargins(15, 15, 15, 15)
# 顶部导航栏
nav_layout = QHBoxLayout()
self.back_button = QPushButton("返回详情")
self.back_button.setFont(QFont("微软雅黑", 10))
self.back_button.setStyleSheet("""
QPushButton {
background-color: #95a5a6;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #7f8c8d;
}
""")
self.back_button.clicked.connect(self.go_back_to_detail)
nav_layout.addWidget(self.back_button)
self.novel_title_label = QLabel()
self.novel_title_label.setFont(QFont("微软雅黑", 14, QFont.Bold))
self.novel_title_label.setStyleSheet("color: #2c3e50;")
nav_layout.addWidget(self.novel_title_label)
nav_layout.addStretch()
# 字体调节按钮
self.font_button = QPushButton("字体调节")
self.font_button.setFont(QFont("微软雅黑", 10))
self.font_button.setStyleSheet("""
QPushButton {
background-color: #17a2b8;
color: white;
padding: 6px 12px;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #138496;
}
""")
self.font_button.clicked.connect(self.adjust_font)
nav_layout.addWidget(self.font_button)
# 增强的显示/隐藏按钮(控制按钮组和章节列表)
self.toggle_components_btn = QPushButton("隐藏组件")
self.toggle_components_btn.setFont(QFont("微软雅黑", 10))
self.toggle_components_btn.setStyleSheet("""
QPushButton {
background-color: #6c757d;
color: white;
padding: 6px 12px;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #5a6268;
}
""")
self.toggle_components_btn.clicked.connect(self.toggle_components)
nav_layout.addWidget(self.toggle_components_btn)
# 背景色拾取按钮
self.bg_color_btn = QPushButton("背景色")
self.bg_color_btn.setFont(QFont("微软雅黑", 10))
self.bg_color_btn.setStyleSheet("""
QPushButton {
background-color: #9b59b6;
color: white;
padding: 6px 12px;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #8e44ad;
}
""")
self.bg_color_btn.clicked.connect(self.pick_background_color)
nav_layout.addWidget(self.bg_color_btn)
# 极简模式按钮
self.minimal_mode_btn = QPushButton("极简模式")
self.minimal_mode_btn.setFont(QFont("微软雅黑", 10))
self.minimal_mode_btn.setStyleSheet("""
QPushButton {
background-color: #e74c3c;
color: white;
padding: 6px 12px;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #c0392b;
}
""")
self.minimal_mode_btn.clicked.connect(self.toggle_minimal_mode)
nav_layout.addWidget(self.minimal_mode_btn)
right_layout.addLayout(nav_layout)
# 按钮组
self.button_frame = QFrame()
button_layout = QHBoxLayout(self.button_frame)
button_layout.setContentsMargins(0, 10, 0, 10)
self.prev_btn = QPushButton("上一章")
self.prev_btn.setFont(QFont("微软雅黑", 10))
self.prev_btn.setStyleSheet("""
QPushButton {
background-color: #17a2b8;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #138496;
}
QPushButton:disabled {
background-color: #6c757d;
}
""")
self.prev_btn.clicked.connect(self.prev_chapter)
button_layout.addWidget(self.prev_btn)
self.next_btn = QPushButton("下一章")
self.next_btn.setFont(QFont("微软雅黑", 10))
self.next_btn.setStyleSheet("""
QPushButton {
background-color: #28a745;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #218838;
}
QPushButton:disabled {
background-color: #6c757d;
}
""")
self.next_btn.clicked.connect(self.next_chapter)
button_layout.addWidget(self.next_btn)
# 多功能按钮
self.multi_function_btn = MultiFunctionButton("文件管理")
self.multi_function_btn.setFont(QFont("微软雅黑", 10))
self.multi_function_btn.setStyleSheet("""
QPushButton {
background-color: #ffc107;
color: #212529;
padding: 8px 16px;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #e0a800;
}
""")
button_layout.addWidget(self.multi_function_btn)
right_layout.addWidget(self.button_frame)
# 内容显示区域
self.content_text = QTextEdit()
self.content_text.setFont(self.current_font)
self.content_text.setStyleSheet("""
QTextEdit {
border: 1px solid #ced4da;
border-radius: 4px;
padding: 15px;
background-color: white;
line-height: 1.6;
}
""")
self.content_text.setReadOnly(True)
right_layout.addWidget(self.content_text)
self.splitter.addWidget(self.right_frame)
# 设置分割比例
self.splitter.setStretchFactor(0, 1)
self.splitter.setStretchFactor(1, 3)
# 保存初始大小
self.splitter.setSizes([200, 600])
main_layout.addWidget(self.splitter)
self.setLayout(main_layout)
# 设置鼠标跟踪
self.setMouseTracking(True)
self.content_text.setMouseTracking(True)
def adjust_font(self):
"""字体调节功能"""
font, ok = QFontDialog.getFont(self.current_font, self)
if ok:
self.current_font = font
self.content_text.setFont(font)
# 极简模式也会使用这个字体设置
def set_content(self, novel_info, chapter_info, chapters):
self.novel_info = novel_info
self.chapter_info = chapter_info
self.chapters = chapters
self.current_index = chapter_info[0] - 1 # 转换为0-based索引
# 更新界面
self.novel_title_label.setText(f"{novel_info[1]} - {chapter_info[1]}")
# 加载章节列表
self.chapter_list.clear()
for chapter in chapters:
self.chapter_list.addItem(f"{chapter[0]}. {chapter[1]}")
# 高亮当前章节
if 0 0:
self.current_index -= 1
self.chapter_info = self.chapters[self.current_index]
self.set_content(self.novel_info, self.chapter_info, self.chapters)
def next_chapter(self):
if self.current_index 0)
self.next_btn.setEnabled(self.current_index = width - border and y = height - border:
return "bottom-left"
elif x >= width - border and y >= height - border:
return "bottom-right"
elif x = width - border:
return "right"
elif y = height - border:
return "bottom"
else:
return None
def update_cursor_shape(self, direction):
"""根据调整方向更新鼠标光标形状[8](@ref)"""
if direction == "top-left" or direction == "bottom-right":
self.parent.setCursor(Qt.SizeFDiagCursor)
elif direction == "top-right" or direction == "bottom-left":
self.parent.setCursor(Qt.SizeBDiagCursor)
elif direction == "left" or direction == "right":
self.parent.setCursor(Qt.SizeHorCursor)
elif direction == "top" or direction == "bottom":
self.parent.setCursor(Qt.SizeVerCursor)
else:
self.parent.setCursor(Qt.ArrowCursor)
def mousePressEvent(self, event):
"""鼠标按下事件 - 支持窗口拖动和调整大小[8](@ref)"""
if self.minimal_mode:
if event.button() == Qt.LeftButton:
# 检查是否在调整区域
self.resize_direction = self.get_resize_direction(event.pos())
if self.resize_direction:
# 开始调整大小
self.resize_start_pos = event.globalPos()
self.resize_start_geometry = self.parent.geometry()
event.accept()
return
else:
# 记录拖动起始位置
self.drag_position = event.globalPos() - self.parent.frameGeometry().topLeft()
event.accept()
elif event.button() == Qt.RightButton:
# 右键退出极简模式
self.exit_minimal_mode()
event.accept()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
"""鼠标移动事件 - 处理窗口拖动和调整大小[8](@ref)"""
if self.minimal_mode:
if event.buttons() == Qt.LeftButton and self.resize_direction:
# 调整窗口大小
delta = event.globalPos() - self.resize_start_pos
new_geometry = self.resize_start_geometry
if "left" in self.resize_direction:
new_geometry.setLeft(new_geometry.left() + delta.x())
if "right" in self.resize_direction:
new_geometry.setRight(new_geometry.right() + delta.x())
if "top" in self.resize_direction:
new_geometry.setTop(new_geometry.top() + delta.y())
if "bottom" in self.resize_direction:
new_geometry.setBottom(new_geometry.bottom() + delta.y())
# 确保窗口有最小尺寸
if new_geometry.width() = 0 and hasattr(self.parent, 'search_results'):
novel_info = self.parent.search_results[index]
self.parent.show_novel_detail(novel_info)
# 小说详情界面
class NovelDetailWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
self.initUI()
def initUI(self):
layout = QVBoxLayout()
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(15)
# 返回按钮
back_button = QPushButton("返回搜索")
back_button.setFont(QFont("微软雅黑", 10))
back_button.setStyleSheet("""
QPushButton {
background-color: #95a5a6;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #7f8c8d;
}
""")
back_button.clicked.connect(self.parent.show_search)
layout.addWidget(back_button)
# 小说信息
self.novel_info_label = QLabel()
self.novel_info_label.setFont(QFont("微软雅黑", 14, QFont.Bold))
self.novel_info_label.setStyleSheet("color: #2c3e50; margin: 10px 0;")
self.novel_info_label.setWordWrap(True)
layout.addWidget(self.novel_info_label)
# 章节列表标签
chapter_label = QLabel("章节列表")
chapter_label.setFont(QFont("微软雅黑", 16, QFont.Bold))
chapter_label.setStyleSheet("color: #2c3e50; margin: 10px 0;")
layout.addWidget(chapter_label)
# 章节列表
self.chapter_list = QListWidget()
self.chapter_list.setFont(QFont("微软雅黑", 10))
self.chapter_list.setStyleSheet("""
QListWidget {
border: 1px solid #bdc3c7;
border-radius: 5px;
background-color: white;
padding: 5px;
}
QListWidget::item {
padding: 8px;
border-bottom: 1px solid #ecf0f1;
}
QListWidget::item:selected {
background-color: #3498db;
color: white;
border-radius: 3px;
}
QListWidget::item:hover {
background-color: #ecf0f1;
border-radius: 3px;
}
""")
self.chapter_list.itemDoubleClicked.connect(self.on_chapter_selected)
layout.addWidget(self.chapter_list)
self.setLayout(layout)
def set_novel_info(self, novel_info):
self.novel_info = novel_info
self.novel_info_label.setText(f"书名:《{novel_info[1]}》\n作者:{novel_info[2]}")
# 加载章节列表
self.parent.progress_bar.setVisible(True)
self.chapter_thread = ChapterThread(novel_info[3])
self.chapter_thread.finished.connect(self.on_chapters_loaded)
self.chapter_thread.error.connect(self.on_chapters_error)
self.chapter_thread.start()
def on_chapters_loaded(self, chapters):
self.parent.progress_bar.setVisible(False)
self.chapter_list.clear()
if not chapters:
QMessageBox.warning(self, "错误", "加载章节失败")
return
self.chapters = chapters
for chapter in chapters:
self.chapter_list.addItem(f"{chapter[0]}. {chapter[1]}")
def on_chapters_error(self, error_msg):
self.parent.progress_bar.setVisible(False)
QMessageBox.critical(self, "错误", f"加载章节失败: {error_msg}")
def on_chapter_selected(self, item):
index = self.chapter_list.currentRow()
if index >= 0 and hasattr(self, 'chapters'):
chapter_info = self.chapters[index]
self.parent.show_reading(self.novel_info, chapter_info, self.chapters)
# 主窗口
class NovelReader(QMainWindow):
def __init__(self):
super().__init__()
self.search_results = []
self.initUI()
session_ask() # 初始化会话
def initUI(self):
self.setWindowTitle("小说在线阅读器")
self.setGeometry(100, 100, 1200, 800)
# 设置应用样式
self.setStyleSheet("""
QMainWindow {
background-color: #f8f9fa;
}
""")
# 创建中央部件和布局
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
layout.setContentsMargins(0, 0, 0, 0)
# 全局进度条
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
self.progress_bar.setStyleSheet("""
QProgressBar {
border: none;
background-color: #e9ecef;
height: 3px;
}
QProgressBar::chunk {
background-color: #3498db;
}
""")
layout.addWidget(self.progress_bar)
# 创建堆叠窗口
self.stacked_widget = QStackedWidget()
layout.addWidget(self.stacked_widget)
# 创建三个界面
self.search_widget = SearchWidget(self)
self.detail_widget = NovelDetailWidget(self)
self.reading_widget = ReadingWidget(self)
# 添加到堆叠窗口
self.stacked_widget.addWidget(self.search_widget)
self.stacked_widget.addWidget(self.detail_widget)
self.stacked_widget.addWidget(self.reading_widget)
# 显示搜索界面
self.stacked_widget.setCurrentIndex(0)
def show_search(self):
self.stacked_widget.setCurrentIndex(0)
def show_novel_detail(self, novel_info=None):
if novel_info is not None:
self.detail_widget.set_novel_info(novel_info)
self.stacked_widget.setCurrentIndex(1)
def show_reading(self, novel_info, chapter_info, chapters):
self.reading_widget.set_content(novel_info, chapter_info, chapters)
self.stacked_widget.setCurrentIndex(2)
if __name__ == '__main__':
app = QApplication(sys.argv)
# 设置应用字体
font = QFont("微软雅黑", 10)
app.setFont(font)
reader = NovelReader()
reader.show()
sys.exit(app.exec_())



