【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(2, 0), &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(0, 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[0]) : ""; //返回得到的点分十进制IP地址,如果转换失败返回""
    //inet_ntoa:将一个32位网络字节序的二进制IP地址转换成相应的点分十进制的IP地址
}

inline SOCKET connect(const string &hostName)   //
{
    auto ip = getIpByHostName(hostName);        //获得host(IP) (上函数)
    if (ip.empty())
        return 0;
    auto sock = socket(AF_INET, SOCK_STREAM, 0);
    //参数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 0;
    //INVALID_SOCKET:该返回值代表创建套接字错误
    SOCKADDR_IN addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(80);                
    addr.sin_addr.s_addr = inet_addr(ip.c_str());
    if (connect(sock, (const sockaddr *)&addr, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)
        return 0;
    //参数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[0], http.size(), 0);   //发送请求
    //参数1:socket,之前创建的套接字
    //参数2:要发送的数据
    //参数3:数据大小
    //参数4:调用执行方式,默认写0即可

}

inline string recvRequest(SOCKET sock)
{
    static timeval wait = { 2, 0 };
    static auto buffer = string(2048 * 100, '\0'); //初始化string容量
    auto len = 0, reclen = 0;
    do {
        fd_set fd = { 0 };
        //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 = 0;
        if (select(0, &fd, nullptr, nullptr, &wait) > 0)
        {
            reclen = recv(sock, &buffer[0] + len, 2048 * 100 - len, 0);
            if (reclen > 0)
                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 > 0);

    return len > 11
        ? buffer[9] == '2' && buffer[10] == '0' && buffer[11] == '0'
        ? buffer.substr(0, 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[0].str(),
            regex("href=\"(https?:)?//(\\S+)\""),
            "$2"));
        curIter = result[0].second;
    }
}

void Go(const string &url, int count)   //BFS
{
    queue<string> urls;
    urls.push(url);
    for (auto i = 0; 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", 200);  //从www.hao123.com开始,计数200次
    cleanupWSA();               //WSA释放
    return 0;
}
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

本注释讲解不足支持,或者想要获得更加详细的资料,请访问以上链接。

猜你喜欢

转载自www.cnblogs.com/dudujerry/p/10353876.html