19、C++项目实战:网络爬虫(HTTP请求)

网络爬虫,说白了就是让程序自动去访问网页,然后把想要的数据抓回来。我刚开始学C++那会儿,总觉得这玩意儿挺神秘——浏览器能看到的页面,代码怎么就能拿到呢?后来自己动手写了一个,才发现核心其实就两件事:发HTTP请求,解析返回的内容。

这一章,我们就用C++来实现一个轻量级的网络爬虫。不依赖第三方库,只用系统自带的socket和标准库,让你真正理解底层是怎么工作的。

爬虫的核心流程

一个最简单的爬虫,其实就三步:

  1. 构造HTTP请求——告诉服务器你要什么
  2. 发送请求并接收响应——通过socket把数据发出去,再读回来
  3. 解析响应内容——从HTML里提取你需要的信息

嗯,听起来是不是很简单?但实际写起来,坑还是挺多的。我曾经在解析HTTP响应头时,因为没处理好chunked编码,导致抓回来的页面全是乱码,排查了半天才发现问题。

网络爬虫核心流程 步骤1:构造HTTP请求 GET /index.html HTTP/1.1 步骤2:发送/接收数据 socket → send → recv 步骤3:解析响应内容 提取标题/链接/数据 ⚠ 常见坑点 • HTTP响应头中的Content-Length和实际数据长度不一致 • 服务器返回chunked编码时,需要按块读取 • 中文网页的编码可能是UTF-8、GBK或GB2312,需要处理

构造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