news 2026/10/13 14:33:59

C语言网页爬虫实战:Windows下libcurl静态编译与断点续传

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言网页爬虫实战:Windows下libcurl静态编译与断点续传

简介:一套用C语言结合curl库实现的网页爬虫项目,面向想从底层理解爬虫工作原理、锻炼C语言网络编程能力的开发者。资源包共5个文件,包含spider.c核心源码、配套Makefile构建脚本、curl.exe命令行工具以及HTTPS所需的ca-bundle.crt证书,并附有已编译可直接运行的spider.exe,整体压缩包仅1.08MB。目前已有2208人学习。项目中spider.c示范了如何通过curl API发起HTTP GET请求、接收服务器返回的HTML数据,并在此基础上解析链接与提取文本、图片等目标信息;ca-bundle.crt则解释了HTTPS通信中服务器身份验证与中间人攻击防范的细节。读者既能直接运行spider.exe观察抓取效果,也可借助curl.exe对照测试,还能在Windows下借助Makefile重新编译,完整走通从源码到可执行文件的流程。这份麻雀虽小五脏俱全的示例,是入门C语言网络编程与爬虫机制的理想实践素材。

1. 用C语言写网页爬虫,还带着编译好的Windows版,到底图什么

“C语言网页爬虫”听起来像是上个时代的组合,但实际需求一直存在:内网巡检脚本、老设备上的采集程序、不允许装解释器的生产环境,这些场景跑不了Python,唯一的通用运行环境就是Windows命令提示符。标题里特意写了“附编译好的windows版”,说明作者已经替你把环境路趟平了——你不需要装任何开发工具,拿到exe就能用。这个方案适合两类人:一类是一线运维,想在不装额外运行时的情况下抓网页做监控;另一类是学C的学生,想看看HTTP协议和socket在真实代码里长什么样,而不是只在课本上读概念。本文就把这条路从原理到编译、从参数到踩坑完整走一遍,最后教你把这个exe改造成断点续传下载器。

2. 先看懂C语言爬虫的本质:一个HTTP请求在底层做了什么

2.1 最原始的HTTP请求:socket连接、发送GET、读响应

抛开所有封装库,爬虫最核心的动作其实就是三件事:建立TCP连接、按HTTP协议格式发一段文本、把返回的字节流读下来。下面这段代码用winsock实现了一个最简demo,它只能抓明文HTTP页面,但能让你看清协议本质:

#include <stdio.h> #include <string.h> #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") int main(void) { WSADATA wsa; SOCKET s; struct sockaddr_in addr; struct hostent *host; char req[512], buf[8192]; int bytes; // 1. 初始化Windows socket库 WSAStartup(MAKEWORD(2, 2), &wsa); s = socket(AF_INET, SOCK_STREAM, 0); // 2. 域名解析:把主机名转成IP host = gethostbyname("example.com"); addr.sin_family = AF_INET; addr.sin_port = htons(80); memcpy(&addr.sin_addr, host->h_addr, host->h_length); // 3. 建立TCP连接 connect(s, (struct sockaddr *)&addr, sizeof(addr)); // 4. 发送HTTP请求,注意每行必须以\r\n结尾 snprintf(req, sizeof(req), "GET /index.html HTTP/1.1\r\n" "Host: example.com\r\n" "User-Agent: crawler/1.0\r\n" "Connection: close\r\n" "\r\n"); send(s, req, (int)strlen(req), 0); // 5. 循环接收响应,直到对端关闭连接 while ((bytes = recv(s, buf, sizeof(buf) - 1, 0)) > 0) { buf[bytes] = '\0'; printf("%s", buf); } closesocket(s); WSACleanup(); return 0; }

逻辑不难,但注意几个参数细节:htons(80)是把端口号转成网络字节序,漏了它端口会错乱;Connection: close告诉服务器收完响应就断开,这样recv读到0时就能停止循环;buf是固定数组,如果响应体超过8KB,这里就会截断。真正的爬虫没人用固定缓冲,后面会讲动态内存方案。

这个demo能跑通,但只限于HTTP明文站点。一旦目标是HTTPS,就得自己接SSL/TLS库;服务器返回Transfer-Encoding: chunked时,还得自己拆块;遇到301跳转,要重新解析Location再发一次请求。这些活单独处理,每一件都是深坑。

2.2 为什么生产环境选libcurl:把黑匣子交给C库

我见过不少新手坚持纯socket写爬虫,最后都在HTTPS证书和chunked编码上翻车。真正做事的方案是用libcurl,它是C语言生态里最成熟的HTTP客户端库,把SSL握手、重定向、超时、压缩这些底层细节全部封装好了。两者对比如下:

功能项纯socket手写libcurl
HTTPS证书验证需要自己接openssl内置,设置CA路径即可
301/302重定向自己解析Location并重发FOLLOWLOCATION一键开启
超时控制用select+计时器手搓TIMEOUT和CONNECTTIMEOUT两个选项
chunked分块编码自己拼块解析自动处理
gzip压缩自己接zlib解压设置ACCEPT_ENCODING即可
断点续传自己发Range头RESUME_FROM_LARGE一个选项

如果你的目标是写一个“能跑、能排查问题”的爬虫,libcurl能省掉至少一周的踩坑时间。代码里只需要调用几个curl_easy_setopt接口,HTTP细节从黑匣子变成“可信赖的库”,这是性价比最高的选择。

2.3 C语言处理响应体的正确姿势:动态内存增长

C爬虫最容易被骂“难写”的地方就是字符串处理。响应体长度未知,固定数组会截断,malloc后又要手动管理释放。我一般用结构体加回调函数解决,这也是libcurl官方示例的标准做法:

typedef struct { char *data; // 动态增长的内存块 size_t size; // 已使用字节数 } MemBuf; static size_t write_cb(void *ptr, size_t size, size_t nmemb, void *userdata) { size_t len = size * nmemb; MemBuf *buf = (MemBuf *)userdata; // 用realloc扩展空间,而不是直接越界写 char *tmp = realloc(buf->data, buf->size + len + 1); if (!tmp) return 0; // 内存不足,返回0会让libcurl认为传输失败 buf->data = tmp; memcpy(buf->data + buf->size, ptr, len); buf->size += len; buf->data[buf->size] = '\0'; return len; }

回调函数是libcurl的“水管”:每收到一块数据就调用一次,把数据灌进内存块。注意realloc的返回值必须赋给临时指针再回写,直接realloc(buf->data)失败时会丢指针。这个方法的关键价值是:无论响应体是1KB还是100MB,内存都能自动匹配,且末尾补'\0'让数据能当字符串用。

3. 在Windows下编译出可用的exe:从MinGW到静态链接

3.1 准备工具链:MinGW-w64和libcurl开发包

要编译出Windows版的exe,你需要一套能在Windows命令行工作的GCC工具链。我用的是MinGW-w64,选线程模型为posix的版本,这样在代码里用C标准线程API时不会报错。libcurl开发包可以用编译好的二进制包,记得把include和lib两个目录拷到工程目录下,方便后续静态链接。目录结构我习惯这样安排:

crawler/ ├── src/crawler.c ├── libcurl/ │ ├── include/curl/curl.h │ └── lib/libcurl.a

如果不喜欢手动整理开发包,也可以用包管理器一键安装依赖,但注意安装完后用gcc --version确认编译器版本,用where curl-config确认libcurl的开发配置脚本是否在PATH里,这决定后面的编译命令能不能一条跑通。

3.2 完整可编译的爬虫源码与静态链接命令

核心代码如下,抓取指定URL并把响应体保存到文件,支持HTTPS、重定向、超时和自定义UA:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <curl/curl.h> // 响应数据头 typedef struct { char *data; size_t size; } RespBuf; static size_t write_cb(void *ptr, size_t size, size_t nmemb, void *userdata) { size_t len = size * nmemb; RespBuf *buf = (RespBuf *)userdata; char *tmp = realloc(buf->data, buf->size + len + 1); if (!tmp) return 0; buf->data = tmp; memcpy(buf->data + buf->size, ptr, len); buf->size += len; buf->data[buf->size] = '\0'; return len; } int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "用法: crawler.exe <url> [输出文件]\n"); return 1; } const char *url = argv[1]; const char *outfile = (argc >= 3) ? argv[2] : "index.html"; CURL *curl = curl_easy_init(); if (!curl) return 1; RespBuf resp = { NULL, 0 }; FILE *fp = fopen(outfile, "wb"); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 crawler/1.0"); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); // 自动跟随重定向 curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); // 整体超时30秒 curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); // 连接超时10秒 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); // 验证证书 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp); CURLcode res = curl_easy_perform(curl); if (res == CURLE_OK) { fwrite(resp.data, 1, resp.size, fp); long http_code = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); printf("HTTP %ld, 保存 %zu 字节\n", http_code, resp.size); } else { fprintf(stderr, "抓取失败: %s\n", curl_easy_strerror(res)); } free(resp.data); fclose(fp); curl_easy_cleanup(curl); curl_global_cleanup(); return (res == CURLE_OK) ? 0 : 1; }

代码里有几个值得注意的参数:FOLLOWLOCATION设为1L后,libcurl会自动处理302跳转,最多默认跳5次;TIMEOUT是整套请求的总时限,不只是连接时间,防止个别慢响应无限拖住进程;SSL_VERIFYPEER保持开启,生产环境不要为了图省事关掉证书验证。curl_easy_getinfo用于取回HTTP状态码,这是判断抓取是否命中的关键指标。

编译命令分两种场景。开发调试时用动态链接,快:

gcc src/crawler.c -o crawler.exe \ -I./libcurl/include -L./libcurl/lib -lcurl -lssl -lcrypto

发布给别人的时候,一定要用静态链接,否则对方机子上会报“缺少libcurl-4.dll”:

gcc src/crawler.c -o crawler.exe \ -I./libcurl/include -L./libcurl/lib \ -lcurl -lssl -lcrypto -lz -static -O2

-static会把libcurl、openssl、zlib全部打进exe,生成的文件体积会从几十KB涨到几MB,但换来的是在干净Windows系统上直接运行。-O2是优化选项,对字符串处理和循环有明显加速。如果你希望生成更精简的文件,可以把编译目标换成-O1 -s,其中-s用于剥离符号表。

3.3 发布文件清单:不要只丢一个exe

编译出来的exe只是主程序,发布时我一般配齐这些文件,避免最终用户在证书验证上卡住:

文件作用
crawler.exe主程序,静态链接后无DLL依赖
ca-bundle.crtCA根证书包,HTTPS验证必需
config.ini可选,存默认UA、超时时间等

CA证书包可以从curl官方发布目录里找,或者从你机器上的openssl证书目录提取一份。注意exe的搜索路径:如果你用了CURLOPT_CAINFO,证书路径要写成相对路径或绝对路径,否则运行时找不到。也可以让exe在启动时把curl/build目录下的ca-bundle.crt自动作为默认证书,这样即便用户只拷贝exe和证书两个文件也能顺利跑通HTTPS站点。

4. 用编译好的crawler.exe跑起来:命令参数与输出设计

4.1 命令行参数的约定

我写爬虫工具的习惯是:参数少而清晰,让它在批处理和手工测试之间都能用。下面是这个exe支持的参数表,你可以按照自己需求裁剪:

参数含义默认值
-u <url>目标网址无
-o <file>输出文件index.html
-t <sec>整体超时秒数30
-k允许不验证证书(仅测试用)关闭
-v显示HTTP状态码和响应字节数关闭

命令行解析直接用strcmp配argc判断,不需要引入getopt库,Windows下getopt本身也是个坑。示例用法:

crawler.exe -u https://example.com/api -o out.json -t 15

执行后输出两行信息,一行抓取状态,一行保存结果:

HTTP 200, content-type: application/json 保存 18432 字节到 out.json

这里content-type来自响应头,能帮你快速判断目标返回的是HTML还是JSON,以下解析逻辑经常依赖这个字段。

4.2 让返回码成为脚本的“哨兵”

爬虫跑在批处理里,最怕的就是静默失败。我用返回码做约定:0表示成功抓取且保存完整,1表示网络错误,2表示证书错误,3表示超时。Windows批处理里直接判断:

@echo off crawler.exe -u https://example.com/data.json -o data.json if %errorlevel% equ 0 ( echo 抓取成功 ) else ( echo 抓取失败,错误码 %errorlevel% )

这个设计的意义是:不需要人工盯着屏幕看输出,脚本根据返回码决定是否重试、是否报警。比如超时(码3)可以先sleep 30秒再重试,证书错误(码2)则说明环境配置有问题,重试没意义。

4.3 批量抓取:在批处理里配合使用

日常巡检经常要抓多个URL,可以在批处理里写循环:

@echo off setlocal enabledelayedexpansion for %%u in (url1 url2 url3 url4) do ( echo [%%u] 开始抓取 crawler.exe -u %%u -o %%u.html -t 10 if !errorlevel! neq 0 ( echo %%u 抓取失败,状态码 !errorlevel! ) )

注意setlocal enabledelayedexpansion这个前置语句:批处理中用!errorlevel!获取循环内动态值,如果写死%errorlevel%,它会在循环开始前被解析,导致每次都读到同一值。-t 10设短超时,避免某个卡住的URL拖完全场。这个模式做小规模巡检够用了。

5. C语言爬虫避坑指南:从编译到跑通的8条血泪经验

5.1 现象:双击exe闪退,或者提示缺DLL

原因:控制台程序双击会瞬间打开又关闭;而“缺少libcurl-4.dll”说明用了动态链接,发布时没带上运行库。

解决:闪退的在cmd里直接运行看报错信息,这是最基础的检查。缺DLL的回到第3章,用-static重新编译,生成后可以右键查看exe版本信息里的依赖项,确保没有红色标记的DLL依赖。

5.2 现象:抓下来的页面中文全是乱码

原因:目标站点返回的是GBK编码,而Windows控制台默认代码页是936,如果程序直接把GBK字节写到文件后你用UTF-8编辑器打开,自然乱码;反过来如果站点是UTF-8,控制台用GBK显示也会乱。

解决:明确“抓取是保真字节流,显示是另一回事”。你只需要原样保存响应体,文件名和内容编码不混为一谈;需要转码时用iconv或者Windows的MultiByteToWideChar,在输出前统一替换成UTF-8。

5.3 现象:浏览器能打开,爬虫返回403

原因:服务器识别出请求头里没有浏览器特征,把默认的curl/x.xUA拉黑了。

解决:在CURLOPT_USERAGENT里伪装成常见浏览器UA,同时补上Accept和Accept-Language头。真正严格的站点会检查Referer,你可以在CURLOPT_REFERER里带上目标站点首页。但别把伪装当万能钥匙,碰上有JS动态渲染的页面,爬虫看到的只是空壳骨架。

5.4 现象:HTTPS站点报错60或证书验证失败

原因:编译时SSL_VERIFYPEER开了,但没有指定CA证书路径,libcurl找不到可信的根证书。

解决:设置CURLOPT_CAINFO指向发布目录里的ca-bundle.crt:

curl_easy_setopt(curl, CURLOPT_CAINFO, "ca-bundle.crt");

不要在代码里硬编码绝对路径,因为别人的机器上目录不一样。用相对路径“ca-bundle.crt”,同时让exe的工作目录和证书文件保持一致。如果你只是自己临时测试,可以用-k参数关闭验证,但这条绝不能用在正式环境。

5.5 现象:程序长时间运行,内存只涨不降

原因:回调函数里每次realloc分配的内存,没有在请求结束后free;或者循环抓取时每次都新建curl句柄却只清理了最后一个。

解决:一个进程里只应有一个RespBuf,每次请求完成后立刻free(resp.data)并重置size=0。使用curl_easy_perform的复用模式,别每次抓取都调用curl_easy_init/curl_easy_cleanup,而是把句柄放在循环外面,用CURLOPT_URL改目标再复用。

5.6 现象:大文件只抓到一部分就不动了

原因:回调返回的字节数不等于实际接收字节数,或者响应是chunked时libcurl没有正常工作,也可能是你提前在回调里返回了0。

解决:回调函数里return len;必须等于传入的字节数,返回0会被libcurl按“传输失败”处理。另外确认CURLOPT_TIMEOUT是否设得太短,大文件下载需要更长超时。用CURLOPT_NOSIGNAL防DNS超时中断也是一个收敛项。

5.7 现象:重定向后内容不对,抓到的明明是登录页

原因:地址栏里的URL会跳转,但爬虫默认没有跟随重定向,或者重定向时丢了POST数据和Referer。

解决:开启FOLLOWLOCATION可以跟踪到最终地址;如果服务端302跳转后需要验证来源,还需在跳转后手动更新Referer。用抓包工具看一下最终请求头,确认Cookie的携带情况。

5.8 现象:编译器报错找不到curl.h或libcurl

原因:头文件和库文件路径没写对,或位数不匹配:64位GCC配了32位的libcurl静态库。

解决:先确认curl.h是否真的在你指定的include下;再看库目录下libcurl.a的架构,可以用file libcurl.a查看,必须和gcc -v显示的target一致。工程里同时存在多个版本的dev包时,这种问题最常见,建议只保留一套。

6. 进阶技巧:把爬虫改成断点续传下载器,并验证编译产物

爬虫和下载器只有一步之遥。前面第4章里-o参数把响应体落盘,但遇到大文件,中途断网就前功尽弃。给exe加一个断点续传能力,用libcurl一个选项就能实现:

// 先获取本地已有文件大小 long resume_pos = 0; FILE *fp = fopen(outfile, "rb"); if (fp) { fseek(fp, 0, SEEK_END); resume_pos = ftell(fp); fclose(fp); } // 告诉libcurl从指定偏移继续下载 curl_easy_setopt(curl, CURLOPT_RESUME_FROM_LARGE, (curl_off_t)resume_pos); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0L); // 大文件不限总时长

运行时第一次是正常下载,第二次中断后重新执行同一命令,它会自动发Range: bytes=<已有大小>-请求,服务器从断点继续发数据,程序以追加模式写文件。验证方法很直接:先下载一半时手动杀掉进程,再跑一遍,对比文件大小和源的Content-Length是否一致,或者用哈希工具比对。我给自己写的下载器定了一条规矩:任何目标URL都必须带超时,否则进程会无限制挂住;断点续传则是给那些“必须把文件拖下来”的场景准备的,两者不冲突。

这个技巧和前面整个方案合起来,就是一个能扔到任何Windows机器上直接跑的采集小工具。我自己平时维护巡检脚本,最深的感受是:C写爬虫拐点不在“能不能抓”,而在“断了怎么恢复、错了怎么报、分发出去怎么不让人返工”。把这三件事处理好,比追新框架有用得多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 14:33:29

Sage采购模块实战:从请购到三单匹配的完整配置与避坑指南

简介&#xff1a;这份SAGE-ERP采购模块操作手册面向企业采购、财务及ERP实施人员&#xff0c;系统梳理了从基础数据维护到采购退货的完整业务链路&#xff0c;帮助使用者快速掌握系统操作要点、规范日常采购流程。内容覆盖产品浓度管理设置、供应商信息建档、供应商报价审核、折…

作者头像 李华
网站建设 2026/10/13 14:32:03

蓝桥杯单片机四合一实战:DS18B20、超声波、光敏、红外多传感器融合

简介&#xff1a;这是一份面向蓝桥杯单片机竞赛选手的综合实训资源&#xff0c;聚焦温度显示、超声波测距、光敏D/A显示与红外接收数据显示四大模块&#xff0c;并融入矩阵键盘按键切换功能&#xff0c;适合正在备赛或希望提升传感器与显示接口开发能力的学习者。资源为zip压缩…

作者头像 李华
网站建设 2026/10/13 14:32:03

鸿蒙6应用开发实战:ArkTS、状态管理与工程化要点

最近把几个内部项目陆续迁到鸿蒙6上跑真机&#xff0c;过程里踩了不少坑&#xff0c;也顺手纠正了几个网上旧资料里已经过时的说法。好多从安卓或者前端转过来的朋友问我要经验&#xff0c;这篇就统一讲一讲。鸿蒙6不是一次小版本升级&#xff0c;它对开发模型、编译链路、状态…

作者头像 李华
网站建设 2026/10/13 14:31:18

AdminLTE 3.1.0-rc实战指南:后台模板解压、定制与避坑

简介&#xff1a;AdminLTE-3.1.0-rc 是一款基于 Bootstrap 的响应式后台管理前端框架&#xff0c;主要面向需要快速搭建 Dashboard 或企业级管理界面的 Web 开发者。它提供导航栏、卡片、表格、表单、模态框等丰富组件&#xff0c;并支持多种配色与深色模式&#xff0c;适合从初…

作者头像 李华
网站建设 2026/10/13 14:30:05

npx命令执行原理与CLI工具选型实战指南

1. 项目概述&#xff1a;一个被误读的CLI工具命名现象“impeccable”这个词最近在开发者社区里频繁冒头&#xff0c;但几乎没人能说清它到底指什么。你搜“impeccable 如何使用”&#xff0c;跳出来的全是零散的报错截图、npm install失败日志&#xff0c;还有人把“impeccable…

作者头像 李华