首先,这个爬虫做了什么:把自己博客首页的文章标题截取下来,保存到一个文本文件中。更改源代码中的host后可以重用到其他采用了iWata主题的wordpress博客。
附上源代码:
#include <boost/asio.hpp> #include <string> #include <iostream> #include <fstream> #include <regex> #include <vector> #include <cstdlib> #define COMPILED_IN_WINDOWS using namespace boost::asio; int main(void) { std::string host("ylinknest.top"); std::string http_port("80"); std::ofstream log_stream("process.log"); io_service ios; ip::tcp::resolver::query one_query(host, http_port, ip::tcp::resolver::query::numeric_service); ip::tcp::resolver resolver(ios); try { //Establish connection std::cout << "Resolving DNS..." << host << '\n'; log_stream << "Resolving DNS..." << host << '\n'; auto endpoint_it = resolver.resolve(one_query); ip::tcp::socket sock(ios); connect(sock, endpoint_it); std::cout << "Resolution result: " + sock.remote_endpoint().address().to_string() + "\n"; log_stream << "Resolution result: " + sock.remote_endpoint().address().to_string() + "\n"; std::cout << "Successfully established connection to " << host + ":" + http_port << '\n'; log_stream << "Successfully established connection to " << host + ":" + http_port << '\n'; //Send an GET request for index page //Problem: If I use "/" in what_target, then the server return 200 // Else, the server return 301. Why and how to handle it? std::string what_target("/"); std::string http_version("HTTP/1.1"); std::string GET_request( "GET " + what_target + " " + http_version + "\r\n" + \ "Host: " + host + "\r\n" + \ "Accept: */*" + "\r\n\r\n"); streambuf http_request; std::ostream request_stream(&http_request); request_stream << GET_request; write(sock, http_request); std::cout << "Posted GET request:\n"; std::cout << GET_request; log_stream << "Posted GET request:\n"; log_stream << GET_request; //Get the feedback streambuf response; std::ofstream ost("get-result.htm"); boost::system::error_code ec; while (read(sock, response, transfer_at_least(512), ec)) { ost << &response; } ost.close(); sock.close(); std::cout << "Socket to " << host + ":" + http_port << " closed.\n"; log_stream << "Socket to " << host + ":" + http_port << " closed.\n"; //Parse //Use this regex: "<h2 class="post-title">.+</h2>" std::ifstream ist("get-result.htm"); std::string buf; std::regex pattern(R"(<h2 class="post-title">.+</h2>)"); std::vector<std::string> fit_result; std::cout << "Pattern matching start.\n"; log_stream << "Pattern matching start.\n"; while (std::getline(ist, buf)) { if (std::regex_search(buf, pattern)) { fit_result.push_back(buf); } } std::cout << "Pattern matching complete.\n"; log_stream << "Pattern matching complete.\n"; //Modify parsing result std::vector<std::string> title_result; for (auto elem : fit_result) { size_t start_index = elem.rfind("title"); size_t end_index = elem.find('>', start_index); std::string sub(elem.substr(start_index, end_index - start_index + 1)); size_t start_quote = sub.find('"'); size_t last_quote = sub.find('"', start_quote + 1); title_result.push_back(sub.substr(start_quote + 1, last_quote - start_quote - 1)); } //Write result to file std::cout << "Write result into files...\n"; log_stream << "Write result into files...\n"; std::ofstream res_stream("match-result.txt"); res_stream << "The parse result of title:\n"; for (const auto& elem : title_result) { std::cout << elem << '\n'; res_stream << elem << '\n'; } std::cout << "Writing done.\n"; log_stream << "Writing done.\n"; res_stream.close(); log_stream.close(); //Problem: Strange control flow shown in txt files #ifdef COMPILED_IN_WINDOWS std::system("pause"); #endif } catch (boost::system::system_error& e) { std::cerr << "Error occured with error code = " << e.code() << ". Message: " << e.what() << '\n'; #ifdef COMPILED_IN_WINDOWS std::system("pause"); #endif return e.code().value(); } return 0; }
代码编译的温馨提示:
- 确认已经调整好编译、链接步骤中与boost库路径相关的设置
- 最高采用C++14标准编译这段代码,更高版本下会对类库报编译错误。
- 非windows环境下,#undef或直接删去代码开头的宏。
下面扯点创作过程:
为什么想拿C++写这玩意呢?我也知道像知乎上那些骗赞的垃圾文章一样,几个类库一凑,可以不到百行,甚至不到50行,把上面这段代码的功能全实现出来。不过那些类库封装了太多更低层的细节,而我把这个爬虫写出来的目的是借这个过程去学习web中计算机之间怎么进行交流。所以直接上标准库去硬x一个,最符合我做这件事的目的。
想写一个爬虫,首先肯定得理清楚,爬虫做了什么?至少先关注爬一个页面上文本的单线程爬虫,就是做了这些事:
- 向server发送获取网页内容的请求
- 接受server发送给client的feedback
- 本地parse这个feedback,获得用户需要的信息,并将其以更有序的形式保存下来
python实现的话,知道这么多就够了。但我们是不满足于用python类库,觉得python的语法很stupid的人。那么还得对上面的步骤再做一些分解。
- “发送获取网页内容的请求”,拆分一下,需要先连接到服务器,再向服务器发送GET请求,说明client想要这个博客的首页文件。
- “连接到服务器”,在用标准库实现功能的前提下,需要先做DNS解析,得到域名对应的IP地址。然后在client和server之间建立socket连接。之后,“连接到服务器”这件事就完成了。
- 连接之后,有点像约着谈事情的朋友见到面,已经互相说过Hello了。这时候肯定得有一方说自己想干什么事。http协议下,GET、POST,以及并不常用的PUT、DELETE等方法,就是规定一方“表达自己想对朋友做些啥( ‵▽′)ψ”的标准模板。这里希望获取到首页的内容,所以需要构造一个GET报文。之后把GET报文通过socket发送出去。
- 接收feedback这步,C++比python稍微繁琐点,不过思路都差不多,把服务器发回来的信息存储到一个文件里去。
- 本地的parse。简短的python爬虫实现里一般会用到beautifulsoup库,能把网页代码看成一棵树,通过有条件地遍历这棵树,得到想要的结果。C++标准库并没有这玩意,不过可以做的事是在目标页面,先F12,凭人去判断一下想获取到的网页元素的源代码的规律。之后可以根据这个规律去构造一个正则表达式,用这个正则表达式在整个网页源代码中去匹配内容,匹配到的结果存到一个容器中,之后对这个容器怎么操作,依据需求而定。
然后是分析一下代码中某些片段:
- asio中,一个socket一定会包含两个endpoint。不过我的代码中并没有出现指定客户端endpoint的情况。因为connect函数使用时,会协调系统,让系统自行安排client一个当前空闲的端口,作为client的endpoint,并用于socket连接。
- 构造GET请求时,一开始是把what_target设为”/index.php”,得到的feedback却是包含301状态码的。用抓包软件查看了一下正常访问(feedback状态码200)的GET报文,是请求”/”资源。自己这样改了一下,GET请求就没问题了。问了一下其他人,说可能与服务器中的入口保护有关系。之后准备翻一下配置文件,看到底为什么无法直接请求到index文件。
- 插入对GET请求构造思路的讲解。一个典型的GET报文有如下结构:
GET [requested_resource] [http_version] [Field_name]: [Field_val] ... [Necessary blank line] [message_body]
对于Field板块,有两个条目挺常用:
Host: [hostname] Accept: [accept_file_type]
Host就是指定域名。Accept就是指定client能识别的文件类型。自己这里用的就是正常的家用电脑,所以设成”*/*”。
请求网页时,不需要通过GET向服务器再提交其他信息,所以空行的消息体置空(反映到具体构造上就是最后有两个空行)
- regex的匹配检测,一开始用的是regex_match方法,发现死活匹配不到对应行……开了变量监视,发现每次读入一整行,不过所有我希望匹配到的行,前面都多了若干tab字符。而regex_match是严格匹配(整个字符串要和正则表达式描述的模式匹配)。改成regex_search解决了这个问题。
- 将文件看成是字节流,编码问题就很容易想明白。正常写入txt时,能保证之后的结果正常。不过在CMD下,标题显示会出现乱码,因为CMD采用GB2312解释进入其中的输入,而博客文字采用了utf8编码,所以会乱码。
自己想到值得写的解释就这么多。有问题可以评论区交流。