剛接觸Qt不久的練手項目堵泽,Github鏈接在這里,release目錄下面是軟件發(fā)布姿染,如果無法打開軟件可以先安裝vc_redist(VS運行環(huán)境)膀息。
已實現(xiàn)的feature包括:
1.指定tag從https://konachan.com下載所有匹配的圖片措拇;
2.自定義爬取線程池最大線程數(shù)量<30(根據實驗我纪,K站在同一IP并發(fā)請求數(shù)量高于30時會ban IP,本來打算爬西刺代理的高匿IP來解決這個問題來著,但是基于幾個方面的考慮最終就沒做浅悉,一是免費IP的速度實在是慢趟据、不穩(wěn)定,二是30個線程在不打算作為一個pure spider爬人家全站的情況下已經滿足需求了仇冯,三是除了多調一個QNetworkProxy的api外也沒什么新的技術含量和練習價值又浪費時間)之宿;
3.網絡請求重發(fā)和本地圖片去重,網絡請求失敗重發(fā)重發(fā)最多三次苛坚,要下載的圖片先檢測本地是否存在(重復)比被,存在則不再下載,在更新本地圖片的時候好用省時泼舱。
界面如下圖所示:
下面記錄和總結一下開發(fā)和學習過程中的思路等缀、問題和收獲。
〇娇昙、關于IDE(VS2015)和框架(Qt5.6.2)本身的一些問題
1. Qt designer打不開
原因是Qt5WebEngineWidgets.dll加載出問題尺迂,更名該動態(tài)庫可解決。
2. 缺少ui_xxx.h頭文件
VS2015集成Qt5有時會出現(xiàn)無法生成ui頭文件(ui_xxx.h)的情況冒掌,需要對.ui文件單獨編譯噪裕。
3. 生成moc_xxx.cpp文件失敗
VS2015集成Qt5有時會出現(xiàn)moc文件(moc_xxx.cpp)在解決方案中存在但實際上并沒有成功生成的情況,需要把解決方案中的moc文件移除股毫,然后重新編譯
4. 如何切換當前項目的Qt版本
首先安裝新的Qt版本膳音,并且環(huán)境變量的QTDIR值也要設置為要切換的Qt版本路徑,然后在Qt5插件的Qt Options選項中添加目標Qt版本并設為default铃诬,最后在當前解決方案上右鍵選擇Change Solution's Qt Version即可祭陷,當然源碼的兼容性就得靠自己來修改了。
一趣席、需求分析與架構設計
從根據輸入tag拼接出匹配結果頁面(根頁面)的url到下載到所有匹配圖片要經過四個步驟:
- 請求根頁面兵志,并通過正則表達式分析根頁面得出分頁數(shù)量,獲取所有分頁頁面的url宣肚;
- 請求分頁頁面想罕,對每個分頁頁面分析得出該頁所有圖片的概覽頁面url;
- 請求概覽頁面霉涨,對每張圖片的概覽頁面分析得出下載頁面url弧呐;
- 請求下載頁面,并保存到本地(下載圖片)嵌纲。
頻繁發(fā)起的https請求毫無疑問要用線程池來處理,而對于不同步驟而言腥沽,請求過程是相同的逮走,結果處理因步驟(url)而異,為了學習練手和有可能的圖站擴展(并沒有)今阳,實現(xiàn)的時候有意多態(tài)思想师溅,把請求過程放在基類中茅信,結果處理的部分抽象為純虛函數(shù),在四個派生類中各自實現(xiàn)墓臭,不過這樣做還要根據不同url創(chuàng)建不同的類蘸鲸,可以矯情地套用一下工廠模式(并沒有)。
對于線程間通信窿锉,即每次的進度更新(界面顯示)和任務添加(產生的新url)酌摇,第一個發(fā)布的版本用了任務隊列,但是要考慮線程安全(加鎖)嗡载,還要用非成員變量窑多,重構后的版本全部采用了事件循環(huán)的方式,win32下采用xxxEvent和WaitForxxxObject的一系列api洼滚,而Qt的則是信號槽埂息,信號槽(signals & slots)是Qt的核心機制之一,基于其元對象系統(tǒng)(The Meta-Object System)遥巴,具體內容留待后續(xù)再敘千康。
二、詳細設計與相關問題
1. 線程池管理線程
用于給線程池添加新任務和通知主線程進度更新铲掐,采用任務隊列的方式需要重寫QThread類的run函數(shù)來實現(xiàn)拾弃,采用信號槽的方式需要繼承QObject類然后movetothread()。
在Qt中創(chuàng)建線程有兩種方式:
一是繼承QThread類并重載run函數(shù)迹炼,開啟線程后會在新線程中執(zhí)行run函數(shù)砸彬,但是需要注意的是它的槽函數(shù)被觸發(fā)時會執(zhí)行在主線程;
二是繼承QObject類斯入,在其中定義信號和槽砂碉,然后調用movetothread移入QThread對象,這樣它的槽函數(shù)被觸發(fā)時會執(zhí)行在新線程中刻两。
關于Qt的信號槽也有幾點注意事項:
一是在類中使用signals和slots需要繼承QObject類并加上Q_OBJECT宏增蹭;
二是信號槽中對象不能是不完整類型;
三是信號參數(shù)要不少于槽參數(shù)磅摹;
四是信號和槽同時也是普通函數(shù)滋迈,只是信號函數(shù)不能由用戶實現(xiàn),調用時都一樣户誓。
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_;
};
- 使用QThreadPool來創(chuàng)建線程池饼灿,QThreadPool開啟線程需要傳入繼承QRunnable類的對象,創(chuàng)建一個繼承自QRunnable的基類帝美,在重寫的run函數(shù)中實現(xiàn)https請求過程碍彭,純虛函數(shù)
virtual void handle_page() = 0;
留待派生類實現(xiàn)。
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_;
};
- https請求的實現(xiàn)可以用Qt提供的QNetworkAccessManager。
但是Qt的網絡庫不太好用庇忌,有幾個問題需要注意:
一是每個QNetworkAccessManager對象可以發(fā)起多個連接舞箍,但是并發(fā)連接數(shù)量有上限且無法配置,官方文檔說明對于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.)疏橄,然而這顯然無法滿足需求,對于這個項目就不能墨守成規(guī)了略就,需要每個線程單獨創(chuàng)建一個manager捎迫;
二是QNetworkAccessManager對象在發(fā)起連接時是異步的,也就是會開一個子線程進行請求残制,可以通過信號槽的方式來異步處理請求結果立砸,但需要同步處理時可以用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()方法時會清空緩沖區(qū)折汞,需要多次使用請求結果時注意保存倔幼;
四是關于SSL的支持需要把對應版本(32/64位)的openssl動態(tài)庫(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_));
}
- 對于頁面分析损同,Qt對正則表達式的支持有QRegExp和QRegularExpression可供選擇,本項目采用前者鸟款,另外值得一提的是QRegExp支持同一對象存放多個pattern膏燃,匹配時可以通過cap()的參數(shù)來選擇采用哪個pattern的匹配結果。然后正則表達式這個東西用的時候經常需要查資料何什,不太好記组哩,貼一下我覺得還行的正則表達式參考文檔正則表達式30分鐘入門教程和正則表達式測試網站Regexper。
- 最后提一下Qt的消息處理自定義方法处渣,可以用來寫日志伶贰,很方便,其中QMessageLogContext是消息發(fā)出時的附加信息如文件名罐栈、行號黍衙、函數(shù)名等等,當然除了qDebug()和qInfo()之外還有對qWarning()荠诬、qCritcal()和qFatal()消息的處理琅翻,main函數(shù)中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的確是一個十分優(yōu)秀的框架,不但功能完善易用凌外,還擁有豐富的幫助文檔利于學習辩尊、進步和參考益缠。本來想在這篇梳理一下這段時間接觸Qt的收獲和理解傅是,但是內容較多,也還不夠深入刊驴,決定后續(xù)再另起博文記錄學習關于Qt的核心元對象系統(tǒng)疮薇、Qt Widgets界面設計以及尚未接觸但久聞大名的QML胸墙。