3 Star 8 Fork 1

rainy / JieNovel

Create your Gitee Account
Explore and code with more than 6 million developers,Free private repositories !:)
Sign up
This repository doesn't specify license. Without author's permission, this code is only for learning and cannot be used for other purposes.
Clone or download
Cancel
Notice: Creating folder will generate an empty file .keep, because not support in Git
Loading...
README.md

<<<<<<< HEAD

软件说明

Release版本下载

目录重点文件
    - cookies.txt       // 缓存用户会话信息
    - history.txt       // 小说最后的阅读链接记录
    - JieConfig.json    // 程序部分样式配置,修改此文件可以自定义部分样式
    - JieNovel.exe      // 在线小说软件
    - python
        - JieMain.exe   // 爬虫服务软件

运行说明

1.先启动JieMain.exe运行爬虫服务,默认监听端口为8080,暂不支持修改。
2.接着正常启动JieNovel小说软件,小说软件启动后,回去链接爬虫软件服务进行本地套接字通信。
3.如果启动小说软件时,没有先启动爬虫软件服务,则会启动Python爬虫软件服务。
4.当小说软件退出时,爬虫软件不会退出,会一直运行,你想手动关掉这个爬虫软件,可以通过系统的任务管理器[Alt + Ctrl + Delete]呼出
5.正使用直接启动JieNovel小说软件即可,该软件会检查系统的程序有没有运行JieMain,如果没有则会在指定路径启动JieMain爬虫服务

ps: 关于你可能会问的部分问题解惑

1.为啥不将Python做成Windows服务程序,或者Linux的守护进程,而是普通进程呢?
答:因为JieMain.exe是Python写的程序软件是通过pyinstaller -Fw JieMain.py 打包出来的程序。

2.为啥pyton目录下和JieNovel目录下,都有两份相同的cookies文件?
答:因为在爬虫软件中获取cookies的文件是相对路径的也就是当前启动程序的路径 + cookies路径,通过JieNovel启动的程序走的相对路径则是JieNovel的路径,而你手动启动目录的路径则是在Python处。

3.为啥要分为JieMain和JieNovel两个软件是要做程序解耦吗?
答:作者是基于基础对象编程设计,之所以做成两个软件,是因为Python的爬虫比cpp要好做太多。

代码说明

事先声明,任何一款软件几乎不会涉及到代码说明。至少作者我是没有见过,所以并没有参考示例。如果有哪里觉得不对的,那么以你为标准,作者只是个写代码的,不懂这些。

代码结构

语言
    1.后台爬虫服务 => Python
    2.前台UI => QML
    3.前台服务 => Cpp

程序基础流程
    初始化
        1.Cpp获取数据 ==req==>>> Python ==req==>>> https://www.23qb.net/
        2.https://www.23qb.net/ ==resp==>>> Python ==resp==>>> Cpp ==resp==>>> Qml
    
    运行
        1.Qml ==req==>>> .Cpp获取数据 ==req==>>> Python ==req==>>> https://www.23qb.net/
        2.https://www.23qb.net/ ==resp==>>> Python ==resp==>>> Cpp ==resp==>>> Qml

文字描述:
    - 初始化
        1.程序在初始化阶段,会向爬虫服务器发送一个获取主页书籍的请求给爬虫服务。
        2.爬虫服务开始处理这个请求,去指定的网站发送一个http请求获取网页HTML页面。
        3.爬虫服务按照指定的方式解析HTML页面,返回指定的数据给小说软件。
        4.Cpp数据中心拿到爬虫服务返回的数据后,设置数据然后通知QML页面响应显示数据。
    
    - 运行
        1.用户在与QML进行交互,触发指定的事件处理,调用Cpp数据服务接口获取数据。
        2.Cpp数据服务发送请求给爬虫服务获取数据。
        3.爬虫服务按照指定的方式解析HTML页面,返回指定的数据给小说软件。
        4.Cpp数据中心拿到爬虫服务返回的数据后,设置数据然后通知QML页面响应显示数据。

ps:这里啰嗦一点,由于真正的数据是网站上抓取下来的,所以你能感受到程序有点慢其实是网站的响应确实很慢。
    还有就是许多网站都做了IP访问频度限制。另外一点就是,被爬取的网站数据接口随时可能发生变化,
    用着用着就提供不了服务了,这个属于正常情况。一旦发生这种情况,就需要更新爬虫服务,
    来调整对网站的数据抓取处理。        

源码讲解

  • CMakeLists.txt 配置文件
set(JieDev off)         // 设置是否为开发模式

// 如果是开发模式,那么定义一个 JIE_DEV宏,并且配置一个CMake .h.in文件会生成一个.h文件,用于设置CMake变量Cpp使用
if (${JieDev})
    add_definitions(-D JIE_DEV)
    configure_file(JieProject.h.in ${PROJECT_SOURCE_DIR}/JieProject.h)
endif()

// 如果是开发者模式,就没有WIN32会生成控制台。
if (${JieDev})
    add_executable(JieNovel ${PROJECT_SOURCES})
else()
    add_executable(JieNovel WIN32 ${PROJECT_SOURCES})
endif()
  • main.cpp 起始文件

// 注册Qml类型,但是不允许实例化这些类型,用于提供,编写代码类型的提示。因
qmlRegisterUncreatableType<JieDataReq>("JieModule", 1, 0, "JieDataReq", "[Jie error : qmlRegisterUncreatableType ]");
qmlRegisterUncreatableType<JieDataResp>("JieModule", 1, 0, "JieDataResp", "[Jie error : qmlRegisterUncreatableType ]");
qmlRegisterUncreatableType<JieConfig>("JieModule", 1, 0, "JieConfig", "[Jie error : qmlRegisterUncreatableType ]");

// 这一部分就是,开发模式和发布版本的代码区别,我们用JIE_DEV来判断究竟使用那些代码
#ifndef JIE_DEV
    // 发布版本的代码
    QProcess process;
    
    // 由于Qt是个异步框架,所以我们很多操作需要等到Qml引擎完成之后才能开始的,所以这里通过引擎发送完成信号处理来做后续处理
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, [&process, &jieData](QObject *, const QUrl &){
        
        // 检查指定的爬虫服务有没有在运行。如果没有在运行,则启动该软件
        if (!JieProcess::Exist("JieMain.exe")) {
            // 爬虫服务器运行之后,我们才开始连接到爬虫服务
            QObject::connect(&process, &QProcess::stateChanged, [&jieData](QProcess::ProcessState newState){
                if (newState == QProcess::Running)
                    jieData.setConnect("127.0.0.1", 8080);
            });
            
            // 启动爬虫服务
            process.setProgram("./python/JieMain.exe");
            process.start();
        } else
            // 爬虫服务已经在运行的情况下,我们直接就连接到爬虫服务
            jieData.setConnect("127.0.0.1", 8080);
        
        // 这个是在引擎运行时,就去提取历史文件记录的阅读记录。
         jieData.setHistory("./history.txt");
    });

    // 这个是提取程序配置,这个操作必须在引擎运行之前完成。因为引擎初始化的时候,要获取配置文件的一些设置
    jieData.setConfig("./JieConfig.json");

#else       
    // 开发版本的代码
    // 开发版本,爬虫服务是作者单独运行的,并且一定是在程序运行之前运行的。
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, [&jieData](QObject *, const QUrl &){
        
        jieData.setConnect("127.0.0.1", 8080);
        jieData.setHistory("history.txt");       
    });      
    
    // PROJECT_SOURCE_DIR 这个是CmMake配置的,我这里配置的是项目代码的地址。
    jieData.setConfig(QString("%1/JieConfig.json").arg(PROJECT_SOURCE_DIR));
#endif

// 注册实例到Qml中使用,分别是 请求模块、响应模块、历史模块、配置模块
auto rootContext = engine.rootContext();
rootContext->setContextProperty("jieData", &jieData);
rootContext->setContextProperty("jieReq", jieData.req());
rootContext->setContextProperty("jieResp", jieData.resp());
rootContext->setContextProperty("jieHistory", jieData.history());
rootContext->setContextProperty("jieConfig", jieData.config());

// 引擎加载QML文件进行解析运行
engine.load(url);
  • 零散代码
// 设置连接到爬虫服务器
void JieData::setConnect(QString addr, quint16 port) noexcept {
    
    // tcp套接字连接
    _socket.connectToHost(addr, port);
    
    // 注册一个信号回调,当tcp成功连接时回调
    QObject::connect(&_socket, &QAbstractSocket::connected, [this]{
        // 注册一个套接字消息处理回调
        QObject::connect(&_socket, &QIODevice::readyRead, this, &JieData::disposeMessage);
        // 发送一个请求到爬虫服务,获取主页数据。
        _dataReq.homeBooks();
    });

    // 这里也处理一个连接失败的回调处理
    QObject::connect(&_socket, &QAbstractSocket::errorOccurred, [this](QAbstractSocket::SocketError socketError){
        if (socketError != QAbstractSocket::RemoteHostClosedError)
            qDebug() << socketError;
        _socket.close();
    });

}

// ---------------------------------------------------------------------------------

// 设置历史记录文件
void JieData::setHistory(QString path) {    
    // 解析历史记录文件,并且设置历史对象数据
    JieHistory::JieHistoryFile(_history, path);
    
    // 这里保存文件的路径,因为在程序退出时,还需要把历史数据写入到文件中
    _history.setPath(path);
    
    // 如果历史对象数据书籍不是null,那么就发送一个通知信号,此信号Qml处理,会提取历史书籍的数据显示
    if (!_history.getBooks().isEmpty())
        emit _history.booksChanged();
}


// ---------------------------------------------------------------------------------

// 设置样式配置文件
void JieData::setConfig(QString path) {
    // 检查文件是否存在
    if (QFileInfo::exists(path)) {
        // 解析文件获取样式配置 
        auto config = JieConfig::FromFile(path);
        if (config.isEmpty()) {
            QMessageBox::warning(nullptr, "waring", "config file error");
            QCoreApplication::exit(-1);
        }
        // 设置程序的配置样式json
        _config.setConfig(config);
    }

}


// ---------------------------------------------------------------------------------

// 爬虫服务返回的响应消息
void JieData::disposeMessage() {
    // 读取本次消息全部内容
    auto result = _socket.readAll();
    
    // 追加到缓冲区
    _buffer.append(result);
    
    // 检查依稀,响应的状态,如果是None表示还没有解析请求任务,则先解析
    if (_respStatus == JieRespStatus::Status::None) {
        // 检查包的起个头标志是否存在不存在则不满解析的标准
        auto reqIndex = _buffer.indexOf("Jie");
        if (reqIndex == -1)
            return ;
        // 移除解析的部分,这只响应对象并且设置请求状态为Resp
        _resp = JieResp::FromJieResp(_buffer.left(reqIndex));
        _buffer.remove(0, reqIndex + 3);
        _respStatus = JieRespStatus::Status::Resp;
    }
    
    // 当请求状态为Resp就进行处理
    if (_respStatus == JieRespStatus::Status::Resp) {
        
        // 检查一些响应头是否包含事件,不存在的话,就放弃这请求
        if (!_resp.isKey("event")) {
            respClear();
            return;
        }
        
        // 处理请求,因为有可能是数据没有收到完毕的情况下,这个事件处理返回的false,则表示请求处理还没有完成
        if (_dataResp.jieEvent(_resp, _buffer))
            respClear();

    }
}


// ---------------------------------------------------------------------------------

// 定义的一些处理宏

// 检查数据请求是否完整,不完整就退出函数,返回FALSE
#define CHECK_DATA_LENGTH(resp, buffer) \
    auto dataLength = resp.value("dataLength").toInt(); \
    if (dataLength != buffer.count())   \
        return false;

// 铅笔网站开始处理
#define QianBiStart(resp, buffer) \
    auto urlStr = resp.value("url");        \
    if (urlStr == "https://www.x23qb.com/") {       \
        CHECK_DATA_LENGTH(resp, buffer)

// 处理完毕
#define QianBiEnd    }

// 处理请求 1.解析数据,交换结果,发射信号,表示数据已经到了
#define FROM_LIST(functor, list, sig) \
    auto result = functor(buffer, urlStr);    \
    if (!result.isEmpty()) {           \
          list.swap(result);            \
          emit sig();                  \
    }


// 事件处理 这里用到宏替换的一些方式,来处理重复代码
bool JieDataResp::jieEvent(JieResp& resp, QByteArray& buffer) {
    auto event = resp.value("event");
    
    // 主页事件
    if (event == "home") {
        QianBiStart(resp, buffer)
        FROM_LIST(JieResp::FromBooks, _books, booksChanged)
        QianBiEnd
    
    // 书籍章节列表        
    } else if (event == "title") {
        QianBiStart(resp, buffer)
        FROM_LIST(JieResp::FromTitles, _titles, titlesChanged)
        QianBiEnd
        
    // 书籍内容        
    } else if (event == "content") {
        QianBiStart(resp, buffer)
        FROM_LIST(JieResp::FromJieBookTitleContent, _content, contentChanged)
        QianBiEnd
    
    // 搜索指定书籍
    } else if (event == "search") {
        QianBiStart(resp, buffer)
        FROM_LIST(JieResp::FromBooks, _books, booksChanged)
        QianBiEnd
    }

    return true;
}

#undef FROM_LIST
#undef QianBiEnd
#undef QianBiStart
#undef CHECK_DATA_LENGTH


// ---------------------------------------------------------------------------------



=======
>>>>>>> dev

Comments ( 0 )

Sign in for post a comment

About

混合编程Python爬虫小说开发,QmlGUI开发。 spread retract
C++ and 5 more languages
Cancel

Releases (2)

All

Contributors

All

Activities

load more
can not load any more
1
https://gitee.com/QingChenMaoYu/jie-novel.git
git@gitee.com:QingChenMaoYu/jie-novel.git
QingChenMaoYu
jie-novel
JieNovel
master

Search

102255 3a0e046c 1850385 102255 7aaa926c 1850385