【PyQt6】小说下载DrissionPage及阅读PyQt6

文章目录

  • [1 简介](#1 简介)
  • [2 DrissionPage](#2 DrissionPage)
  • [3 阅读界面](#3 阅读界面)
    • [3.1 Qt Designer](#3.1 Qt Designer)
  • [3.2 界面类](#3.2 界面类)
  • 总结

1 简介

看到一本小说 《无敌六皇子》 【https://www.xsobiquge.org/book/178299/】看小说简介觉得挺有意思的,想读一读。浏览器阅读 不能保存进度,就好烦。想着就爬下来,本地看。

看着挺简单的网站,requests 就只能爬个目录页,分章内容总是被拒,UA Refer Cookie 以及所有的请求头都加了,也没有效果,最终还是回到浏览器去,此时 DrissionPage 就用上了。

2 DrissionPage

DrissionPage 号称同时实现"写得快"和"跑得快",试过以后还真是巨方便。

DrissionPage 的教程我就略过不谈了,网上有总结的,感谢哪些前辈们的分享。

我就直接分享一下 具体的爬虫代码吧, 代码量真的很少

demo

python 复制代码
from DrissionPage import SessionPage
import os
import time
import random
import re

# 创建页面对象
page = SessionPage()

subpages = []

reg = r"第(\d+)章.*"
pat = re.compile(reg)


def formatTitle(title):
    match = pat.search(title)
    if match:
        idx = match.group(1)
        res = title.replace(idx, f'{idx:0>4}')
        return res

    return title


def download_catalog(url):
    page.get(start_url)
    # 根据 xpath 或 css selector 查找

    xpath = '//*[@id="list"]/dl/dd'
    lists = page.eles(f'xpath:{xpath}')
    for li in lists:
        a = li.ele('tag:a')
        href = a.attr('href')
        title = a.text
        title = formatTitle(title)
        # print(a.attr('href'), a.text)
        subpages.append({'href': href, 'title': title})
    print(f'{url} 下载完成')


def download_chapter():
    for i in range(min_chapters-1, max_chapters):
        dic = subpages[i]
        time.sleep(random.random() * 2)
        ret = page.get(dic['href'])
        if not ret:
            print(ret)
            return
        # //*[@id="content"]
        xpath = '//*[@id="content"]'
        elem = page.ele(f'xpath:{xpath}')
        context = elem.text

        with open(f"{dic['title']}.txt", 'w', encoding='utf-8') as f:
            lines = context.splitlines()
            for line in lines:
                line = line.strip()
                if line == '网页版章节内容慢,请下载爱阅小说app阅读最新内容':
                    break
                if line:
                    f.write('\t'+line+'\n')
        print(f'{dic['title']} 下载完成')


if __name__ == '__main__':
    book_name = "无敌六皇子"
    start_url = 'https://www.xsobiquge.org/book/178299/'

    min_chapters = 401
    max_chapters = 600

    if not os.path.exists(book_name):
        os.mkdir(book_name)

    download_catalog(start_url)
    os.chdir(book_name)
    download_chapter()

3 阅读界面

小说的分章终于保存到了本地,txt 格式,直接使用 VSC 看,那个阅读体验真的不好。于是就顺手写了一个阅读器,大体上就是用 QTextEdit 来阅读,调整一下字体大小,行高,宽度,上一章 下一章的功能,基本就能满足要求,阅读进度使用 json 保存

3.1 Qt Designer

用 Designer 简单写个界面,东西很少,看图就明白

加载页面使用 self.ui = load_ui.loadUi("Novel.ui", self)

这种方式有利于看界面效果,但没代码提示,写代码不友好

建议界面定型后,使用 pyuic6 把ui文件转成 py文件,再导入

3.2 界面类

界面类,这一部分也没啥东西,直接上代码看吧

python 复制代码
from PyQt6.QtCore import QTimer, QSettings, Qt
from PyQt6.QtGui import QCloseEvent, QKeyEvent, QPalette, QColor
from PyQt6.QtWidgets import (
    QMainWindow,
    QWidget,
    QApplication,
    QFileDialog,
    QSplitter,
    QStyleFactory,
)
from PyQt6.uic import load_ui
import os
import json


class MainWindow(QMainWindow):
    # ================= 构造函数 =============================
    def __init__(self, parent: QWidget = None):
        super().__init__(parent)

        # --------------------------------- 加载界面 -------------------------------------------
        self.ui = load_ui.loadUi("Novel.ui", self)

        # --------------------------------- 设置属性 -------------------------------------------
        self.dir_path = None
        self.filename = None
        self.verBar = 0

        # --------------------------------- 设置 splitter -------------------------------------
        splitter = self.ui.splitter
        splitter.setStretchFactor(0, 1)
        splitter.setStretchFactor(1, 4)
        splitter.setHandleWidth(1)

        # --------------------------------- 设置 菜单项  -------------------------------------
        opd = self.ui.opendir_act
        opd.triggered.connect(self.opendir_act_triggered)

        # -- 设置 QListWidget 文件目录
        listw = self.ui.listWidget
        listw.currentTextChanged.connect(self.currentFileChanged)

        # -----------------设置 QPlainTextEdit 文件阅读区 -------------------------------------
        pte = self.ui.plainTextEdit
        font = pte.font()
        font.setFamily("楷体")
        font.setPointSize(30)
        pte.setFont(font)
        pte.setReadOnly(True)
        # pte.setViewportMargins(0, 10, 0, 10)

        # -- 导入设置数据       -------------------------------------------------------
        if not os.path.exists("data.txt"):
            return
        with open("data.txt", "r", encoding="utf-8") as f:
            dic = json.loads(f.read())
            self.dir_path = dic["path"]
            self.file = dic["file"]

            font.setPointSize(dic["pointSize"])
            pte.setFont(font)
            self.verBar = dic["verBar"]

            self.update_listWidget()

        # -- 链接信号
        # pte.cursorPositionChanged.connect(self.cursorPositionChanged)
        # pte.verticalScrollBar().valueChanged.connect(self.cursorPositionChanged)

    def cursorPositionChanged(self, value):
        pte = self.ui.plainTextEdit
        # pte.verticalScrollBar().blockSignals(True)
        max = pte.verticalScrollBar().maximum()
        if max - value > 50:
            value -= 50
        else:
            value = max
        pte.verticalScrollBar().setValue(value)

        # def f():
        #     pte.verticalScrollBar().blockSignals(False)
        # QTimer.singleShot(100, f)

        pass

    def update_listWidget(self):
        listw = self.ui.listWidget
        listw.blockSignals(True)
        # 保存当前目录
        cur_dir = os.getcwd()
        os.chdir(self.dir_path)

        files = os.listdir(self.dir_path)
        if not files:
            return

        listw.clear()
        listw.addItems(files)

        # 切换回当前目录
        os.chdir(cur_dir)

        def f():
            listw.setCurrentItem(None)
            listw.blockSignals(False)
            idx = files.index(self.file)
            listw.setCurrentRow(idx)

        QTimer.singleShot(100, f)

    def opendir_act_triggered(self):
        self.dir_path = QFileDialog.getExistingDirectory()

        if self.dir_path:
            listw = self.ui.listWidget
            listw.blockSignals(True)
            # 保存当前目录
            cur_dir = os.getcwd()
            os.chdir(self.dir_path)

            files = os.listdir(self.dir_path)
            if not files:
                return
            files = [
                file for file in files if os.path.isfile(file) and file != "data.txt"
            ]
            listw.clear()
            listw.addItems(files)
            # 切换回当前目录
            os.chdir(cur_dir)

            def f():
                listw.setCurrentItem(None)
                listw.blockSignals(False)

            QTimer.singleShot(100, f)

    def currentFileChanged(self, file):

        self.setWindowTitle(file)

        self.filename = file
        pte = self.ui.plainTextEdit

        # 保存当前目录
        cur_dir = os.getcwd()
        os.chdir(self.dir_path)

        with open(file, "r", encoding="utf-8") as f:
            lines = f.readlines()

        os.chdir(cur_dir)
        # text_cursor = QTextCursor()

        pte.clear()

        text_cursor = pte.textCursor()
        width = pte.width()
        TextBlockFormat = text_cursor.blockFormat()
        TextBlockFormat.setLeftMargin(width * 0.1)
        TextBlockFormat.setRightMargin(width * 0.1)
        TextBlockFormat.setBottomMargin(50)
        TextBlockFormat.setLineHeight(150, 1)
        TextBlockFormat.setTextIndent(20 * 4)
        text_cursor.setBlockFormat(TextBlockFormat)

        for line in lines:
            text_cursor.insertText(line.strip() + "\n")

        pte.verticalScrollBar().setValue(self.verBar)

    def closeEvent(self, a0: QCloseEvent) -> None:
        pte = self.ui.plainTextEdit
        if self.filename:
            pte = self.ui.plainTextEdit
            font = pte.font()

            dict = {
                "path": self.dir_path,
                "file": self.filename,
                "pointSize": font.pointSize(),
                "verBar": pte.verticalScrollBar().value(),
            }
            # os.chdir(self.dir_path)
            with open("data.txt", "w", encoding="utf-8") as f:
                f.write(json.dumps(dict, ensure_ascii=False))

        # 保存 self.ui.splitter 的状态
        splitter_saveState(self.ui.splitter)

        return super().closeEvent(a0)

    def keyReleaseEvent(self, a0: QKeyEvent) -> None:
        # print('*'*20)
        pte = self.ui.plainTextEdit
        match a0.key():
            # 上一章
            case Qt.Key.Key_Left:
                nextrow = self.ui.listWidget.currentRow() - 1
                if nextrow > -1:
                    self.ui.listWidget.setCurrentRow(nextrow)
                    # self.verBar = 0
                    pte.verticalScrollBar().setValue(0)
                a0.accept()

            # 下一章
            case Qt.Key.Key_Right:
                nextrow = self.ui.listWidget.currentRow() + 1
                if nextrow < self.ui.listWidget.count():
                    self.ui.listWidget.setCurrentRow(nextrow)
                    self.verBar = 0
                    pte.verticalScrollBar().setValue(0)
                a0.accept()
        return super().keyReleaseEvent(a0)


def splitter_restoreState(splitter: QSplitter):
    set = QSettings("splitterSizes", QSettings.Format.IniFormat)
    splitter.restoreState(set.value("splitterSizes"))


def splitter_saveState(splitter: QSplitter):
    set = QSettings("splitterSizes", QSettings.Format.IniFormat)
    set.setValue("splitterSizes", splitter.saveState())


if __name__ == "__main__":
    qApp = QApplication([])

    qApp.setStyle(QStyleFactory.create("fusion"))

    palette = qApp.palette()
    palette.setColor(QPalette.ColorRole.Base, QColor(31, 31, 31))
    palette.setColor(QPalette.ColorRole.Text, QColor(78, 201, 176))
    palette.setColor(QPalette.ColorRole.Window, QColor(43, 43, 43))

    palette.setColor(QPalette.ColorRole.Highlight, QColor(38, 79, 120))
    palette.setColor(QPalette.ColorRole.HighlightedText, QColor(255, 255, 255))
    palette.setColor(QPalette.ColorRole.ButtonText, QColor(204, 204, 204))

    qApp.setPalette(palette)

    mw = MainWindow()

    mw.showMaximized()

    splitter_restoreState(mw.ui.splitter)

    qApp.exec()

总结

到此这个阅读器也就能马马虎虎的使用了,翻页的时候不太友好,因为 QTextEdit 翻页的时候,默认翻的是viewport 的大小,对于文字来说,有时会卡半行,真想重写 QTextEdit 的空格键啊,有机会再说。

最后再说一句 Python 真的很方便啊,能随手写写小工具

相关推荐
写bug如流水23 分钟前
【Python】Python闭包的妙用与注意事项
开发语言·python·spring
程序小健1 小时前
基于深度学习的学情智能监测系统设计与实现(PYQT+YOLOv8+训练数据集+论文+部署文档)
人工智能·深度学习·yolo·目标检测·pyqt
MavenTalk1 小时前
经典Python应用库一览
开发语言·python·pycharm·requests
42fourtytoo2 小时前
SQL总结
数据库·python·sql·计算机网络·安全·网络安全
MavenTalk2 小时前
Python中流行的开源OCR项目
开发语言·python·ocr
coffee_baby2 小时前
《 Spring Boot实战:优雅构建责任链模式投诉处理业务》
spring boot·python·设计模式·责任链模式
Envyᥫᩣ2 小时前
Python中的机器学习:从入门到实战
python·前端框架
西贝爱学习2 小时前
py 元组,列表,函数的学习和使用
python
weijie.zwj3 小时前
LLM基础概念:Prompt
人工智能·python·langchain
high_tea3 小时前
pytest - 多线程提速
python·pytest