建立MFC应用程序
具体操作步骤
1.配置MFC的属性
具体配置过程可以看此内容的第一篇
2.导入CHttp类(第一篇的)
将第一篇中的CHttp类添加到此mfc程序中,此类只实现连接url获取html页面的功能。关于爬取百度图片模块的函数这里并未作为CHttp类中的函数,而是作为mfc主窗口类的函数,为了后面显示结果要显示到界面上,需要使用控件变量来显示结果,就需要将此模块变为主窗口类的成员函数。
注意:在mfc中,将CHttp类导入后,还需要引入头文件,头文件的引入要在stdafx.h中的最下方引入,否则可能会报错。
#include "CHttp.h"
#include<algorithm>
#include <string>
using namespace std;
3.搭好界面
具体需要实现的功能如图所示,输入爬取内容,输入爬取页数,输入保存路径,点击爬取就能启动,而且还有暂停,恢复,停止等功能,这就需要创建一个线程来进行爬取工作,在输出结果中还有对是否下载的判断,这里创建了另一个线程判断此下载线程的状态并输出当前状态。还有将所得到的结果输出到界面的功能。
对各个控件添加变量进行绑定,并添加一些后面会用到的成员变量及成员函数在主对话框的头文件中,可以看到如下内容
//需要线程中使用的变量
string text; //转换后的文本
int fistPage; //转换后的首页
int lastPage; //转换后的尾页
string savePath; //转换后的路径
CWinThread *pThread; //定义线程句柄
//用于接收编辑框的变量
CString strText, strFistPage, strLastPage, strSavaPath, strThreads;
// 爬取内容
CEdit m_text;
// 爬取的开始页面
CEdit m_fistPage;
// 爬取的结束页面
CEdit m_lastPage;
// 保存的路径
CEdit m_savaPath;
// 线程数
CEdit m_threads;
// 输出结果
CEdit m_result;
// 用于显示结果
CString strResult;
//匹配图片的网络路径
bool RegexIamageBaiDu(string & html, vector<string>& vecImage);//参数1为要解析的html页面,参数2为解析出来的图片网络路径(多条路径)
//开始下载图片
bool downLoadImage(vector<string>& vecImage, string savepathM, string name, int fistPage);//参数1为要下载的图片网络路径,参数2为保存路径,参数3为要保存的图片名称
//开始抓取图片
void StartCatchBaiDu(string name, string savepathM, int fistPage, int lastPage);//参数1为要搜索的图片名称,参数2为保存路径,参数3为开始页数,参数4为结束页数
afx_msg void OnBnClickedBtnend();
afx_msg void OnBnClickedBtnresume();
afx_msg void OnBnClickedBtnpause();
// 显示下载状态
CStatic m_status;
4.实现保存路径选择并显示路径功能
具体参考https://www.cnblogs.com/coolbear/p/3458104.html
本文在保存路径最右侧的按钮函数中使用的是:
//选择保存路径
void CspiderBaiDuImgDlg::OnBnClickedBtnchoosepath()
{
// TODO: 在此添加控件通知处理程序代码
TCHAR chPath[255]; //用来存储路径的字符串
CString strPath;
BROWSEINFO bInfo;
GetModuleFileName(NULL, chPath, MAX_PATH);
strPath = chPath;
ZeroMemory(&bInfo, sizeof(bInfo));
bInfo.hwndOwner = m_hWnd;
bInfo.lpszTitle = _T("请选择路径: ");
bInfo.ulFlags = BIF_RETURNONLYFSDIRS | BIF_EDITBOX;
//bInfo.lpfn = BrowseCallbackProc;
bInfo.lParam = (LPARAM)strPath.GetBuffer(strPath.GetLength());
LPITEMIDLIST lpDlist; //用来保存返回信息的IDList
lpDlist = SHBrowseForFolder(&bInfo); //显示选择对话框
if (lpDlist != NULL) //用户按了确定按钮
{
SHGetPathFromIDList(lpDlist, chPath);//把项目标识列表转化成字符串
strPath = chPath; //将TCHAR类型的字符串转换为CString类型的字符串
//strPath.Replace(_T("\\"), _T("/"));
m_savaPath.SetWindowTextW(strPath);
}
}
5.将爬取图片模块函数变为MFC主窗口类的函数
将其变为主窗口类的函数,是为了其中的一些显示信息能方便在mfc的编辑框中显示。
在主窗口类的头文件中的定义如下:
//匹配图片的网络路径
bool RegexIamageBaiDu(string & html, vector<string>& vecImage);//参数1为要解析的html页面,参数2为解析出来的图片网络路径(多条路径)
//开始下载图片
bool downLoadImage(vector<string>& vecImage, string savepathM, string name, int fistPage);//参数1为要下载的图片网络路径,参数2为保存路径,参数3为要保存的图片名称
//开始抓取图片
void StartCatchBaiDu(string name, string savepathM, int fistPage, int lastPage);//参数1为要搜索的图片名称,参数2为保存路径,参数3为开始页数,参数4为结束页数
而其函数实现如下:
//匹配图片路径
// 参数1为要解析的html页面,参数2为解析出来的图片网络路径(多条路径), q为翻页页数
bool CspiderBaiDuImgDlg::RegexIamageBaiDu(string & html, vector<string>& vecImage){
smatch mat;
regex rgx("http://[^\\s'\"<>()]+"); //用来匹配所有的http://图片连接,此处可以匹配百度中的所需要的正式图片
string::const_iterator start = html.begin();
string::const_iterator end = html.end();
string per;
int i = 1;
//int num = 10;
while (regex_search(start, end, mat, rgx))
{
string per(mat[0].first, mat[0].second);//匹配出的每一条,构造一下per
//判断里面url是否有图片jpg、png等等
//匹配到了图片连接
if (per.find(".jpg") != string::npos)
{
vecImage.push_back(per);
}
start = mat[0].second;
i++;
}
return true;
}
//根据网络路径下载图片(自定义)
bool CspiderBaiDuImgDlg::downLoadImage(vector<string>& vecImage, string savepathM, string name, int fistPage){
string per; //用来暂时储存图片
//下载图片
int j = 0;
for (int i = 0; i < vecImage.size(); i++){
//将vecImage的url连接由string变为符合下面下载函数的格式
per = vecImage[i];
size_t len = per.length();//获取字符串长度
int nmlen = MultiByteToWideChar(CP_ACP, 0, per.c_str(), len + 1, NULL, 0);//如果函数运行成功,并且cchWideChar为零 //返回值是接收到待转换字符串的缓冲区所需求的宽字符数大小。
wchar_t* buffer = new wchar_t[nmlen];
MultiByteToWideChar(CP_ACP, 0, per.c_str(), len + 1, buffer, nmlen);
//转变savepath的格式
//string savepath = savepathM + vecImage[i].substr(vecImage[i].find_last_of('/') + 1); //此处使用的是其原来的图片名称
//此处使用全局变量numImagesR,防止后面的数字把前面的数字覆盖
//用名字第几页第几个图片来命名
string savepath = savepathM + "/" + name + to_string(fistPage) + "-" + to_string(i+1) + ".jpg"; //此处使用数字对其重命名
size_t len1 = savepath.length();
wchar_t* imgsavepath = new wchar_t[len1];
int nmlen1 = MultiByteToWideChar(CP_ACP, 0, savepath.c_str(), len1 + 1, imgsavepath, len1);
//下载文件stringToLPCWSTR
HRESULT hr = URLDownloadToFile(NULL, buffer, imgsavepath, 0, NULL);//buffer
if (hr == S_OK)
{
//获取下载文件大小
//方法二 C方式(需要将string类型转换成char *类型)
FILE* fp = fopen(savepath.c_str(), "r");
fseek(fp, 0, SEEK_END);
size_t size = ftell(fp);
fclose(fp);
CString sizeImg;
sizeImg.Format(_T("下载完成 [%4dKB] | "), (size/1000));
//string转cstring,并换行
CString perUrl(per.c_str());
perUrl = sizeImg + perUrl + _T("\r\n");
int iLen = m_result.GetWindowTextLength();
m_result.SetSel(iLen, iLen, TRUE);
m_result.ReplaceSel(perUrl, FALSE);
j++;
}
else{
//某一图片下载失败时返回false,在后面还会从头开始下载,这里就先略过吧
//return false;
//若此张图片下载失败就继续下一章的下载
continue;
}
}
CString numImages;
numImages.Format(_T("当前页面共成功下载%d张\r\n"), j);
int iLen = m_result.GetWindowTextLength();
m_result.SetSel(iLen, iLen, TRUE);
m_result.ReplaceSel(numImages, FALSE);
return true;
}
//开始抓取百度图片
void CspiderBaiDuImgDlg::StartCatchBaiDu(string name, string savepathM, int fistPage, int lastPage){
int page = (fistPage - 1) * 20;//fistPage是开始页
string startUrl = "https://image.baidu.com/search/flip?tn=baiduimage&ie=utf-8&word={" + name + "}&pn={" + to_string(page) + "}";
queue<string> q; //声明一个url队列,#include <queue>
q.push(startUrl); //将起始url放入队列
string cururl;
CHttp http;
while (!q.empty()) //队列不为空,就一直执行此循环(一直爬取连接不止一个网站上的,在这网站上其他的也可以)
{
cururl = q.front();
q.pop();
string html;
//判断此链接是否为http链接,不同的连接使用不同的获取页面方式
//如果不是http就执行https的操作
if (string::npos == cururl.find("http://"))
{
if (false == http.GetHtmlHttps(cururl, html)){
int iLen = m_result.GetWindowTextLength();
m_result.SetSel(iLen, iLen, TRUE);
m_result.ReplaceSel(_T("获取html页面失败\r\n"), FALSE);
continue;
}
}
//如果不是https就执行http的操作
if (string::npos == cururl.find("https://"))
{
if (false == http.GetHtmlHttp(cururl, html)){
int iLen = m_result.GetWindowTextLength();
m_result.SetSel(iLen, iLen, TRUE);
m_result.ReplaceSel(_T("获取html页面失败\r\n"), FALSE);
continue;
}
}
//获取https图片
vector<string> vecImage;
if (false == RegexIamageBaiDu(html, vecImage)) //此处匹配图片地址,也将队列q压入一个新的地址(此地址为下一个所需下载的地址)
{
int iLen = m_result.GetWindowTextLength();
m_result.SetSel(iLen, iLen, TRUE);
m_result.ReplaceSel(_T("获取图片网络链接失败\r\n"), FALSE);
continue;
}
else{
CString regextext;
regextext.Format(_T("获取图片链接成功,第:%d页,图片总数量:%d张\r\n"), fistPage, vecImage.size());
int iLen = m_result.GetWindowTextLength();
m_result.SetSel(iLen, iLen, TRUE);
m_result.ReplaceSel(regextext, FALSE);
}
//下载图片
if (false == downLoadImage(vecImage, savepathM, name, fistPage))
{
int iLen = m_result.GetWindowTextLength();
m_result.SetSel(iLen, iLen, TRUE);
m_result.ReplaceSel(_T("下载图片失败\r\n"), FALSE);
continue;
}
else{
CString downloadtext(cururl.c_str());
downloadtext = _T("当前爬取网址为:") + downloadtext + _T("\r\n");
int iLen = m_result.GetWindowTextLength();
m_result.SetSel(iLen, iLen, TRUE);
m_result.ReplaceSel(downloadtext, FALSE);
}
if (fistPage == lastPage)//爬numPage页就结束了,也可以去掉这句话
{
CString page;
page.Format(_T("爬取任务结束,当前为第%d页,欢迎再次使用百度图片爬取工具!\r\n"), fistPage);
int iLen = m_result.GetWindowTextLength();
m_result.SetSel(iLen, iLen, TRUE);
m_result.ReplaceSel(page, FALSE);
break;
}
fistPage++;
page = (fistPage - 1) * 20;
cururl = "https://image.baidu.com/search/flip?tn=baiduimage&ie=utf-8&word={" + name + "}&pn={" + to_string(page) + "}"; //重新给当前url赋值
q.push(cururl); //将当前url放入队列
}
//释放
//如果不是http就执行https的操作
if (string::npos == cururl.find("http://"))
{
SSL_shutdown(http.sslHandle);
SSL_free(http.sslHandle);
SSL_CTX_free(http.sslContext);
}
closesocket(http.g_sock);
WSACleanup();
return;
}
6.开始爬取按钮代码实现
//开始爬取
void CspiderBaiDuImgDlg::OnBnClickedBtndownload()
{
// TODO: 在此添加控件通知处理程序代码
//接收搜索内容并转换成string类型
m_text.GetWindowTextW(strText);
text = CW2A(strText.GetString());
//接收开始页数,结束页数并转换类型
m_fistPage.GetWindowTextW(strFistPage);
m_lastPage.GetWindowTextW(strLastPage);
//将CString转化为int类型
fistPage = _wtoi(strFistPage);
lastPage = _wtoi(strLastPage);
//接收保存路径并将其转换成string类型
m_savaPath.GetWindowTextW(strSavaPath);
strSavaPath.Replace(_T("\\"), _T("/")); //将保存路径的格式转换一下,否则后面的下载程序无法下载
savePath = CW2A(strSavaPath.GetString());
//判断接收的数据是否为空若为空,不能执行此操作
if (strText.IsEmpty()|strFistPage.IsEmpty()|strLastPage.IsEmpty()|strSavaPath.IsEmpty())
{
AfxMessageBox(_T("内容填写不完整,请输入完整!"));
return;
}
if (strSavaPath.GetLength()<3)
{
AfxMessageBox(_T("请输入正确保存目录!"));
return;
}
//在开始的时候将编辑框内容清空
m_result.SetWindowTextW(_T(""));
//判断线程是否结束
if (code == 1) //线程正在运行
{
AfxMessageBox(_T("正在执行下载任务,请结束后再开始!"));
return;
}
//创建一个新的线程来执行抓取操作
pThread = AfxBeginThread(ThreadProc,this); //参数1线程名,参数2this表示此窗口指针
}
7.下载线程函数实现
在主窗口类的cpp文件的上方进行线程函数的定义:如下
int code = 0; //用来接收线程状态值
//定义爬取线程
UINT __cdecl ThreadProc(LPVOID lpParameter){
code = 1;//将先程状态值设为1,表明线程正在进行
CspiderBaiDuImgDlg *pThisGlg = (CspiderBaiDuImgDlg*)lpParameter;
pThisGlg->StartCatchBaiDu(pThisGlg->text, pThisGlg->savePath, pThisGlg->fistPage, pThisGlg->lastPage);
code = 0; //线程结束时再变为0
return 0;
}
此处使用code作为线程状态值,为了后面的暂停、恢复、停止等功能实现时显示当前状态。
8.创建判断线程
在初始化函数中创建判断线程
创建一个判断线程,判断执行线程是否还在执行
m_status.SetWindowTextW(_T("当前无任务进行"));
CWinThread *pJudge = AfxBeginThread(ThreadJudge, this);
判断线程函数如下:
//定义判断爬取线程是否工作的线程(未能成功实现)
UINT __cdecl ThreadJudge(LPVOID lpParameter){
CspiderBaiDuImgDlg *pThisGlg = (CspiderBaiDuImgDlg*)lpParameter;
while(1){
//获取线程pThread的执行状态,如果
//若为真表明线程正在执行
if (code == 1){
pThisGlg->m_status.SetWindowTextW(_T("正在下载,请等候. . ."));
}
else if (code == 2){
pThisGlg->m_status.SetWindowTextW(_T("下载已暂停"));
}
else{
pThisGlg->m_status.SetWindowTextW(_T("当前无任务进行"));
}
Sleep(1000*1); //隔1秒进行一次判断
}
return 0;
}
7.暂停按钮函数实现(挂起线程)
//暂停线程(暂时挂起)
void CspiderBaiDuImgDlg::OnBnClickedBtnpause()
{
// TODO: 在此添加控件通知处理程序代码
//接收搜索内容
m_text.GetWindowTextW(strText);
//接收开始页数,结束页数
m_fistPage.GetWindowTextW(strFistPage);
m_lastPage.GetWindowTextW(strLastPage);
//接收保存路径
m_savaPath.GetWindowTextW(strSavaPath);
//判断接收的数据是否为空若为空,不能执行此操作
if (strText.IsEmpty() | strFistPage.IsEmpty() | strLastPage.IsEmpty() | strSavaPath.IsEmpty())
{
AfxMessageBox(_T("内容填写不完整,请输入完整!"));
return;
}
if (strSavaPath.GetLength() < 3)
{
AfxMessageBox(_T("请输入正确保存目录!"));
return;
}
//如果无爬取操作
if (code == 0)
{
AfxMessageBox(_T("当前无任务执行不能执行此操作!"));
return;
}
//暂停线程操作
pThread->SuspendThread();
//设置线程状态值
code = 2; //表示暂停
int iLen = m_result.GetWindowTextLength();
m_result.SetSel(iLen, iLen, TRUE);
m_result.ReplaceSel(_T("下载已暂停,请进行后续操作"), FALSE);
}
8.恢复按钮函数实现(恢复线程)
//恢复线程
void CspiderBaiDuImgDlg::OnBnClickedBtnresume()
{
// TODO: 在此添加控件通知处理程序代码
//接收搜索内容
m_text.GetWindowTextW(strText);
//接收开始页数,结束页数
m_fistPage.GetWindowTextW(strFistPage);
m_lastPage.GetWindowTextW(strLastPage);
//接收保存路径
m_savaPath.GetWindowTextW(strSavaPath);
//判断接收的数据是否为空若为空,不能执行此操作
if (strText.IsEmpty() | strFistPage.IsEmpty() | strLastPage.IsEmpty() | strSavaPath.IsEmpty())
{
AfxMessageBox(_T("内容填写不完整,请输入完整!"));
return;
}
if (strSavaPath.GetLength() < 3)
{
AfxMessageBox(_T("请输入正确保存目录!"));
return;
}
//如果无爬取操作
if (code==0)
{
AfxMessageBox(_T("当前无任务执行不能执行此操作!"));
return;
}
//恢复线程操作
pThread->ResumeThread();
//设置线程状态值
code = 1; //表示正在进行
//编辑框追加内容
int iLen = m_result.GetWindowTextLength();
m_result.SetSel(iLen, iLen, TRUE);
m_result.ReplaceSel(_T("下载已恢复"), FALSE);
}
9.停止按钮函数实现(结束线程)
//结束线程
void CspiderBaiDuImgDlg::OnBnClickedBtnend()
{
// TODO: 在此添加控件通知处理程序代码
//接收搜索内容
m_text.GetWindowTextW(strText);
//接收开始页数,结束页数
m_fistPage.GetWindowTextW(strFistPage);
m_lastPage.GetWindowTextW(strLastPage);
//接收保存路径
m_savaPath.GetWindowTextW(strSavaPath);
//判断接收的数据是否为空若为空,不能执行此操作
if (strText.IsEmpty() | strFistPage.IsEmpty() | strLastPage.IsEmpty() | strSavaPath.IsEmpty())
{
AfxMessageBox(_T("内容填写不完整,请输入完整!"));
return;
}
if (strSavaPath.GetLength() < 3)
{
AfxMessageBox(_T("请输入正确保存目录!"));
return;
}
//如果无爬取操作
if (code == 0)
{
AfxMessageBox(_T("当前无任务执行不能执行此操作!"));
return;
}
//结束线程操作
TerminateThread(pThread->m_hThread, 0);
//设置线程状态值
code = 0; //表示结束
int iLen = m_result.GetWindowTextLength();
m_result.SetSel(iLen, iLen, TRUE);
m_result.ReplaceSel(_T("下载已结束,欢迎继续使用"), FALSE);
}