C++实现爬虫,深入理解爬虫原理(最详细,最简单的教程)

news2024/11/19 17:45:25

前言:

我目前主要学习方向是c++,看到网上基本上都是用python写的爬虫,我也试过,确实非常方便,几行代码就能解决,但却就是因为python封装的太好,过于简单,使得很多人包括我最开始的时候,都很难理解爬虫原理.所以就想着能不能用c++实现一个简单的爬虫.

最后我成功实现C++版爬虫从某图片网站爬取了将近两万多张图片,便记录一下,供大家学习

有兴趣的同学可直接下载源码对比学习,下载源码点这里

5月17日更新:

也可进入我的公众号,查看升级优化版爬虫代码文章,以及完整的代码,还有持续更新的资源!

在这里插入图片描述

文章目录

    • 前言:
  • 一、先手动爬虫,理解爬虫原理
  • 二、尝试手动爬取图片
  • 四、寻找网页的规律
  • 四、从手动爬虫中总结一下爬虫的过程
  • 五、写代码前的知识储备
  • 六、开始写代码
  • 总结

一、先手动爬虫,理解爬虫原理

首先我们得清楚网页的原理,如一页展示图片的网页,右键可查看网页源代码
(考虑到该程序的特殊性,避免不必要的麻烦,不方便展示该网站名称,大家可自行找图片网站替代,尽量与本文的网站格式相近)
(当然也可以下载我的源代码,里面有详细的注释,VS2019环境下亲测可用)
在这里插入图片描述
网页源代码如图:
在这里插入图片描述
为方便演示爬虫原理,可在源代码页Ctrl+A 全选 ,再Ctrl+C复制,然后在桌面新建一个txt文本,Ctrl+V复制结果如图:
在这里插入图片描述
然后保存退出,修改文件后缀名为.html
修改前:
在这里插入图片描述
修改后:
在这里插入图片描述
然后直接点击该文件,可以发现显示出与先前网站一样内容的网页,只是显示样式发生了变化.
在这里插入图片描述

至此,我们就完成了一次手动爬取一张网页

而此次我们想爬取的不是网页,而是网页上展示的图片,所以这还不够

二、尝试手动爬取图片

正如上所诉,图片是在网页上展示的,所以我们就要尝试在网页上找到图片的地址
在网页源代码中,可以很容易找到以<img src="…"形式展示的标签
如图:
在这里插入图片描述
当尝试点击img 后紧跟的链接时,发现展示出一张图片,就是先前网页上看到的某张图片
在这里插入图片描述
在这里插入图片描述
然后右键该图片,另存任意位置
在这里插入图片描述
至此,我们就完成了一次手动爬取图片的过程

四、寻找网页的规律

完成了手动爬取一个网页和一张图片后,我们可以发现,即使是使用了程序代替手工,每次爬取的图片都是有限的,因为大量图片都存放在大量不同的网页中,所以我们还需要找到各个网页之间的规律
于是我点开下一页图片,可以看到地址栏发生了变化
第一页:
在这里插入图片描述

第二页
在这里插入图片描述

第三页:
在这里插入图片描述

可以发现,除了第一页的地址后缀为index,之后的其它网页都是index_x.html结尾,x为它的页数

至此,我们就发现了该网站网页的规律:除了第一页,其它网页都是以站名/index_X.html的格式存在,X为网页的页数

四、从手动爬虫中总结一下爬虫的过程

1.将要爬取的网页源代码下载到本地
2.从网页源代码中解析出图片的地址,然后下载到本地.
3.若不再使用网页源代码,可删除,避免占用大量磁盘空间.然后回到第一步循环,直到爬取完成

五、写代码前的知识储备

1.针对下载网页源代码和下载图片的需求,可使用微软提供的一个下载函数可以很方便实现下载功能:
头文件:

#include<Windows.h> //头文件
#pragma comment(lib,"Urlmon.lib") //链接包含该函数的静态库

函数:

HRESULT URLDownloadToFileA(
LPUNKNOWN pCaller, //用于显示下载进度,但需要继承COM接口,过于麻烦,可直接填0
LPCTSTR    zURL,//填写要下载的地址
LPCTSTR   szFileName,//填写下载完成到本地,保存的文件名
DWORD    dwReserved,//保留参数,必须为0
LPBINDSTATUSCALLBACK lpfnCB //接受下载进度的回调函数,不需要,直接填0
);
返回值为S_OK则成功,否则下载失败

该函数功能:将zURL参数地址下载保存为本地szFileName参数文件

关于该函数的更多详细内容,请点击这里!

2.针对要从网页源代码中解析出图片地址,可以使用C++自带的正则表达式库

#include<regex>

还不会正则表达式戳这里马上学习

这里主要用到的是该库的regex_search函数

bool regex_search(
_BidIt _First, //进行匹配的起始迭代器,就是从哪里开始匹配
 _BidIt _Last, //进行匹配的结束迭代器,就是从哪里结束匹配
match_results<_BidIt, _Alloc>& _Matches,//匹配到的结果返回给这个参数
const basic_regex<_Elem, _RxTraits>& _Re,//填写匹配规则的参数,也就是正则表达式
regex_constants::match_flag_type _Flgs=regex_constants::match_default //匹配模式,用默认的就行,不用填
)

该函数将在_First参数到_Last参数指定的范围匹配符合_Re模式的字符串,将结果存储到_Matches参数中

使用方法是将_Re正则表达式中,匹配到的两个小括号内的内容(.*)送回到_Matches参数,通过_Matches[index].str()获取想要的内容,index为第几个小括号匹配的内容

再通过_Matches[0].second得到当前已经匹配到的位置,方便继续循环匹配

不好理解继续往下看,实践是最好的学习方法!

六、开始写代码

首先,在任意一个盘新建一个空文件夹,方便后面使用

我在D盘下新建了一个叫ps的空文件夹
在这里插入图片描述
然后在VS中新建一个控制台项目,我这里项目名称为test,并新建一个叫main.cpp的空文件
在这里插入图片描述
添加头文件和库

#include<Windows.h>
#include<regex>
#include<iostream>
#pragma comment(lib,"Urlmon.lib")
using namespace std;
int main() {

}

在这里插入图片描述
写个循环,生成所有网页的地址,因为是从第二页开始有规律的,所以这里直接从2开始循环(网站不方便展示,可下载我的源代码)

	string url;
	url.resize(1024);
	for (int i = 2; i <= 1212; i++) {
		sprintf_s((char*)url.c_str(), 1024, "http://....../index_%d.htm", i);
		
	}

在这里插入图片描述
写一个通用函数GetTextFromUrlA,从指定URL中获取想要的文本内容

/// <summary>
/// 从网页中获取指定文本
/// </summary>
/// <param name="url">网页地址</param>
/// <param name="pattern">匹配模式</param>
/// <param name="patternIndex">返回第几个()中的内容</param>
/// <param name="num">返回匹配到的数量</param>
/// <returns>返回的字符串数组</returns>
string* GetTextFromUrlA(const string& url, const string& pattern, int patternIndex, int& num)
{
	HRESULT ret = URLDownloadToFileA(NULL, url.c_str(), ".\tmp.txt", 0, NULL); //下载网页到tmp.txt文件中
	if (ret != S_OK) { //如果下载失败返回NULL
		return NULL;
	}
	//下载成功,读取文本内容
	FILE* file;
	errno_t err = fopen_s(&file, ".\tmp.txt", "r");
	if (err != 0) {
		return NULL;
	}
	fseek(file, 0, SEEK_END);
	int nSize = ftell(file);
	fseek(file, 0, SEEK_SET);
	std::string buf;
	buf.resize(nSize + 1);
	fread((char*)buf.c_str(), sizeof(char), nSize, file);
	fclose(file);

	//开始匹配
	regex r(pattern); //初始化匹配模式变量r
	smatch result;	  //保存匹配到的结果result
	string::const_iterator begin = buf.begin(); //获取文本开始的迭代器
	string::const_iterator end = buf.end();		//获取文本结束的迭代器
	int i = 0; //统计可以匹配到的个数
	while (regex_search(begin, end, result, r)) { //匹配成功返回true,继续下一次匹配.失败则退出循环
		i++; //匹配到一个,加一
		begin = result[0].second;  //获取当前匹配到的位置,更新匹配的开始位置
	}
	num = i;
	//知道了有多少个,分配对应内存,重新开始匹配
	begin = buf.begin();
	string* strBuf = new string[i + 1]{}; 
	int index = 0;
	while (regex_search(begin, end, result, r)) {
		strBuf[index++] = result[patternIndex].str();
		begin = result[0].second;
	}
	
	DeleteFileA(".\tmp.txt"); //匹配完成,可以删除下载的文件了

	return strBuf; //返回匹配到的结果
}

在这里插入图片描述
然后写匹配模式,调用该函数,因为我写的匹配模式串中,第一个括号里的是我想要的内容,所以后面填的1,这个请根据大家具体情况写适当的匹配模式串,需要注意的是C++中 " 这个符号需要写成 "

int nums = 0;
string* str =GetTextFromUrlA(url.c_str(), "<li><a href="(.*?)"\s{0,1}title="(.*?)".*?><img src="(.*?)" .*?</li>", 1, nums);
if (nums <= 0) {
	cout << "该页面下载失败!" << endl;
	continue;
}

在这里插入图片描述
测试一下下载函数是否有问题

在这里插入图片描述
成功获取指定内容,大家可能已经发现,这些字符串都是.htm结尾,说明这还是个html文档,根本不是图片,而且都是缺少站名的部分索引

这是因为我发现,该网站是两层文档展示的,随便点进去一张图片,第二层,长这样
在这里插入图片描述
所以接下来,我们要做的就是,补全URL,再从第二层网页中获取图片地址就可以了!

for (int j = 0; j < nums; j++) {
	Sleep(1000); //避免过快访问网站,被封杀ip,多次实验,1秒爬取一张,可以稳定爬取
	sprintf_s((char*)url.c_str(), 1024, "http://....%s", str[j].c_str()); //格式化图片url
	int nPic{};
	string* sPic = GetTextFromUrlA(url.c_str(), "<div class="pic">.*?<img src="(.*?)" .*?</p>", 1, nPic);//获取图片地址
	if (nPic <= 0) {
		continue;
	}
	for (int k = 0; k < nPic; k++) {
		cout << sPic[k] << endl;
	}
	delete[] sPic;
}

在这里插入图片描述

成功获取该页面所有图片的地址

在这里插入图片描述
最后就简单了,将这些地址下载保存就可以了

因为需要解析名字之类的,所以最好还是写个函数

/// <summary>
/// 将url指定图片下载到本地dir目录下
/// </summary>
/// <param name="dir">存放图片的目录</param>
/// <param name="url">要下载的图片</param>
/// <returns></returns>
bool DownPic(const string& dir,const string& url) {
	//将图片名字解析到name变量中
	string name;
	for (int i = url.size()-1; i >= 0; i--) {
		if (url[i] == '/') {
			name = &url[i+1];
			break;
		}
	}
	name = dir + "\" + name; //将name转化为本地目录文件
	HRESULT ret=URLDownloadToFileA(NULL, url.c_str(), name.c_str(), 0, NULL);//下载
	if (ret == S_OK) {
		return true;
	}

	return false;
}

在这里插入图片描述
最后测试一下
在这里插入图片描述
完美爬取

完整代码(缺少站名):

#include<Windows.h>
#include<regex>
#include<iostream>
#pragma comment(lib,"Urlmon.lib")
using namespace std;
/// <summary>
/// 从网页中获取指定文本
/// </summary>
/// <param name="url">网页地址</param>
/// <param name="pattern">匹配模式</param>
/// <param name="patternIndex">返回第几个()中的内容</param>
/// <param name="num">返回匹配到的数量</param>
/// <returns>返回的字符串数组</returns>
string* GetTextFromUrlA(const string& url, const string& pattern, int patternIndex, int& num)
{
	HRESULT ret = URLDownloadToFileA(NULL, url.c_str(), ".\tmp.txt", 0, NULL); //下载网页到tmp.txt文件中
	if (ret != S_OK) { //如果下载失败返回NULL
		return NULL;
	}
	//下载成功,读取文本内容
	FILE* file;
	errno_t err = fopen_s(&file, ".\tmp.txt", "r");
	if (err != 0) {
		return NULL;
	}
	fseek(file, 0, SEEK_END);
	int nSize = ftell(file);
	fseek(file, 0, SEEK_SET);
	std::string buf;
	buf.resize(nSize + 1);
	fread((char*)buf.c_str(), sizeof(char), nSize, file);
	fclose(file);

	//开始匹配
	regex r(pattern); //初始化匹配模式变量r
	smatch result;	  //保存匹配到的结果result
	string::const_iterator begin = buf.begin(); //获取文本开始的迭代器
	string::const_iterator end = buf.end();		//获取文本结束的迭代器
	int i = 0; //统计可以匹配到的个数
	while (regex_search(begin, end, result, r)) { //匹配成功返回true,继续下一次匹配.失败则退出循环
		i++; //匹配到一个,加一
		begin = result[0].second;  //获取当前匹配到的位置,更新匹配的开始位置
	}
	num = i;
	//知道了有多少个,分配对应内存,重新开始匹配
	begin = buf.begin();
	string* strBuf = new string[i + 1]{};
	int index = 0;
	while (regex_search(begin, end, result, r)) {
		strBuf[index++] = result[patternIndex].str();
		begin = result[0].second;
	}

	DeleteFileA(".\tmp.txt"); //匹配完成,可以删除下载的文件了

	return strBuf; //返回匹配到的结果
}
/// <summary>
/// 将url指定图片下载到本地dir目录下
/// </summary>
/// <param name="dir">存放图片的目录</param>
/// <param name="url">要下载的图片</param>
/// <returns></returns>
bool DownPic(const string& dir,const string& url) {
	//将图片名字解析到name变量中
	string name;
	for (int i = url.size()-1; i >= 0; i--) {
		if (url[i] == '/') {
			name = &url[i+1];
			break;
		}
	}
	name = dir + "\" + name; //将name转化为本地目录文件
	HRESULT ret=URLDownloadToFileA(NULL, url.c_str(), name.c_str(), 0, NULL);//下载
	if (ret == S_OK) {
		return true;
	}

	return false;
}

int main() {
	string url; //保存url
	url.resize(1024);
	for (int i = 2; i <= 1212; i++) {
		cout << "当前页面:"<<i << endl;
		sprintf_s((char*)url.c_str(), 1024, "http://.../index_%d.htm", i); //格式化url
		int nums = 0;
		string* str = GetTextFromUrlA(url.c_str(), "<li><a href="(.*?)"\s{0,1}title="(.*?)".*?><img src="(.*?)" .*?</li>", 1, nums);
		if (nums <= 0) {
			cout << "该页面下载失败!" << endl;
			continue;
		}
		for (int j = 0; j < nums; j++) {
			Sleep(1000); //避免过快访问网站,被封杀ip,多次实验,1秒爬取一张,可以稳定爬取
			sprintf_s((char*)url.c_str(), 1024, "http://...%s", str[j].c_str()); //格式化图片url
			int nPic{};
			string* sPic = GetTextFromUrlA(url.c_str(), "<div class="pic">.*?<img src="(.*?)" .*?</p>", 1, nPic);//获取图片地址
			if (nPic <= 0) {
				continue;
			}
			for (int k = 0; k < nPic; k++) {
				if (DownPic("D:\ps", sPic[k])) cout <<sPic[k]<<":下载成功"<<endl;
				else cout << sPic[k] << ":下载失败"<<endl;
			}
			delete[] sPic;
		}

		delete[] str;
	}
}

总结

对比之下,python写爬虫确实非常方便,也推荐大家用python写爬虫.

但在此之前,应该要理解爬虫的原理,C++虽然繁琐,但却是理解原理的好助手.

当大家慢慢积累出属于自己的C++类库时,也许比python更好用也说不一定呢

源码下载

先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/409139.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue执行配置选项npm run serve的本质

vue执行配置选项npm run serve运行开发服务器的本质 目录 vue执行配置选项npm run serve运行开发服务器的本质 一、启动开发服务器、调用“工具链”、编译、并运行当前工程 二、npm run serve内部原理分析 三、vue编译运行过程 喜欢的&#xff0c;就收藏并点个赞&#xff…

前端解决跨域问题(9个方法)

什么是跨域&#xff1f; 跨域&#xff0c;是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的&#xff0c;是浏览器对JavaScript实施的安全限制。 这里说明一下&#xff0c;无法跨域是浏览器对于用户安全的考虑&#xff0c;如果自己写个没有同源策略的浏览器&a…

微信小程序篇_01 微信小程序与Java后端接口交互

微信小程序与Java后端接口交互准备创建后端项目创建小程序项目本文主要介绍小程序前后端数据的交互&#xff0c;实践演示。 准备 创建后端项目 我这里就创建一个SpringBoot项目作为演示。 在创建项目中选择Spring Initializr 要勾选SpringWeb框架&#xff0c;当然你也可以…

在html页面以导入的方式使用axios进行异步请求

一、下载axios GitHub地址&#xff1a;https://github.com/axios/axios 注&#xff1a;该页面下面有官方的帮助文档内容。 下载zip包到本地后解压 在axios-1.x\dist\esm下找到axios.min.js文件 这个文件就是后面需要导入的文件 二、导入文件 首先在IDEA中的webapp文件夹下…

使用 Vue3 开发了四个月,回顾 emit 的用法

前言&#xff1a;不知不觉已经从 React 转 Vue3 开发四个多月了&#xff0c;但其实自从刚接触 vue 开始&#xff0c;就对 emit 这个指令似懂非懂。所以在项目中其实我还是很害怕去使用这个指令的&#xff0c;但是在有些场景又不得不使用&#xff0c;就只能看着同事的代码比葫芦…

Vue的环境配置

vue环境配置安装地址&#xff1a;安装 — Vue.js 一、Node官网地址&#xff1a;下载 | Node.js 中文网 查看node版本∶打开命令工具&#xff0c;执行"node -v"命令查看版本号。 二、git-bash命令行工具 git-bash命令行工具官网地址&#xff1a;Git 查看git版本&#…

【Vue】跳转外部链接并携带参数并触发

需求&#xff1a; 当平台运行多个系统时&#xff08;"系统一"&#xff0c;"系统二" 都是不同时间段开发的&#xff09; 可以从"系统一"点击按钮跳转到"系统二"&#xff0c;并且完成 "系统二" 登录操作&#xff0c;直接显示&…

【小程序项目开发 --- 京东商城】 启航篇之uni-app项目搭建

&#x1f935;‍♂️ 个人主页: 计算机魔术师 &#x1f468;‍&#x1f4bb; 作者简介&#xff1a;CSDN内容合伙人&#xff0c;全栈领域优质创作者。 &#x1f310; 推荐一款找工作神器网站: 点击跳转牛客网 |笔试题库|面试经验|实习招聘内推| 还没有账户的小伙伴 速速点击链接…

【VUE3】ElementUI--el-date-picker下拉控件样式修改(高亮显示设置)

ElementUI--el-date-picker下拉控件样式修改一些废话事发背景实现效果实现思路总结和完整代码参考资料一些废话 默默做前端&#xff0c;分享一些自己在项目需求实现中遇到的奇妙问题&#xff08;主要是网上搜索不到解决办法自己解决后的总结方法和解决办法但不全面&#xff0c;…

关于oss使用sts 后台签发临时token前端直传大文件的错误记录

文章目录前言遇到的问题1. NoSuchBucket : The specified bucket does not exist.2. com.aliyuncs.exceptions.ClientException: InvalidParameter.RoleSessionName : The parameter RoleSessionName is wrongly formed.3. 报错&#xff1a;Access to XMLHttpRequest at 上传ur…

servlet交互过程图详解,servlet的常见问题,创建web项目(一)

目录 一.B/S模式和C/S模式 C/S&#xff1a;客户端服务器端结构模式 B/S&#xff1a;浏览器服务器模结构模式 B/S结构图&#xff1a; 二.创建web项目 三.举例说明Servlet执行流程图&#xff1a; 四.Servlet项目中常见的问题 一.当创建web项目时&#xff0c;没有勾选Java …

一篇文章吃透 CSS3 属性: transition过渡 与 transform动画

最近有人私信我 CSS 中的 transition (过渡) 和 transform (动画) 属性&#xff0c;这两个属性的参数确实比较复杂&#xff0c;它们可以做出 CSS 的一些基础动画效果&#xff0c;平移&#xff0c;旋转&#xff0c;倾角......等等&#xff0c;这些也是我早期学习 CSS 的难记易忘…

Java Web 开发详解

一、Web基础 1、Web 概述 Web 在英文中的含义是网状物、网络。在计算机领域&#xff0c;它通常指的是后者&#xff0c;即网络。 像 WWW 是由 3 个单词组成的&#xff0c;即World Wide Web&#xff0c;中文含义是万维网。 他们的出现都是为了让我们在网络的世界中获取资源&a…

JS总结——数组,对象遍历的方法

对于数组或者对象的遍历&#xff0c;筛选&#xff0c;提取等操作是前端开发中经常有的需求&#xff0c;不要再只会写普通的for循环了&#xff0c;虽然普通的for循环已经能完成一切的功能的&#xff0c;是一个特殊方法&#xff0c;但是针对各种需求&#xff0c;js给我们提供了许…

uniapp —— 实现左右联动商品分类页面

uniapp —— 实现左右联动商品分类页面 零、前因 我们在日常的开发种&#xff0c;这种页面在项目当中经常会用得到&#xff0c;所以本篇文章会比较注重描述其思路&#xff0c;顺带附上其代码&#xff0c;以便以后在任何代码环境下都能使用上&#xff0c;先介绍一下实现思路&a…

Vue 中 iconfont 使用

图标库的发展过程&#xff0c;iconfont图标库的重要性&#xff08;项目常用&#xff09; 对于前端而言&#xff0c;图标的发展可谓日新月异。从img标签&#xff0c;到雪碧图&#xff0c;再到字体图标&#xff0c;svg&#xff0c;甚至svg也有了类似于雪碧图的方案svg-sprite-lo…

npm run 是什么?为什么使用npm run 这一命令,就能够将 webpack 跑起来并进行下一步的操作?

npm run 实际上是衔接 node 和 webpack 的连接点。先看看终端运行的npm是什么,如下图: 图中的关键点是最后一行 C:\Users\***\AppData\Roaming\npm\node_modules\npm。从它可以推断出系统环境变量下配置的npm的路径为C:\Users\***\AppData\Roaming\npm,可以去环境变量中确认…

css实现炫酷充电动画

先绘制一个电池&#xff0c;电池头部和电池的身体 这里其实就是两个div&#xff0c;使用z-index改变层级&#xff0c;电池的身体盖住头部&#xff0c;圆角使用border-radius完成 html部分,完整的css部分在最后 <div class"chargerBox"><div class"ch…

Vue ref获取元素和组件实例

获取元素 获取元素还不简单&#xff1f;直接document.querySelector(“#id”)不就获取到了吗&#xff1f;例如下面的代码。在写Vue的过程中&#xff0c;我们从来不会写这种代码&#xff0c;但是下面的代码在vue里面是可以运行的。 methods:{getEl(){let title document.queryS…

CSS实现: 水平居中 的几种方法

实现方法&#xff1a; 1、添加 margin 值 auto 2、定位 position(子绝父相) 偏移值 left margin-left 回退 [ 需要计算&#xff0c;有点 麻烦 ] 3、定位 position(子绝父相) 偏移值 left CSS-2d transform 4、文字居中 text-align:center; 行内块元素 5、弹性盒子布局 [ 推…