首先,这个爬虫做了什么:把自己博客首页的文章标题截取下来,保存到一个文本文件中。更改源代码中的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编码,所以会乱码。

自己想到值得写的解释就这么多。有问题可以评论区交流。