【C++&爬虫】C++实现网络爬虫&socket初级教程
2019年了,发现以前的很多教程都不能用了。
我自己写的socket发给服务器总是返回301错误——资源永久转移。很多教程都是这样,困扰了我很久。
终于我发现了一篇能用的爬虫代码,参考MSDN以及众多博主的博客,大概给这篇代码做了注解。
#define _WINSOCK_DEPRECATED_NO_WARNINGS #include <iostream>
#include <vector>
#include <list>
#include <map>
#include <queue>
#include <string>
#include <utility>
#include <regex>
#include <fstream>
#include <WinSock2.h>
#include <Windows.h> #pragma comment(lib, "ws2_32.lib") using namespace std; void startupWSA() //初始化socket
{
WSADATA wsadata;
WSAStartup(MAKEWORD(, ), &wsadata);
//参数1:指定wsa版本
//参数2:传输版本,套接字规范等信息到WSADATA,用于接收WSA套接字详细信息
} inline void cleanupWSA() //释放socket
{
WSACleanup();
//无参数,清理释放WSA资源
} inline pair<string, string> binaryString(const string &str, const string &dilme)
{
pair<string, string> result(str, "");
auto pos = str.find(dilme);
if (pos != string::npos)
{
result.first = str.substr(, pos);
result.second = str.substr(pos + dilme.size());
}
return result;
} inline string getIpByHostName(const string &hostName) //从域名获得IP地址
{
hostent* phost = gethostbyname(hostName.c_str()); //从域名得到IP地址(DNS)
//hostent:该结构通过函数来存储关于一个给定的主机,如主机名,IPv4地址
return phost ? inet_ntoa(*(in_addr *)phost->h_addr_list[]) : ""; //返回得到的点分十进制IP地址,如果转换失败返回""
//inet_ntoa:将一个32位网络字节序的二进制IP地址转换成相应的点分十进制的IP地址
} inline SOCKET connect(const string &hostName) //
{
auto ip = getIpByHostName(hostName); //获得host(IP) (上函数)
if (ip.empty())
return ;
auto sock = socket(AF_INET, SOCK_STREAM, );
//参数1(domain):协议域,又称协议族(family)。
//常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。
//协议族决定了socket的地址类型,在通信中必须采用对应的地址,
//如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、
//AF_UNIX决定了要用一个绝对路径名作为地址。 //参数2(type):指定Socket类型。
//常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。
//流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。
//数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用。 //参数3(protocol):指定协议。
//常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,
//分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
//注意:1.type和protocol不可以随意组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合。
//当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。 if (sock == INVALID_SOCKET)
return ;
//INVALID_SOCKET:该返回值代表创建套接字错误
SOCKADDR_IN addr;
addr.sin_family = AF_INET;
addr.sin_port = htons();
addr.sin_addr.s_addr = inet_addr(ip.c_str());
if (connect(sock, (const sockaddr *)&addr, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)
return ;
//参数1:套接字描述(之前创建的套接字)
//参数2:指向结构sockaddr的指针(取地址)
//参数3:结构的大小 //返回值(SOCKET_ERROR):表示连接失败 //SOCKADDR_IN:该结构主要使用三个变量(成员)
//sin_family:指定协议族,可参考前面socket函数的第一个参数解释
//sin_port:网络字节序,指的是整数在内存中保存的顺序,即主机字节顺序
//(使用的函数htons:
//将主机字节顺序转为网络字节顺序, 不同的CPU有不同的字节顺序类型,
//这些字节顺序类型指的是整数在内存中保存的顺序,即主机字节顺序。
//将整型变量从主机字节顺序转变成网络字节顺序, 就是整数在地址空间存储方式变为:
//高位字节存放在内存的低地址处。
//例如 : 12345->0x3039(16进制)->0x930(字节翻转)--> 14640 )
//sin_addr:其中成员s_addr是IPv4地址结构,IN_ADDR结构
//(使用的inet_addr:该函数转换包含IPv4点分十进制地址转换成一个适当的地址的字符串 IN_ADDR结构。)
return sock;
} inline bool sendRequest(SOCKET sock, const string &host, const string &get)
{
string http
= "GET " + get + " HTTP/1.1\r\n"
+ "HOST: " + host + "\r\n"
+ "Connection: close\r\n\r\n"; //设置报文
return http.size() == send(sock, &http[], http.size(), ); //发送请求
//参数1:socket,之前创建的套接字
//参数2:要发送的数据
//参数3:数据大小
//参数4:调用执行方式,默认写0即可 } inline string recvRequest(SOCKET sock)
{
static timeval wait = { , };
static auto buffer = string( * , '\0'); //初始化string容量
auto len = , reclen = ;
do {
fd_set fd = { };
//fd_set:实际上是一个long型数组,是文件描述符的集合
//每一个数组元素都能与一打开的文件句柄(不管是socket句柄,还是其他文件或命名管道或设备句柄)建立联系,
//建立联系的工作由程序员完成,当调用select()时,
//由内核根据IO状态修改fd_set的内容,
//由此来通知执行了select()的进程哪一socket或文件发生了可读或可写事件。
//总之,这个结构维护一个或者多个socket(的状态)
FD_SET(sock, &fd);
//FD_SET:用于维护fd_set集合的宏
//参数1:socket套接字
//参数2:传入的fd_set
reclen = ;
if (select(, &fd, nullptr, nullptr, &wait) > )
{
reclen = recv(sock, &buffer[] + len, * - len, );
if (reclen > )
len += reclen;
}
//select:非阻塞式的函数,用于确定一个或者多个socket的状态
//对每一个套接口,调用者可查询它的可读性、可写性及错误状态信息,
//用fd_set结构来表示一组等待检查的套接口 //参数1(nfds):socket监视的文件句柄数,视进程中打开的文件数而定。
//参数2(readfds):socket监视的可读文件句柄集合
//参数3(writefds):socket监视的可写文件句柄集合
//参数4(exceptfds):socket监视的异常文件句柄集合
//参数5(timeout):传入参数,本次socket()超时结束时间(可精确到百万分之一秒) //recv:用于从服务器接收数据的函数 //参数1:socket套接字
//参数2:接收数据的缓冲区(buffer)
//参数3:缓冲区长度
//参数4:指定调用方式,默认写0 //返回值:成功接收的字节长度
FD_ZERO(&fd);
//FD_ZERO:用于清空fd_set集合的宏
//参数1:传入fd_set集合参数 //与fd_set配套的宏有:
//FD_CLR(s, *set)
//从集合中删除s这个元素
//FD_ISSET(s, *set)
//判断s是否是集合成员,是返回非0,否则返回0
//FD_SET(s, *set)
//将s作为成员加入集合
//FD_ZERO(*set)
//将集合初始化(为空集合)
} while (reclen > ); return len >
? buffer[] == '' && buffer[] == '' && buffer[] == ''
? buffer.substr(, len)
: ""
: "";
//如果返回的字节长度大于11,那么...
//...如果服务器发送的状态码为200 OK...
//...那么返回发来的数据;
//...如果不是200 OK...
//...返回""
//如果不是大于11...
//...返回""
} inline void extUrl(const string &buffer, queue<string> &urlQueue)
{
if (buffer.empty())
{
return;
}
smatch result;
auto curIter = buffer.begin();
auto endIter = buffer.end();
while (regex_search(curIter, endIter, result, regex("href=\"(https?:)?//\\S+\"")))
{
urlQueue.push(regex_replace(
result[].str(),
regex("href=\"(https?:)?//(\\S+)\""),
"$2"));
curIter = result[].second;
}
} void Go(const string &url, int count) //BFS
{
queue<string> urls;
urls.push(url);
for (auto i = ; i != count; ++i)
{
if (!urls.empty())
{
auto &url = urls.front();
auto pair = binaryString(url, "/");
auto sock = connect(pair.first);
if (sock && sendRequest(sock, pair.first, "/" + pair.second))
{
auto buffer = move(recvRequest(sock));
extUrl(buffer, urls);
}
closesocket(sock); //关闭socket
cout << url << ": count=> " << urls.size() << endl; //统计该网页url数量
urls.pop(); }
}
} int main()
{
startupWSA(); //开启WSA
Go("www.hao123.com", ); //从www.hao123.com开始,计数200次
cleanupWSA(); //WSA释放
return ;
}
Code
请尽量使用Visual Studio2017(或者VS系列)进行编译,避免IDE听不懂各自的方言。
注释已经非常详细了,接下来是引用的博客:
主要代码: https://www.cnblogs.com/mmc1206x/p/3932622.html
对于关键函数的参数说明: https://www.jianshu.com/p/e3c187da4420
对于fd_set以及select函数通俗易懂的解读: https://blog.csdn.net/rootusers/article/details/43604729
以上是主要思路以及部分函数参考的博客,我的注释中不足之处请看这些博客;
以下是MSDN官方文档以及维基/百度百科等参考的资料:
//对于MSDN以及英文资料的翻译: fanyi.baidu.com 和 translate.google.com
https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-select
https://zh.wikipedia.org/wiki/Select_(Unix)
https://baike.baidu.com/item/fd_set/6075513
https://www.ibm.com/support/knowledgecenter/en/SSB23S_1.1.0.15/gtpc2/cpp_fd_set.html
https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-fd_set
https://docs.microsoft.com/en-us/windows/desktop/api/wsipv6ok/nf-wsipv6ok-inet_addr
https://docs.microsoft.com/zh-cn/windows/desktop/api/winsock2/ns-winsock2-in_addr
https://docs.oracle.com/cd/E19620-01/805-4041/6j3r8iu2l/index.html
https://docs.microsoft.com/en-us/windows/desktop/api/winsock/ns-winsock-hostent
https://docs.microsoft.com/en-us/windows/desktop/api/winsock/ns-winsock-wsadata
https://docs.microsoft.com/en-us/windows/desktop/api/winsock/nf-winsock-wsastartup
本注释讲解不足支持,或者想要获得更加详细的资料,请访问以上链接。
【C++&爬虫】C++实现网络爬虫&socket初级教程的更多相关文章
- Python 网络爬虫 001 (科普) 网络爬虫简介
Python 网络爬虫 001 (科普) 网络爬虫简介 1. 网络爬虫是干什么的 我举几个生活中的例子: 例子一: 我平时会将 学到的知识 和 积累的经验 写成博客发送到CSDN博客网站上,那么对于我 ...
- python网络爬虫之初始网络爬虫
第一次接触到python是一个很偶然的因素,由于经常在网上看连载小说,很多小说都是上几百的连载.因此想到能不能自己做一个工具自动下载这些小说,然后copy到电脑或者手机上,这样在没有网络或者网络信号不 ...
- Python爬虫《Python网络爬虫相关基础概念》
引入 之前在授课过程中,好多同学都问过我这样的一个问题:为什么要学习爬虫,学习爬虫能够为我们以后的发展带来那些好处?其实学习爬虫的原因和为我们以后发展带来的好处都是显而易见的,无论是从实际的应用还是从 ...
- python网络爬虫之初识网络爬虫
第一次接触到python是一个很偶然的因素,由于经常在网上看连载小说,很多小说都是上几百的连载.因此想到能不能自己做一个工具自动下载这些小说,然后copy到电脑或者手机上,这样在没有网络或者网络信号不 ...
- PYTHON网络爬虫与信息提取[网络爬虫协议](单元二)
robots.txt在网站的根目录下 遵守 自动或人工识别robots.txt再进行内容爬取 约束性:建议性,不遵守协议,存在法律风险. 基本语法: User-agent: * Disallow: / ...
- Python网络爬虫
http://blog.csdn.net/pi9nc/article/details/9734437 一.网络爬虫的定义 网络爬虫,即Web Spider,是一个很形象的名字. 把互联网比喻成一个蜘蛛 ...
- 【Python开发】【神经网络与深度学习】网络爬虫之python实现
一.网络爬虫的定义 网络爬虫,即Web Spider,是一个很形象的名字. 把互联网比喻成一个蜘蛛网,那么Spider就是在网上爬来爬去的蜘蛛. 网络蜘蛛是通过网页的链接地址来寻找网页的. 从网站某一 ...
- [Python]网络爬虫(一):抓取网页的含义和URL基本构成
一.网络爬虫的定义 网络爬虫,即Web Spider,是一个很形象的名字. 把互联网比喻成一个蜘蛛网,那么Spider就是在网上爬来爬去的蜘蛛.网络蜘蛛是通过网页的链接地址来寻找网页的. 从网站某一个 ...
- larbin是一种开源的网络爬虫/网络蜘
larbin是一种开源的网络爬虫/网络蜘蛛,由法国的年轻人 Sébastien Ailleret独立开发.larbin目的是能够跟踪页面的url进行扩展的抓取,最后为搜索引擎提供广泛的数据来源.Lar ...
随机推荐
- 阿里云 Django部署参考
Linux下安装Python3和django并配置mysql作为django默认服务器 CentOS7.3安装Python3.6 yum except KeyboardInterrupt, e: 错误 ...
- android ViewPager实现的轮播图广告自定义视图,网络获取图片和数据
public class SlideShowAdView extends FrameLayout { //轮播图图片数量 private static int IMAGE_COUNT = 3; ...
- getHiddenProp() 浏览器状态切换改变
<script> function getHiddenProp() { var prefixes = ['webkit', 'moz', 'ms', 'o']; // if 'hidden ...
- cc.AudioSource
cc.AudioSource1:AudioSource组件是音频源组件, 发出声音的源头2: AudioSource组件面板: clip: 声源的播放的音频对象: AudioClip, mp3, wa ...
- 题解 洛谷P1903/BZOJ2120【[国家集训队]数颜色 / 维护队列】
对于不会树套树.主席树的本蒟蒻,还是老老实实的用莫队做吧.... 其实这题跟普通莫队差不了多远,无非就是有了一个时间,当我们按正常流程排完序后,按照基本的莫队来,做莫队时每次循环对于这一次操作,我们在 ...
- Python学习笔记(2)——Python的函数、模块、包和库
Table of Contents 1. 函数. 2. 模块. 3. 包(package) 4. 库(library) 初识Python,对于没有接触过编程的我,恐怕只能听懂什么是函数,这里介绍一下几 ...
- centOS取消锁屏
自己在使用虚拟机运行centos 7时,centos 7默认几分钟不动就锁屏,实在很讨厌,所以在设置中将其去掉 1.左上角点击应用程序,在下面选择系统工具,在系统工具中选择设置 2.选择设置下面的隐私 ...
- odoo 权限配置讲解
今天来讲解一下odoo权限配置的简单讲解,配合公司开发的权限模块的使用,进行odoo权限配置的说明 BaseSecurityExtend 2.0模块 这是公司自主开发的一款针对odoo菜单级别进行可视 ...
- allegro中原理图和pcb中元件的交互
一.前言: 所谓的交互是这样的,在原理图里点击某个元件,在pcb图中就相应的被选中,这样在元器件刚导进pcb中布局放置元器件的时候可以为我们提供很大的方便. 二.前提: pcb中导入元件是这种方式: ...
- 嵌入式linux启动信息完全注释
嵌入式linux启动信息完全注释 from:http://www.embedlinux.cn/ShowPost.asp?ThreadID=377 摘要 我们在这里讨论的是对嵌入式linux系统的启动过 ...