小说摸鱼阅读器

查看 86|回复 9
作者:phantomxjc   
最近爱上了看小说,但是直接在电脑端网页上看小说,每次跳着章节看都要返回目录,重新开始找那个章节,
主打一个就很
烦!烦!烦!烦!
so,我就想着手机上的小说阅读器看着挺不错的,有章节导航,于是,搬过来,哈哈
很多功能都是见名知意,不过个人也加了一个特色小功能,叫做文件管理,
右键文件管理,选择一个txt文件,他会将其路径保存在一个txt当中,左键点击,则会将其内容显示在小说阅读界面,总之懂得都懂,哈哈哈哈
详细代码如下(不喜欢刨析代码,请别让我刨析):
[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)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QPropertyAnimation, QEasingCurve, QRect
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 = ''
        session.get(url=url, headers=headers, timeout=30)
    except Exception as e:
        print(f"Session初始化失败: {e}")
def novel(name):
    novels_list = []
    try:
        url = ''
        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 = '' + 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 = '' + 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 = '' + 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 = '' + 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 mousePressEvent(self, event: QMouseEvent):
        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 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 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.initUI()
    def initUI(self):
        main_layout = QHBoxLayout()
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.setSpacing(0)
        # 使用QSplitter来管理左右布局[6,7,8](@ref)
        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()
        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.go_back_to_detail)
        nav_layout.addWidget(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.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)
        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(QFont("微软雅黑", 12))
        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)
        # 设置分割比例[7,8](@ref)
        self.splitter.setStretchFactor(0, 1)
        self.splitter.setStretchFactor(1, 3)
        # 保存初始大小
        self.splitter.setSizes([200, 600])
        main_layout.addWidget(self.splitter)
        self.setLayout(main_layout)
    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.next_btn.setEnabled(self.current_index  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 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(chapter_info)
# 主窗口(保持不变)
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;
            }
            QWidget {
                background-color: white;
            }
        """)
        # 创建中央部件和布局
        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)
        # 创建堆叠窗口[3,4](@ref)
        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, chapter_info):
        if hasattr(self.detail_widget, 'novel_info') and hasattr(self.detail_widget, 'chapters'):
            self.reading_widget.set_content(
                self.detail_widget.novel_info,
                chapter_info,
                self.detail_widget.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_())
效果图如下:


1.png (53.7 KB, 下载次数: 1)
下载附件
2025-9-30 12:33 上传



2.png (40.51 KB, 下载次数: 1)
下载附件
2025-9-30 12:33 上传



3.png (117.17 KB, 下载次数: 1)
下载附件
2025-9-30 12:33 上传



4.png (56.32 KB, 下载次数: 1)
下载附件
2025-9-30 12:33 上传

百度网盘链接如下:
通过网盘分享的文件:小说阅读器.exe
链接: https://pan.baidu.com/s/1_G9hPWLvU5rzujYOCde6Tg?pwd=jgx3 提取码: jgx3 复制这段内容后打开百度网盘手机App,操作更方便哦
夸克网盘:
我用夸克网盘给你分享了「小说阅读器.exe」,点击链接或复制整段内容,打开「夸克APP」即可获取。
/~2c8738UpNi~:/
链接:https://pan.quark.cn/s/c8bb213c869e

微软, 章节

属于你   

摸鱼不够隐蔽
kaimenzzz   

可惜我们的办公电脑上装了监控软件
逝去的初夏   

看出了便捷下载,没看出摸鱼,生怕发现不了我
qianjieqaq   

不够隐蔽呀,之前有一个单行阅读器非常隐蔽
Vampiremss   

多谢分享,试用一下
vip-happy   

不错的阅读器,支持原创。
期待下一版可以调节字体大小和颜色。
naixubao   

是个好工具,期待后续的摸鱼,感谢楼主分享。
Yanpeen   

没有老板键,不敢摸鱼了
alidadai   

需要单行显示
您需要登录后才可以回帖 登录 | 立即注册

返回顶部