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手写libcurlHTTPS证书验证需要自己接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.crtcurl_easy_setopt(curl, CURLOPT_CAINFO, ca-bundle.crt);不要在代码里硬编码绝对路径因为别人的机器上目录不一样。用相对路径“ca-bundle.crt”同时让exe的工作目录和证书文件保持一致。如果你只是自己临时测试可以用-k参数关闭验证但这条绝不能用在正式环境。5.5 现象程序长时间运行内存只涨不降原因回调函数里每次realloc分配的内存没有在请求结束后free或者循环抓取时每次都新建curl句柄却只清理了最后一个。解决一个进程里只应有一个RespBuf每次请求完成后立刻free(resp.data)并重置size0。使用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写爬虫拐点不在“能不能抓”而在“断了怎么恢复、错了怎么报、分发出去怎么不让人返工”。把这三件事处理好比追新框架有用得多。希望帮到你。本文还有配套的精品资源点击获取