Qt实现Konachan基于tag的图片下载器

刚接触Qt不久的练手项目,Github链接在这里,release目录下面是软件发布,如果无法打开软件可以先安装vc_redist(VS运行环境)。

已实现的feature包括:
1.指定tag从https://konachan.com下载所有匹配的图片;
2.自定义爬取线程池最大线程数量<30(根据实验,K站在同一IP并发请求数量高于30时会ban IP,本来打算爬西刺代理的高匿IP来解决这个问题来着,但是基于几个方面的考虑最终就没做,一是免费IP的速度实在是慢、不稳定,二是30个线程在不打算作为一个pure spider爬人家全站的情况下已经满足需求了,三是除了多调一个QNetworkProxy的api外也没什么新的技术含量和练习价值又浪费时间);
3.网络请求重发和本地图片去重,网络请求失败重发重发最多三次,要下载的图片先检测本地是否存在(重复),存在则不再下载,在更新本地图片的时候好用省时。

界面如下图所示:


KonachanCollecter.png

下面记录和总结一下开发和学习过程中的思路、问题和收获。

〇、关于IDE(VS2015)和框架(Qt5.6.2)本身的一些问题

1. Qt designer打不开

原因是Qt5WebEngineWidgets.dll加载出问题,更名该动态库可解决。

2. 缺少ui_xxx.h头文件

VS2015集成Qt5有时会出现无法生成ui头文件(ui_xxx.h)的情况,需要对.ui文件单独编译。

3. 生成moc_xxx.cpp文件失败

VS2015集成Qt5有时会出现moc文件(moc_xxx.cpp)在解决方案中存在但实际上并没有成功生成的情况,需要把解决方案中的moc文件移除,然后重新编译

4. 如何切换当前项目的Qt版本

首先安装新的Qt版本,并且环境变量的QTDIR值也要设置为要切换的Qt版本路径,然后在Qt5插件的Qt Options选项中添加目标Qt版本并设为default,最后在当前解决方案上右键选择Change Solution's Qt Version即可,当然源码的兼容性就得靠自己来修改了。

一、需求分析与架构设计

从根据输入tag拼接出匹配结果页面(根页面)的url到下载到所有匹配图片要经过四个步骤:

  • 请求根页面,并通过正则表达式分析根页面得出分页数量,获取所有分页页面的url;
  • 请求分页页面,对每个分页页面分析得出该页所有图片的概览页面url;
  • 请求概览页面,对每张图片的概览页面分析得出下载页面url;
  • 请求下载页面,并保存到本地(下载图片)。

频繁发起的https请求毫无疑问要用线程池来处理,而对于不同步骤而言,请求过程是相同的,结果处理因步骤(url)而异,为了学习练手和有可能的图站扩展(并没有),实现的时候有意多态思想,把请求过程放在基类中,结果处理的部分抽象为纯虚函数,在四个派生类中各自实现,不过这样做还要根据不同url创建不同的类,可以矫情地套用一下工厂模式(并没有)。

对于线程间通信,即每次的进度更新(界面显示)和任务添加(产生的新url),第一个发布的版本用了任务队列,但是要考虑线程安全(加锁),还要用非成员变量,重构后的版本全部采用了事件循环的方式,win32下采用xxxEvent和WaitForxxxObject的一系列api,而Qt的则是信号槽,信号槽(signals & slots)是Qt的核心机制之一,基于其元对象系统(The Meta-Object System),具体内容留待后续再叙。

二、详细设计与相关问题

1. 线程池管理线程

用于给线程池添加新任务和通知主线程进度更新,采用任务队列的方式需要重写QThread类的run函数来实现,采用信号槽的方式需要继承QObject类然后movetothread()。

在Qt中创建线程有两种方式:
一是继承QThread类并重载run函数,开启线程后会在新线程中执行run函数,但是需要注意的是它的槽函数被触发时会执行在主线程;
二是继承QObject类,在其中定义信号和槽,然后调用movetothread移入QThread对象,这样它的槽函数被触发时会执行在新线程中。

关于Qt的信号槽也有几点注意事项:
一是在类中使用signals和slots需要继承QObject类并加上Q_OBJECT宏;
二是信号槽中对象不能是不完整类型;
三是信号参数要不少于槽参数;
四是信号和槽同时也是普通函数,只是信号函数不能由用户实现,调用时都一样。

class PoolManager :public QObject
{
    Q_OBJECT
public:
    PoolManager(QString path,int max);
    ~PoolManager();

public slots:
    void add_new_task(QString task);

signals:
    void new_progress(QString progress);

private:
    QString path_;
    QThreadPool thread_pool_;
};
  1. 使用QThreadPool来创建线程池,QThreadPool开启线程需要传入继承QRunnable类的对象,创建一个继承自QRunnable的基类,在重写的run函数中实现https请求过程,纯虚函数virtual void handle_page() = 0;留待派生类实现。
class TaskWorker :public QObject ,public QRunnable
{
    Q_OBJECT

public:
    void run();
    TaskWorker* set_url(QString url);
    TaskWorker* set_path(QString path);
    virtual void handle_page() = 0;

signals:
    void new_task(QString task);

protected:
    QString url_;
    QString path_;
    QByteArray page_;
};
  1. https请求的实现可以用Qt提供的QNetworkAccessManager
    但是Qt的网络库不太好用,有几个问题需要注意:

一是每个QNetworkAccessManager对象可以发起多个连接,但是并发连接数量有上限且无法配置,官方文档说明对于http连接最多支持同时6个请求(QNetworkAccessManager queues the requests it receives. The number of requests executed in parallel is dependent on the protocol. Currently, for the HTTP protocol on desktop platforms, 6 requests are executed in parallel for one host/port combination.),一个项目只需要一个manager(One QNetworkAccessManager should be enough for the whole Qt application.),然而这显然无法满足需求,对于这个项目就不能墨守成规了,需要每个线程单独创建一个manager;

二是QNetworkAccessManager对象在发起连接时是异步的,也就是会开一个子线程进行请求,可以通过信号槽的方式来异步处理请求结果,但需要同步处理时可以用QEventLoop来阻塞,另外QNetworkAccessManager对象在主线程退出前不会主动释放,其子线程也不会主动退出,因此完成后要主动释放掉QNetworkAccessManager对象来关闭子线程,不然会造成隐式内存泄露

三是关于请求返回的QNetworkReply*对象,它需要用户去主动释放(After the request has finished, it is the responsibility of the user to delete the QNetworkReply object at an appropriate time. Do not directly delete it inside the slot connected to finished(). You can use the deleteLater() function.),并且在调用readAll()方法时会清空缓冲区,需要多次使用请求结果时注意保存;

四是关于SSL的支持需要把对应版本(32/64位)的openssl动态库(ssleay32.dll和libeay32.dll)和Qt5Network.dll放在一起,Qt5.6.2是在Qt5.6.2\Tools\QtCreator\bin目录下;

void TaskWorker::run()
{
    QSslConfiguration ssl_config;
    ssl_config.setPeerVerifyMode(QSslSocket::VerifyNone);
    ssl_config.setProtocol(QSsl::TlsV1_2);
    QNetworkRequest request;
    request.setSslConfiguration(ssl_config);
    request.setUrl(QUrl(url_));
    QEventLoop loop;
    QNetworkAccessManager* manager = new QNetworkAccessManager;
    QNetworkReply* reply;
    bool suc = false;
    for (int i = 0;i < 3 && !suc;i++)
    {// resend when error
        reply = manager->get(request);
        connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
        loop.exec();
        if (!reply->error()) {
            suc = true;// quit loop when success
            page_ = reply->readAll();
        }
        else qInfo("Network Error(%d): %s![%s]", reply->error(), qPrintable(reply->errorString()), qPrintable(reply->url().toString()));
        delete reply; reply = nullptr;// mem release
    }
    manager->deleteLater();
    suc ? handle_page() : qInfo("Request failed: %s", qPrintable(url_));
}
  1. 对于页面分析,Qt对正则表达式的支持有QRegExpQRegularExpression可供选择,本项目采用前者,另外值得一提的是QRegExp支持同一对象存放多个pattern,匹配时可以通过cap()的参数来选择采用哪个pattern的匹配结果。然后正则表达式这个东西用的时候经常需要查资料,不太好记,贴一下我觉得还行的正则表达式参考文档正则表达式30分钟入门教程和正则表达式测试网站Regexper
  2. 最后提一下Qt的消息处理自定义方法,可以用来写日志,很方便,其中QMessageLogContext是消息发出时的附加信息如文件名、行号、函数名等等,当然除了qDebug()和qInfo()之外还有对qWarning()、qCritcal()和qFatal()消息的处理,main函数中QApplication对象的定义是Qt Widgets应用所必不可少的,相关内容留待后叙。
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    KonachanCollecter w;
    w.show();
    // change qDebug&qInfo to logger
    qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &context, const QString &msg) {
        QString time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
        QFile file("kc_log.txt");
        file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Append);
#ifdef _DEBUG
        if (type == QtDebugMsg) file.write(QString("%1: %2 (%3:%4, %5)\n").arg(time).arg(msg).arg(context.file).arg(context.line).arg(context.function).toLocal8Bit());
#endif // _DEBUG
        if (type == QtInfoMsg) file.write(QString("%1: %2\n").arg(time).arg(msg).toLocal8Bit());
        file.close();
    });
    return a.exec();
}


抛去网络库不谈,Qt的确是一个十分优秀的框架,不但功能完善易用,还拥有丰富的帮助文档利于学习、进步和参考。本来想在这篇梳理一下这段时间接触Qt的收获和理解,但是内容较多,也还不够深入,决定后续再另起博文记录学习关于Qt的核心元对象系统、Qt Widgets界面设计以及尚未接触但久闻大名的QML。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容

  • 为什么在头文件中有的是使用前置声明,而有的是包含头文件? 如下代码: 前置声明(forward declarati...
    Joe_HUST阅读 1,250评论 0 6
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,263评论 25 707
  • 我很少买白颜色的衣服。 从小到大白颜色的衣服不超过十件,印象中可以记得的只有三件,一件是小时候参加学校相声大赛买的...
    摇摆的费列罗阅读 530评论 0 1
  • 想知道,一个22岁的中学老师,能如何逆袭???
    留一人独白阅读 179评论 0 0
  • 新关注简书还不知道如何玩
    格桑姐姐阅读 109评论 0 0