19、C++项目实战:网络爬虫(HTTP请求)
网络爬虫,说白了就是让程序自动去访问网页,然后把想要的数据抓回来。我刚开始学C++那会儿,总觉得这玩意儿挺神秘——浏览器能看到的页面,代码怎么就能拿到呢?后来自己动手写了一个,才发现核心其实就两件事:发HTTP请求,解析返回的内容。
这一章,我们就用C++来实现一个轻量级的网络爬虫。不依赖第三方库,只用系统自带的socket和标准库,让你真正理解底层是怎么工作的。
爬虫的核心流程
一个最简单的爬虫,其实就三步:
- 构造HTTP请求——告诉服务器你要什么
- 发送请求并接收响应——通过socket把数据发出去,再读回来
- 解析响应内容——从HTML里提取你需要的信息
嗯,听起来是不是很简单?但实际写起来,坑还是挺多的。我曾经在解析HTTP响应头时,因为没处理好chunked编码,导致抓回来的页面全是乱码,排查了半天才发现问题。
构造HTTP请求
HTTP请求的格式其实很固定。你想想看,浏览器访问一个网页时,发出去的就是一段文本。我们手动拼出来就行。
我个人习惯用stringstream来拼接,清晰又不容易出错:
std::string buildHttpGetRequest(const std::string& host,
const std::string& path) {
std::ostringstream oss;
oss << "GET " << path << " HTTP/1.1\r\n"
<< "Host: " << host << "\r\n"
<< "User-Agent: Mozilla/5.0 (C++ Spider)\r\n"
<< "Connection: close\r\n"
<< "\r\n";
return oss.str();
}
💡 小提示: 注意每一行末尾必须是 \r\n,这是HTTP协议规定的。我见过有人只写了 \n,结果服务器根本不搭理。
通过socket发送和接收
这里我们用Linux下的socket API。Windows下大同小异,只是需要先调用WSAStartup。
核心代码其实不长:
std::string httpGet(const std::string& host, const std::string& path) {
// 1. 创建socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
throw std::runtime_error("socket创建失败");
}
// 2. 连接服务器
struct hostent* server = gethostbyname(host.c_str());
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
memcpy(&serv_addr.sin_addr.s_addr, server->h_addr, server->h_length);
serv_addr.sin_port = htons(80);
if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
close(sock);
throw std::runtime_error("连接失败");
}
// 3. 发送HTTP请求
std::string request = buildHttpGetRequest(host, path);
send(sock, request.c_str(), request.length(), 0);
// 4. 接收响应
std::string response;
char buffer[4096];
int bytes;
while ((bytes = recv(sock, buffer, sizeof(buffer) - 1, 0)) > 0) {
buffer[bytes] = '\0';
response += buffer;
}
close(sock);
return response;
}
⚠ 注意: 上面的代码为了简洁,省略了错误处理。实际项目中,每个系统调用都要检查返回值。我曾经因为没检查connect的返回值,在服务器宕机时程序直接崩溃了。
解析HTTP响应
服务器返回的数据,前半部分是响应头,后半部分是正文。两者之间用空行(\r\n\r\n)隔开。
解析的时候,我一般这样处理:
struct HttpResponse {
int statusCode;
std::string statusText;
std::map<std::string, std::string> headers;
std::string body;
};
HttpResponse parseHttpResponse(const std::string& raw) {
HttpResponse resp;
// 找到空行位置
size_t pos = raw.find("\r\n\r\n");
if (pos == std::string::npos) {
throw std::runtime_error("无效的HTTP响应");
}
std::string headerPart = raw.substr(0, pos);
resp.body = raw.substr(pos + 4);
// 解析状态行
std::istringstream headerStream(headerPart);
std::string httpVersion;
headerStream >> httpVersion >> resp.statusCode;
std::getline(headerStream, resp.statusText);
// 解析头部字段
std::string line;
while (std::getline(headerStream, line) && line != "\r") {
size_t colon = line.find(':');
if (colon != std::string::npos) {
std::string key = line.substr(0, colon);
std::string value = line.substr(colon + 2);
// 去掉末尾的\r
if (!value.empty() && value.back() == '\r') {
value.pop_back();
}
resp.headers[key] = value;
}
}
return resp;
}
提取网页中的信息
拿到HTML正文后,怎么提取数据呢?最笨但最可靠的方法就是字符串查找。对于简单的需求,完全够用。
比如提取网页标题:
std::string extractTitle(const std::string& html) {
std::string startTag = "<title>";
std::string endTag = "</title>";
size_t start = html.find(startTag);
if (start == std::string::npos) return "";
start += startTag.length();
size_t end = html.find(endTag, start);
if (end == std::string::npos) return "";
return html.substr(start, end - start);
}
🔑 核心要点:
- HTTP请求本质上是文本协议,手动拼字符串即可
- socket编程要特别注意错误处理,每个系统调用都可能失败
- 解析响应时,先分离头部和正文,再分别处理
- 简单的HTML解析用字符串查找就够了,复杂场景才需要正则或第三方库
完整示例:抓取一个网页
把上面的代码组合起来,就是一个能用的爬虫了:
int main() {
try {
std::string html = httpGet("www.example.com", "/");
HttpResponse resp = parseHttpResponse(html);
std::cout << "状态码: " << resp.statusCode << std::endl;
std::cout << "标题: " << extractTitle(resp.body) << std::endl;
std::cout << "内容长度: " << resp.body.length() << " 字节" << std::endl;
} catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << std::endl;
return 1;
}
return 0;
}
避坑指南
写爬虫时,有几个坑我踩过,分享给你:
- 编码问题:中文网页可能是UTF-8、GBK甚至GB2312。我建议统一转成UTF-8处理,不然提取出来的中文全是乱码。
- 超时处理:网络请求一定要设超时。我曾经没设超时,结果爬到一个慢服务器,程序卡了整整5分钟。
- 遵守robots.txt:虽然技术上可以爬任何页面,但最好遵守网站的爬虫协议。这是基本的网络礼仪。
- 频率控制:别太快,不然你的IP会被封。我一般每次请求之间sleep 1秒。
扩展思路
这个基础爬虫写好后,你可以往几个方向扩展:
- 支持HTTPS:用OpenSSL库,在socket之上加一层SSL加密
- 多线程爬取:用线程池同时爬多个页面,效率翻倍
- 解析链接:从HTML中提取所有<a>标签的href,实现递归爬取
- 保存到数据库:把抓到的数据存到SQLite或MySQL里
嗯,网络爬虫的核心其实就这么点东西。别被那些花里胡哨的框架吓到,底层原理搞懂了,用什么工具都顺手。
公众号:蓝海资料掘金营,微信deep3321