1. 程式人生 > >【C++&爬蟲】C++實現網絡爬蟲&socket初級教程

【C++&爬蟲】C++實現網絡爬蟲&socket初級教程

ddr 自己 u2l seq hostname front 命名管道 display blog

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

本註釋講解不足支持,或者想要獲得更加詳細的資料,請訪問以上鏈接。

【C++&爬蟲】C++實現網絡爬蟲&socket初級教程