zoukankan      html  css  js  c++  java
  • 【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
    "
            + "HOST: " + host + "
    "
            + "Connection: close
    
    ";          //设置报文
        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, ''); //初始化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

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

  • 相关阅读:
    arm,iptables: No chain/target/match by that name.
    Windows7-USB-DVD-tool提示不能拷贝文件的处理
    WPF实现WORD 2013墨迹批注功能
    windows下实现屏幕分享(C#)
    Owin WebAPI上传文件
    js 下不同浏览器,new Date转换结果时差
    jquery 动态增加的html元素,初始化设置在id或class上的事件无效
    WPF DataGrid模拟click实现效果
    基于Bootstrap的步骤引导html页面
    XWalkView+html 开发Android应用
  • 原文地址:https://www.cnblogs.com/dudujerry/p/10353876.html
Copyright © 2011-2022 走看看