C++版网络爬虫
网络爬虫是按照一定规则,自动地抓取万维网信息的程序或脚本。这里用C++语言写一个小程序,实现简单的图片爬取的功能,目的是为了通过这么一个小程序理解爬虫的基本原理及基本流程。其大致实现流程如下:
图一:实现流程图
下面按照流程中的步骤依次用代码实现。
第一步:输入初始的URL
这里要实现的是图片资源的爬取,初始url可以选择一张带图片的网络地址,如http://www.tuxi.com.cn/views-153568413321-1535684133214492.html.其中http://表示协议名称,www.tuxi.com.cn表示主机域名,剩下的部分就表示资源(图片,网页等)在主机上的资源路径
//输入初始url
std::string url;
std::cin >> url;
第二步:创建资源文件夹
创建一个本地文件夹路径,用于存储爬取的资源
//#include <windows.h>
CreateDirectory(L"./Image", NULL);
第三步: URL入队及遍历队列
将初始的url放入队列中,并开始从一个Url开始爬取,首先获取html网页,然后解析网页中的字符串,将包含“http://”字符的字符串提取出来,判断提取的字符串中包含.jpg,.jpeg,.png等图片后缀的字符串,则将其归入资源下载的vector中,待后续下载资源,若不包含的则放入队列中继续循环;
这里注意一点,socket在遍历队列前初始化一次即可,每次遍历一次网页时,连接一次服务器,下载完网页和资源后再释放连接;
//初始化socket
InitSocket();
//将url塞入队列
urlQueue.push(url);
//若队列不为空
while(!urlQueue.empty())
{
//从队列中取出一个URL
std::string curUrl = urlQueue.front();
urlQueue.pop();
//解析URL,获取主机名及资源路径
AnalyseURL(curUrl, strHost, strResPath);
if(strHost.empty())
{
continue;
}
//连接一次,下载 网页,下载资源
Connect();
//下载HTML网页
DownHtmlPage(strHtml);
std::vector<std::string> vecImagesUrl;
//解析网页
AnalyseHtml(strHtml, vecImagesUrl);
//遍历vecImagesUrl中的url,开始下载图片
for(unsigned int i = 0; i < vecImagesUrl.size(); i++)
{
DownImage(vecImagesUrl[i]);
}
}
注意,要是用windows下的socket相关的函数,需要引入Lib,#pragma comment(lib, “ws2_32.lib”),否则编译不通过
//初始化socket
bool InitSocket()
{
//协议版本
WSAData wd;
if(0 != WSAStartup(MAKEWORD(2,2), &wd))
{
return false;
}
if(LOBYTE(wd.wVersion) !=2 || HIBYTE(wd.wVersion) != 2)
{
return false;
}
//创建套接字(流式套接字,TCP)
m_socket = socket(AF_INET, SOCK_STREAM, 0);
if(INVALID_SOCKET == m_socket)
{
return false;
}
return true;
}
// 连接服务器
bool Connect() //连接服务器
{
//根据域名获取主机名
hostent *p = gethostbyname(strHost.c_str());
if(NULL == p)
{
return false;
}
//构建协议地址族
sockaddr_in sa;
memcpy(&sa.sin_addr, p->h_addr, 4);
sa.sin_family = AF_INET;
sa.sin_port = htons(80);//80 host to net short
//连接服务
if(SOCKET_ERROR == connect(m_socket, (sockaddr*)&sa, sizeof(sa)))
{
return false;
}
return true;
}
解析URL
- 完整的URL包含协议+主机(端口,默认80)+资源路径+参数
- 将URL字符串统一转成小写
- 查若字符串中不包含"http://",则return false
- 若字符串长度是否小于7(http://的长度),也return false
- 提取主机名和资源路径
//解析URL
bool AnalyseURL(std::string& url, std::string& strHost, std::string& strResPath)
{
//http://www.tuxi.com.cn/views-153568413321-1535684133214492.html
//将url 字符串转为小写
transform(url.begin(),url.end(),url.begin(),::tolower);
//暂时只找http://,不包含https://
if(std::string::npos == url.find("http://"))//若未查找到
{
return false;
}
if(url.length() <= 7)//若字符长度小于7
{
return false;
}
int pos = url.find('/', 7);//从第7个位置开始查找'/',返回查找到的位置
//http://www.tuxi.com.cn
if(pos == std::string::npos)//若没找到'/'
{
strHost = url.substr(7);//从第7个位置一直截到最后
strResPath = "/";
}
else
{
strHost = url.substr(7, pos - 7);//截取7~pos之间的字符串, 第二个参数表示要截取字符的个数
strResPath = url.substr(pos);
}
if(strHost.empty())
{
return false;
}
return true;
}
下载HTML
- 发送Get请求
- 接收返回头信息
- 接收返回的HTML信息
- 不知道GET请求信息格式,可使用Fiddler工具抓取一个网页,
//下载HTML网页
bool DownHtmlPage(std::string& strHtml)
{
//构建GET/POST请求
//利用Fiddler抓取的一条HTTP请求
//GET http://img0.imgtn.bdimg.com/it/u=928937394,3734864397&fm=27&gp=0.jpg HTTP/1.1
//Host: img0.imgtn.bdimg.com
//Connection: keep-alive
//User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36
//Accept: image/webp,image/apng,image/*,*/*;q=0.8
//Referer: http://image.baidu.com/search/detail?ct=503316480&z=0&ipn=false&word=%E8%83%A1%E6%AD%8C%20%E5%9B%BE%E7%89%87&hs=2&pn=1&spn=0&di=7700&pi=0&rn=1&tn=baiduimagedetail&is=0%2C0&ie=utf-8&oe=utf-8&cl=2&lm=-1&cs=2723770182%2C1520177662&os=1928506863%2C3402861817&simid=68093457%2C629089172&adpicid=0&lpn=0&ln=30&fr=ala&fm=&sme=&cg=star&bdtype=0&oriquery=%E8%83%A1%E6%AD%8C%20%E5%9B%BE%E7%89%87&objurl=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2Fd%2F56775a8644c9d.jpg%3Fdown&fromurl=ippr_z2C%24qAzdH3FAzdH3Fo_z%26e3Bfi7uw1wfit_z%26e3Bv54AzdH3Fetjof-8cncmb98nnd8-8cncmb98nnd899ld_z%26e3Bip4s&gsm=0&islist=&querylist=
//Accept-Encoding: gzip, deflate
//Accept-Language: zh-CN,zh;q=0.9
std::string strRequest;
strRequest = strRequest + "GET " + strResPath + " HTTP/1.1\r\n";
strRequest = strRequest + "Host: " + strHost + "\r\n";
strRequest = strRequest + "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36\r\n";//用于检测客户端浏览器
strRequest = strRequest + "Connection: Close\r\n\r\n";//连接完后直接关闭,注意结束时必须换两行
//发送请求至服务器
if(SOCKET_ERROR == send(m_socket, strRequest.c_str(), strRequest.length(), 0))
{
return false;
}
//char* buffer = new char[strHtml.length()+1];
编码装换
//wchar_t* pBuffer = new wchar_t[(strHtml.length()+1)*2];
//WideCharToMultiByte(CP_UTF8, 0, (LPCWCH)html.c_str(), html.length(),pBuffer,html.(length()+1)*2,NULL,NULL);
//WideCharToMultiByte(CP_ACP, 0,pBuffer , wcslen(pBuffer),buffer,html.length()+1,NULL,NULL);
//从服务器返回的消息一般会包含头信息,解析网页时不需要要用到,这里将其过滤
//同发送消息头一样,接收消息头也是以"\r\n\r\n"结尾的
//接收
char ch = 0;
int res = 0;
while((res = recv(m_socket, &ch, sizeof(ch), 0)) > 0)//一个字节一个字节的接收
{
if(ch == '\r')
{
recv(m_socket, &ch, sizeof(ch), 0);
if(ch == '\n')
{
recv(m_socket, &ch, sizeof(ch), 0);
if(ch == '\r')
{
recv(m_socket, &ch, sizeof(ch), 0);
if(ch == '\n')
{
break;
}
}
}
}
}
//接收HTML
while((res = recv(m_socket, &ch, sizeof(ch), 0)) > 0)//一个字节一个字节的接收
{
strHtml += ch;
}
return true;
}
解析HTML
- 用正则表达式匹配HTML字符串中的URL(注意C++11才支持regex)
- 将匹配出来的url分成两类,一类包含".jpg",".jpeg",".png"的放入vecImages中,剩下的放入队列继续循环
- 遍历完该网页,则下载该网页包含的图片资源
//解析网页
bool AnalyseHtml(const std::string& strHtml, std::vector<std::string>& vecImagesUrl)
{
注意,C++ 11之前标准库中未提供正则表达式,可使用boost
需要用到正则表达式
先匹配出网页中所有的http请求
std::smatch mat;
//注意正则表达式的格式一定要正确
std::regex rex("http://[^\\s'\"<>()]+");//以http://开头,切不包含中括号中的字符一次及以上的字符串
//C++11 的用法
std::string::const_iterator start = strHtml.begin();
std::string::const_iterator end = strHtml.end();
while(std::regex_search(start, end, mat, rex))
{
std::string per(mat[0].first, mat[0].second);
//std::cout << per << std::endl;
//若url中包含图片资源
if(per.find(".jpg") != std::string::npos || per.find(".jpeg") != std::string::npos||per.find(".png") != std::string::npos)
{
vecImagesUrl.push_back(per);
}
else
{
//去掉w3c网页
if(per.find("http://www.w3.org/") == std::string::npos)
{
urlQueue.push(per);//放入队列
}
}
start = mat[0].second;
}
return true;
}
下载图片
- 构建GET请求
- 发送请求至服务器
- 过滤接收头信息
- 接收图片信息并写入文件
//下载图片
bool DownImage(const std::string& imgUrl)
{
//注意,下载图片时,url可能发生变化,需要清空之前的socket,重新连接服务器
closesocket(m_socket);
strHost.empty();
strResPath.empty();
m_socket = 0;
InitSocket();
Connect();
//同先构建GET请求
std::string strRequest;
strRequest = strRequest + "GET " + strResPath + " HTTP/1.1\r\n";
strRequest = strRequest + "Host: " + strHost + "\r\n";
strRequest = strRequest + "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36\r\n";//用于检测客户端浏览器
strRequest = strRequest + "Connection: Close\r\n\r\n";//连接完后直接关闭,注意结束时必须换两行
//发送请求至服务器
if(SOCKET_ERROR == send(m_socket, strRequest.c_str(), strRequest.length(), 0))
{
return false;
}
std::string filename = ".\\Image\\";
filename += strResPath.substr(strResPath.find_last_of('/'));
//打开文件,二进制
FILE* fp = fopen(filename.c_str(), 'wb');
if(NULL == fp)
{
return false;
}
//过滤头信息
//接收
char ch = 0;
int res = 0;
while((res = recv(m_socket, &ch, sizeof(ch), 0)) > 0)//一个字节一个字节的接收
{
if(ch == '\r')
{
recv(m_socket, &ch, sizeof(ch), 0);
if(ch == '\n')
{
recv(m_socket, &ch, sizeof(ch), 0);
if(ch == '\r')
{
recv(m_socket, &ch, sizeof(ch), 0);
if(ch == '\n')
{
break;
}
}
}
}
}
while((res = recv(m_socket, &ch, sizeof(ch), 0)) > 0)//一个字节一个字节的接收
{
//将接收的字符写入文件中
fwrite(&ch, 1, 1, fp);
}
//关闭文件
fclose(fp);
return true;
}
关闭连接,释放socket
- 每遍历完一次URL,则关闭连接
- 遍历完所有的URL后,将socket置为0
//断开连接
closesocket(m_socket);
strHost.empty();
strResPath.empty();
m_socket = 0;