上一篇文章《C++编程入门——HTTP爬虫初步,准备依赖库curl》介绍了如何使用vcpkg安装curl库,本篇文章将在其基础上介绍如何使用curl下载HTTP协议传输的文件。
深交所股票基本概况数据是公开数据,可以直接从深圳证券交易所(简称深交所)的官方网站上找到下载链接,数据下载页的地址是:
股票数据-基本概况https://www.szse.cn/market/stock/indicator/index.html在页面右上角可以找到下载链接,如下图所示:
准备环境
请按照 《C++编程入门——HTTP爬虫初步,准备依赖库curl》的指引准备curl库。
准备工程
使用Visual Studio创建控制台应用程序,工程名填写“StockDataDownloader”,创建成功后如下图所示:
编写代码
把“StockDataDownloader.cpp”的内容改为下面的代码,记得一行一行的手动敲,不要复制粘贴。敲代码的过程其实是在积累经验,经验足够才能“升级”。
#include <fstream>
#include <curl/curl.h>
// 申明用于下载数据的函数
bool downloadData(const char* url, const char* outputFilePath);
int main()
{
const char* url = "https://www.szse.cn/api/report/ShowReport?SHOWTYPE=xlsx&CATALOGID=1803_after&TABKEY=tab1&txtQueryDate=2025-04-21&random=0.1151430633896432";
const char* outputFilePath = "D:\\StockDatasSZ.xlsx";
if (downloadData(url, outputFilePath))
{
printf("下载成功,文件已保存在“%s”。\n", outputFilePath);
return 0;
}
else
{
printf("下载失败!\n");
return -1;
}
}
// 回调函数,由curl自动调用,用于将下载的数据写入文件
size_t onDataArrived(void* contents, size_t size, size_t nmemb, void* userp)
{
// 把第72行传递过来的参数转换成正确的类型
std::ofstream* outputFile = (std::ofstream*)userp;
if (outputFile == nullptr)
{
return 0;
}
// 向文件写入数据
outputFile->write((const char*)contents, size * nmemb);
// 检查是否写入失败
if (outputFile->fail())
{
// 失败了返回0,curl会停止数据传输
return 0;
}
// 成功了返回已写入的字节数,curl会继续传输
return size * nmemb;
}
// 实现用于下载数据的函数
bool downloadData(const char* url, const char* outputFilePath)
{
// 打开磁盘文件,收到数据时好直接写入文件
std::ofstream outputFile(outputFilePath, std::ios::binary);
if (outputFile.is_open() == false)
{
printf("打开文件“%s”失败。\n", outputFilePath);
return false;
}
// 初始化curl
curl_global_init(CURL_GLOBAL_DEFAULT);
// 创建一个HTTP请求
CURL* request = curl_easy_init();
if (request == nullptr)
{
printf("初始化curl失败。\n");
curl_global_cleanup();
return false;
}
// 设置HTTP请求的URL
curl_easy_setopt(request, CURLOPT_URL, url);
// 设置接收数据的回调函数,收到数据时curl会自动调用该函数
curl_easy_setopt(request, CURLOPT_WRITEFUNCTION, onDataArrived);
// 设置传递给回调函数的参数
curl_easy_setopt(request, CURLOPT_WRITEDATA, &outputFile);
// 执行请求
CURLcode res = curl_easy_perform(request);
if (res != CURLE_OK)
{
printf("下载“%s”失败,错误信息:%s\n",
url,
curl_easy_strerror(res));
curl_easy_cleanup(request);
curl_global_cleanup();
return false;
}
// 清理,释放curl内部占用的内存
curl_easy_cleanup(request);
curl_global_cleanup();
return true;
}
运行程序
按“F5”或通过菜单“调试->开始调试”启动程序,在控制台中输出生成是否成功的提示,如下图:
如果下载成功,在D:盘中会保存有一个名为“StockDatasSZ.xlsx”的文件,它是xslx类型的文件,用Excel或者WPS可以打开。用Excel打开后的效果类似下图,软件版本不同,外观可能存在些许差异,但数据应该是一样的。
涨知识
main函数返回值
main函数是有返回值的,返回值的类型是int。main函数的返回值是返回给操作系统的,通常约定成功返回0,失败返回非0,非0值通常被认为是错误代码。
回调函数
代码第25行实现了回调函数onDataArrived,并且在70行通过curl_easy_setopt传递给了curl。回调函数的意义就在于当curl收到数据的时候,它可以调用我们实现的函数,来接收数据。为了让curl知道我们的回调函数,必须通过调用curl_easy_setopt把回调函数的地址传递给curl。
void*类型
代码第25行onDataArrived函数的最后一个参数“userp”是void*类型,“void*”是void类型的指针。在C++中所有的指针都保存的是内存地址,因此void*中也保存的是内存地址,用void表示这块内存中的数据类型是“未知”的,这时候程序员应该知道这块内存中保存的是什么类型的数据。
在本例中,userp参数的值是代码第72行通过curl_easy_setopt传递给onDataArrived的,因此它的类型是“std::ofstream*”。所以在代码第28行,我们可以安全的把userp强制转换成std::ofstream*类型。
强制类型转换不能乱用,只能用来把指针转换成它“本来”的类型,比如下面的用法就是错误的,会造成程序崩溃:
const char* str = "abc";
// 错误的转换
int* num = (int*)str;
*num = 999;
在实际工作中,void*通常用来传递任意类型的指针,因为指针本质上是内存地址,所以通过void*传递的实际上是内存地址。需要操作数据时,再把void*传递的内存地址转换成它本来的类型进行操作。
既然所有指针都是内存地址那么为什么不用int*、char*来传递内存地址呢?这是因为void和int、char这些类型不一样,void不能被直接操作,不能赋值,也不能取值。用void*传递内存地址表示你不能直接操作这块内存,得先弄清楚这块内存里面是什么类型的数据,才能进行操作。这样就能避免用错误的类型操作指针,造成程序崩溃。