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初级教程的更多相关文章

  1. Python 网络爬虫 001 (科普) 网络爬虫简介

    Python 网络爬虫 001 (科普) 网络爬虫简介 1. 网络爬虫是干什么的 我举几个生活中的例子: 例子一: 我平时会将 学到的知识 和 积累的经验 写成博客发送到CSDN博客网站上,那么对于我 ...

  2. python网络爬虫之初始网络爬虫

    第一次接触到python是一个很偶然的因素,由于经常在网上看连载小说,很多小说都是上几百的连载.因此想到能不能自己做一个工具自动下载这些小说,然后copy到电脑或者手机上,这样在没有网络或者网络信号不 ...

  3. Python爬虫《Python网络爬虫相关基础概念》

    引入 之前在授课过程中,好多同学都问过我这样的一个问题:为什么要学习爬虫,学习爬虫能够为我们以后的发展带来那些好处?其实学习爬虫的原因和为我们以后发展带来的好处都是显而易见的,无论是从实际的应用还是从 ...

  4. python网络爬虫之初识网络爬虫

    第一次接触到python是一个很偶然的因素,由于经常在网上看连载小说,很多小说都是上几百的连载.因此想到能不能自己做一个工具自动下载这些小说,然后copy到电脑或者手机上,这样在没有网络或者网络信号不 ...

  5. PYTHON网络爬虫与信息提取[网络爬虫协议](单元二)

    robots.txt在网站的根目录下 遵守 自动或人工识别robots.txt再进行内容爬取 约束性:建议性,不遵守协议,存在法律风险. 基本语法: User-agent: * Disallow: / ...

  6. Python网络爬虫

    http://blog.csdn.net/pi9nc/article/details/9734437 一.网络爬虫的定义 网络爬虫,即Web Spider,是一个很形象的名字. 把互联网比喻成一个蜘蛛 ...

  7. 【Python开发】【神经网络与深度学习】网络爬虫之python实现

    一.网络爬虫的定义 网络爬虫,即Web Spider,是一个很形象的名字. 把互联网比喻成一个蜘蛛网,那么Spider就是在网上爬来爬去的蜘蛛. 网络蜘蛛是通过网页的链接地址来寻找网页的. 从网站某一 ...

  8. [Python]网络爬虫(一):抓取网页的含义和URL基本构成

    一.网络爬虫的定义 网络爬虫,即Web Spider,是一个很形象的名字. 把互联网比喻成一个蜘蛛网,那么Spider就是在网上爬来爬去的蜘蛛.网络蜘蛛是通过网页的链接地址来寻找网页的. 从网站某一个 ...

  9. larbin是一种开源的网络爬虫/网络蜘

    larbin是一种开源的网络爬虫/网络蜘蛛,由法国的年轻人 Sébastien Ailleret独立开发.larbin目的是能够跟踪页面的url进行扩展的抓取,最后为搜索引擎提供广泛的数据来源.Lar ...

随机推荐

  1. 阿里云 Django部署参考

    Linux下安装Python3和django并配置mysql作为django默认服务器 CentOS7.3安装Python3.6 yum except KeyboardInterrupt, e: 错误 ...

  2. android ViewPager实现的轮播图广告自定义视图,网络获取图片和数据

    public class SlideShowAdView extends FrameLayout { //轮播图图片数量    private static int IMAGE_COUNT = 3;  ...

  3. getHiddenProp() 浏览器状态切换改变

    <script> function getHiddenProp() { var prefixes = ['webkit', 'moz', 'ms', 'o']; // if 'hidden ...

  4. cc.AudioSource

    cc.AudioSource1:AudioSource组件是音频源组件, 发出声音的源头2: AudioSource组件面板: clip: 声源的播放的音频对象: AudioClip, mp3, wa ...

  5. 题解 洛谷P1903/BZOJ2120【[国家集训队]数颜色 / 维护队列】

    对于不会树套树.主席树的本蒟蒻,还是老老实实的用莫队做吧.... 其实这题跟普通莫队差不了多远,无非就是有了一个时间,当我们按正常流程排完序后,按照基本的莫队来,做莫队时每次循环对于这一次操作,我们在 ...

  6. Python学习笔记(2)——Python的函数、模块、包和库

    Table of Contents 1. 函数. 2. 模块. 3. 包(package) 4. 库(library) 初识Python,对于没有接触过编程的我,恐怕只能听懂什么是函数,这里介绍一下几 ...

  7. centOS取消锁屏

    自己在使用虚拟机运行centos 7时,centos 7默认几分钟不动就锁屏,实在很讨厌,所以在设置中将其去掉 1.左上角点击应用程序,在下面选择系统工具,在系统工具中选择设置 2.选择设置下面的隐私 ...

  8. odoo 权限配置讲解

    今天来讲解一下odoo权限配置的简单讲解,配合公司开发的权限模块的使用,进行odoo权限配置的说明 BaseSecurityExtend 2.0模块 这是公司自主开发的一款针对odoo菜单级别进行可视 ...

  9. allegro中原理图和pcb中元件的交互

    一.前言: 所谓的交互是这样的,在原理图里点击某个元件,在pcb图中就相应的被选中,这样在元器件刚导进pcb中布局放置元器件的时候可以为我们提供很大的方便. 二.前提: pcb中导入元件是这种方式: ...

  10. 嵌入式linux启动信息完全注释

    嵌入式linux启动信息完全注释 from:http://www.embedlinux.cn/ShowPost.asp?ThreadID=377 摘要 我们在这里讨论的是对嵌入式linux系统的启动过 ...