提示
这是一篇简单的学习记录,没有过多复杂的内容。但是因为是随心记录的,不适合于初学者学习,建议有一定编程基础与计算机知识的人参考。
环境
- Microsoft Visual Stduio 2022 Community
- ISO C++ 20 标准
- vcpkg 包管理器
目标
制作一个简易的下载器,通过调整请求头 Range 来实现分段下载。此程序考查了:
- 输入、输出
- 模块、支持库的调用
- 文件的读写
- if 判断语句
- for while 循环语句
- 字符串拼接
- 命名空间的使用
- 异常的处理
代码
#include <iostream> #include <fstream> #include <string> #include <thread> #include <cpr/cpr.h> static void progress(int per); static std::string strip(const std::string& str); static bool download_file(std::string download_url); // 程序入口点 int main() { std::string download_url; std::cout << "欢迎使用没什么用下载器,请输入下载链接:"; // 不用 std:cin 因为不可以获取空格 std::getline(std::cin, download_url); download_url = strip(download_url); std::cout << "你输入的是:" + download_url + "\n我要开始下载喽亲!" << std::endl; std::cin.sync(); // 清空缓存区 或者使用 std::cin 吃掉换行 // 请求服务器下载 try { // 判断是否为合法的 URL if (download_url.substr(0, 3) != "http") { std::cout << "这不是一个合法的请求。" << std::endl; std::abort(); } download_file(download_url); } catch (const std::exception& e) { std::cout << "(" << e.what() << ")我死了,因为程序运行出现了错误。" << std::endl; } return 0; } // 绘制进度条 static void progress(int per) { std::cout << "\r进度条:|"; for (int i = 0; i < per; i++) { std::cout << "*"; } for (int i = 0; i < 100 - per; i++) { std::cout << " "; } std::cout << "|" << per << "%" << std::flush; } static std::string strip(const std::string& str) { size_t start = str.find_first_not_of(" \t\r\n"); // 找到第一个非空白字符的位置 size_t end = str.find_last_not_of(" \t\r\n"); // 找到最后一个非空白字符的位置 if (start == std::string::npos) // 如果全是空白字符 return ""; // 返回空字符串 return str.substr(start, end - start + 1); // 返回去除首尾空白字符后的子串 } static bool download_file(std::string download_url) { // 取出文件名 std::string filename = download_url.substr(download_url.rfind("/") + 1); // 打开一个文件 std::ofstream file{ filename , std::ios::binary }; if (!file.is_open()) { std::cout << "文件不能创建!取消下载!" << std::endl; return false; } // 输出文件名 std::cout << "文件名为 " << filename << " " << std::endl; // 建立一个会话 cpr::Session session; session.SetUrl( cpr::Url(download_url) ); // 获取文件长度 size_t file_length = session.GetDownloadFileLength(); std::cout << "长度为 " << file_length << " B" << std::endl; // 分段下载文件 size_t response_length = 1024; // 每次请求大小 size_t downloaded_length = 0; // 已经下载的大小 while (downloaded_length < file_length) { session.SetHeader( cpr::Header{ {"Range", "bytes=" + std::to_string(downloaded_length) + "-" + std::to_string(downloaded_length + response_length - 1)} } ); cpr::Response response = session.Get(); // & 代表直接引用 response.header 中的元素,const 设置常量,禁止修改 /*for (const auto& header : response.header) { std::cout << header.first << ":" << header.second << std::endl; }*/ auto data = response.text.data(); auto size = std::stoi(response.header["Content-Length"]); file.write(data, size); downloaded_length += size; progress(static_cast<double> (downloaded_length) / file_length * 100); } return true; }
说明
- 爬虫使用的是 cpr 库,其中的 spr::Response 类中的 text 对象虽然直译为“文本”,但本质就是存在内存的中的一段数据,是 std::string 类型的。使用
response.text.data()
可以获取到对应的指针。通过response.downloaded_bytes
或者response.header["Content-Length"]
可以获取到获取的字节大小,从而实现获取原字节数据。 - for 循环有两种写法,第一种是
for (int i=0;i<100;i++)
对于可遍历的对象可以有for (const auto& item: items)
。其中const
表示定义为常量,不让后续代码进行破坏。&表示的是直接引用items
中的各个项,避免不必要的复制浪费内存。 - 关于数据类型转化,一般可用
(变量类型) 变量
或变量 (变量类型)
进行强制转化,转化过程会产生一个新的变量(即会有一个新的指针地址)。- 在 《C++ Primer Plus 第8版 中文版》第 55 页:C++还引入了4个强制类型转换运算符,对数据类型的转换更为严格。例如
static_cast<>
可将数值类型转化为另一种数据类型。(个人理解:用于数值间的转化。) - 在代码中我使用了
static_cast<double>
对size_t
类型的downloaded_length
进行强制转化,传送到接收int
类型的函数中,会抛出warning C4244: “参数”: 从“double”转换到“int”,可能丢失数据
的警告。我们可以显式的告诉编译器我知道这个风险,progress((int)(static_cast<double> (downloaded_length) / file_length * 100));
同样,你可以掩耳盗铃在前面加上:#pragma warning(disable: 4244)
之后再#pragma warning(default: 4244)
。
- 在 《C++ Primer Plus 第8版 中文版》第 55 页:C++还引入了4个强制类型转换运算符,对数据类型的转换更为严格。例如
- 另外对于初始化数据类型时,使用
{}
更加安全,准确来说在 C++11 以上的标准中才加入进来的,其不允许窄化变量:int a{1.1};
会报错,因为这种转化会丢失浮点位数据。但是使用()
与=
是允许的。只不过会抛出异常。更多内容查看 《C++ Primer Plus 第8版 中文版》第 54 页。 - 程序中引用的
#include <thread>
并未使用,因为只是在测试进度条的时候用来延迟的,忘记删除了,延迟方法std::this_thread::sleep_for(std::chrono::milliseconds(100));
。 - 输入时使用
std::getline(std:cin, 变量)
之后还需要使用std::cin.sync();
或者std::cin.get();
来吃掉换行符,因为std::getline(std:cin, 变量)
不会读取换行符,会将其留在缓存区中。注:std::cin.sync()
是清空缓存区。
不足
- 程序没有判断是否支持断点续传
- 对异常处理不够精确