【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 真的很方便啊,能随手写写小工具

相关推荐
(・Д・)ノ5 分钟前
python打卡day27
开发语言·python
小oo呆1 小时前
【学习心得】Jupyter 如何在conda的base环境中其他虚拟环境内核
python·jupyter·conda
小白学大数据2 小时前
Scrapy框架下地图爬虫的进度监控与优化策略
开发语言·爬虫·python·scrapy·数据分析
浊酒南街2 小时前
TensorFlow之微分求导
人工智能·python·tensorflow
立秋67892 小时前
用Python绘制梦幻星空
开发语言·python·pygame
alpszero2 小时前
YOLO11解决方案之对象裁剪探索
人工智能·python·计算机视觉·yolo11
白云千载尽3 小时前
相机、雷达标定工具,以及雷达自动标定的思路
python·自动驾驶·ros
咕噜咕噜啦啦3 小时前
python爬虫实战训练
爬虫·python
盛夏绽放3 小时前
Python字符串常用内置函数详解
服务器·开发语言·python
我想睡觉2613 小时前
Python训练营打卡DAY27
开发语言·python·机器学习