公司动态

C++实战:从零构建命令行天气查询工具

📅 2026/7/22 4:47:04
C++实战:从零构建命令行天气查询工具
1. 项目概述从零构建一个实用的C天气查询工具最近在整理自己的代码库翻出来一个几年前写的C命令行天气查询工具。当时写它纯粹是为了解决一个很实际的需求在Linux终端下快速看一眼天气不用打开浏览器也不用依赖那些臃肿的桌面应用。这个项目麻雀虽小五脏俱全涉及网络请求、JSON解析、字符串处理、跨平台编译等C实战中常见的“硬骨头”。今天我就把这个项目的源码和实现思路完整地拆解一遍无论你是刚学完C基础想找个练手项目还是想了解如何用C处理网络API这篇文章都能给你一份可以直接“抄作业”的参考。这个工具的核心功能很简单输入一个城市名比如“Beijing”或“上海”它就能从公开的天气API获取并解析数据然后在终端里清晰地展示温度、天气状况、湿度、风速等关键信息。整个过程完全在命令行完成轻量、高效。实现它你需要掌握几个关键点如何使用C发起HTTP请求、如何处理返回的JSON格式数据、如何设计一个结构清晰且易于维护的程序结构。下面我们就一步步来拆解。2. 核心思路与技术选型为什么这么设计在动手写代码之前先想清楚技术路线至关重要。一个看似简单的天气查询背后有几个绕不开的问题数据从哪来怎么取取回来怎么用2.1 数据源的选择稳定、免费、易用的API首先得找个靠谱的天气数据提供商。市面上有很多选择比如和风天气、OpenWeatherMap等。考虑到教程的通用性和可访问性我选择了心知天气SENIVERSE的免费API。它的优点很明显提供稳定的免费额度足够个人学习使用文档清晰返回标准的JSON格式数据非常适合教学和自用项目。当然你也可以替换成任何你喜欢的API核心的HTTP请求和JSON解析逻辑是相通的。注意使用任何第三方API第一件事就是去官网注册账号获取你的专属API Key。这个Key相当于访问凭证一定要妥善保管不要直接硬编码在提交到公开仓库的源码里。2.2 网络库的抉择轻量级与跨平台C标准库没有提供原生的HTTP客户端支持这是我们需要引入第三方库的原因。选择哪个库直接影响到项目的复杂度和可移植性。cURL行业标准功能强大但稍显笨重。cURL几乎是C/C领域处理网络协议的事实标准支持HTTPS、FTP等一大堆协议。但它通常需要额外安装开发库并且在项目构建时需要链接。对于我们的简单需求来说它有点“杀鸡用牛刀”。libcurl的C封装如CPR更现代的接口。CPR库提供了类似Pythonrequests库的优雅API用起来非常顺手。但它本质上是libcurl的包装依然依赖libcurl。纯C的轻量级方案如httplib本教程的选择。我最终选择了httplib。它是一个单头文件header-only的库由yhirose开发。你只需要在项目中包含httplib.h这一个文件就能直接使用HTTP客户端和服务器功能。它足够轻量依赖少且支持HTTPS需要依赖OpenSSL但我们的免费API通常用HTTP也够用或者可以编译时链接OpenSSL。这对于初学者构建一个独立、干净的项目非常友好。2.3 数据解析JSON库的挑选API返回的数据是JSON字符串我们需要把它转换成C里方便操作的数据结构比如std::map或自定义结构体。同样标准库不提供JSON解析。nlohmann/json当前最流行的选择。这也是一个单头文件库语法非常直观几乎可以像JavaScript一样操作JSON。例如json j json::parse(jsonString); string city j[results][0][location][name];。它的易用性极高是本教程的推荐。RapidJSON性能极致但API稍复杂。腾讯开源的RapidJSON以速度著称但它的API是面向DOM文档对象模型和SAX流式解析风格的对于新手来说学习曲线比nlohmann/json陡峭。JsonCpp老牌稳定。也是一个不错的选择但相比nlohmann/json其现代性和便捷性稍逊。综合来看httplibnlohmann/json的组合能以最小的环境依赖和最简单的代码实现我们的核心功能非常适合教学和快速原型开发。2.4 项目结构设计一个清晰的项目结构能让代码更易读、易维护。我建议这样组织weather_cpp/ ├── include/ │ └── httplib.h # 网络库单头文件 ├── src/ │ ├── main.cpp # 程序入口负责参数解析和流程控制 │ ├── weather_api.cpp # 封装与天气API的交互逻辑 │ └── weather_api.h ├── third_party/ │ └── json.hpp # nlohmann/json 单头文件 ├── CMakeLists.txt # 跨平台构建脚本 └── README.md使用CMake作为构建系统可以很好地管理依赖和跨平台编译Windows, Linux, macOS。3. 环境准备与核心库集成工欲善其事必先利其器。我们先来把开发环境搭建好。3.1 编译器与构建工具你需要一个支持C11或更新标准的编译器。Linux/macOS上通常自带GCC或ClangWindows上推荐使用MinGW-w64或Visual Studio的MSVC。我个人的开发环境是WSL2Ubuntu配合GCC以及Windows上的Visual Studio Code CMake。关键一步安装CMake。CMake是一个跨平台的自动化构建系统生成器。你可以从官网下载安装。在Ubuntu上一句命令即可sudo apt-get update sudo apt-get install cmake g3.2 获取并放置第三方库如前所述我们使用单头文件库省去了复杂的编译安装过程。下载httplib.h访问https://github.com/yhirose/cpp-httplib下载仓库中的httplib.h文件放到项目的include/目录下。下载json.hpp访问https://github.com/nlohmann/json下载仓库中的single_include/nlohmann/json.hpp文件重命名为json.hpp或保持原名放到项目的third_party/目录下。这样我们的项目就自包含了所有必要的依赖无需系统级安装非常干净。3.3 编写CMakeLists.txt这是项目的“总指挥”告诉编译器如何编译、链接。创建一个CMakeLists.txt文件内容如下cmake_minimum_required(VERSION 3.10) project(WeatherCLI CXX) # 设置C标准 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 包含头文件目录 include_directories(${PROJECT_SOURCE_DIR}/include) include_directories(${PROJECT_SOURCE_DIR}/third_party) # 如果是Windows且使用MSVC可能需要定义宏来避免Windows.h的冲突 if (WIN32 AND MSVC) add_definitions(-D_WINSOCK_DEPRECATED_NO_WARNINGS) endif() # 添加可执行文件并链接所有源文件 add_executable(weather_cli src/main.cpp src/weather_api.cpp ) # 在Linux/macOS下如果需要HTTPS支持需要链接OpenSSL和pthread if (UNIX AND NOT APPLE) target_link_libraries(weather_cli pthread ssl crypto) elseif (APPLE) # macOS 链接 target_link_libraries(weather_cli ssl crypto) endif()这个配置做了几件事指定C11标准将我们存放头文件的目录加入编译器搜索路径根据平台差异进行条件设置最后定义生成的可执行文件名为weather_cli并指定了源文件。在Linux下因为httplib可能用到线程和SSL所以我们链接了pthread,ssl,crypto库。4. 核心代码实现一步步构建天气查询引擎环境搭好了现在进入最核心的编码环节。我们将从底层API交互模块开始自底向上构建整个程序。4.1 封装天气API交互模块 (weather_api.h和weather_api.cpp)这个模块负责所有与心知天气API打交道的细节对外提供一个干净的接口。首先看头文件weather_api.h它定义了数据结构和核心函数。// weather_api.h #ifndef WEATHER_API_H #define WEATHER_API_H #include string #include vector // 定义一个结构体来存放解析后的天气信息 struct WeatherInfo { std::string cityName; // 城市名 std::string text; // 天气现象文字 std::string code; // 天气现象代码 std::string temperature; // 温度 std::string lastUpdate; // 数据更新时间 // 可以根据需要扩展更多字段如湿度、风速、风向等 std::string humidity; // 湿度 std::string windDirection; // 风向 std::string windScale; // 风力等级 }; // 核心API类 class WeatherAPI { public: // 构造函数传入你的API Key explicit WeatherAPI(const std::string apiKey); // 根据城市名查询实时天气返回解析后的WeatherInfo对象 WeatherInfo fetchCurrentWeather(const std::string cityName) const; // 获取最近一次的错误信息如果请求或解析失败 std::string getLastError() const { return lastError_; } private: std::string apiKey_; // 存储API Key mutable std::string lastError_; // 记录错误信息mutable允许在const成员函数中修改 // 内部方法构建完整的API请求URL std::string buildRequestUrl(const std::string cityName) const; // 内部方法解析JSON响应到WeatherInfo结构体 bool parseJsonResponse(const std::string jsonStr, WeatherInfo info) const; }; #endif // WEATHER_API_H接下来是实现文件weather_api.cpp这里包含了具体的网络请求和解析逻辑。// weather_api.cpp #include weather_api.h #include httplib.h // 注意路径我们在CMake中已设置 #include json.hpp // 注意路径 #include iostream #include sstream using json nlohmann::json; WeatherAPI::WeatherAPI(const std::string apiKey) : apiKey_(apiKey) {} std::string WeatherAPI::buildRequestUrl(const std::string cityName) const { // 心知天气实时天气API地址免费版 std::string baseUrl https://api.seniverse.com/v3/weather/now.json; // 使用stringstream来安全地构建查询字符串 std::ostringstream urlStream; urlStream baseUrl ?key apiKey_ location cityName languagezh-Hansunitc; return urlStream.str(); } WeatherInfo WeatherAPI::fetchCurrentWeather(const std::string cityName) const { WeatherInfo info; lastError_.clear(); // 清空旧错误 // 1. 构建请求URL std::string url buildRequestUrl(cityName); // httplib需要将URL拆分为主机和路径 // 这里简单处理假设是HTTPS。对于免费API也可以使用HTTPseniverse.com httplib::Client cli(api.seniverse.com, 443); // 端口443是HTTPS // 2. 发起GET请求 // 注意心知天气的免费API可能需要使用HTTP如果是则改为 // httplib::Client cli(api.seniverse.com); // 并且URL中不要带https:// auto res cli.Get(url.c_str()); // 3. 检查请求结果 if (!res) { lastError_ 网络请求失败无法连接到服务器或超时。; return info; // 返回空的info } if (res-status ! 200) { std::ostringstream errMsg; errMsg API请求失败状态码: res-status 响应体: res-body; lastError_ errMsg.str(); return info; } // 4. 解析JSON响应 if (!parseJsonResponse(res-body, info)) { // parseJsonResponse内部会设置lastError_ return info; } return info; } bool WeatherAPI::parseJsonResponse(const std::string jsonStr, WeatherInfo info) const { try { json j json::parse(jsonStr); // 检查API返回的状态信息心知天气的格式 if (j.contains(status_code) j[status_code] ! OK) { lastError_ API返回错误: j.dump(); return false; } // 解析核心数据。根据心知天气文档数据在results数组的第一个元素中 auto results j[results]; if (results.is_array() !results.empty()) { auto location results[0][location]; auto now results[0][now]; auto lastUpdate results[0][last_update]; // 注意字段名可能为last_update info.cityName location[name].getstd::string(); info.text now[text].getstd::string(); info.code now[code].getstd::string(); info.temperature now[temperature].getstd::string(); info.lastUpdate lastUpdate.getstd::string(); // 解析扩展字段如果API返回 if (now.contains(humidity)) { info.humidity now[humidity].getstd::string() %; } if (now.contains(wind_direction)) { info.windDirection now[wind_direction].getstd::string(); } if (now.contains(wind_scale)) { info.windScale now[wind_scale].getstd::string() 级; } return true; } else { lastError_ JSON解析错误未找到有效的‘results’数据。; return false; } } catch (const json::exception e) { lastError_ std::string(JSON解析异常: ) e.what(); return false; } catch (...) { lastError_ 解析响应时发生未知异常。; return false; } }代码要点解析与避坑指南URL编码城市名中可能包含空格或中文在构建URL时必须进行编码。httplib的Get方法参数是C风格字符串它内部或系统库可能会处理一部分但最稳妥的做法是使用httplib的Params对象或手动编码。这里为了代码清晰先省略但实际应用中如果查询“New York”需要将其编码为New%20York。一个简单的处理方法是使用httplib的detail::encode_url函数它是内部函数需谨慎或引入其他编码库如libcurl的curl_easy_escape。在我们的示例中先假设输入是英文或拼音无空格。错误处理网络请求充满不确定性必须对每一步进行错误检查连接是否建立、HTTP状态码是否为200、返回的JSON是否能正确解析。我们将错误信息存储在lastError_中方便上层调用者查询。JSON解析安全使用nlohmann/json的contains()方法在访问前检查键是否存在避免因API返回字段变化导致程序崩溃。使用try-catch块捕获解析异常。API Key管理在构造函数中传入API Key是简单的做法。更安全的方式是从环境变量或配置文件中读取避免密钥泄露在源码中。4.2 编写程序主入口 (main.cpp)主程序负责协调工作解析命令行参数调用WeatherAPI类获取数据并友好地展示结果。// main.cpp #include weather_api.h #include iostream #include string int main(int argc, char* argv[]) { // 1. 设置你的API Key // **重要不要将真实的API Key提交到公开Git仓库** // 最佳实践是从环境变量或配置文件中读取。 const std::string apiKey YOUR_API_KEY_HERE; // 请替换成你自己的Key // 2. 检查命令行参数 std::string cityName; if (argc 2) { cityName argv[1]; } else { std::cerr 用法: argv[0] 城市名 std::endl; std::cerr 示例: argv[0] Beijing std::endl; std::cerr argv[0] 上海 std::endl; return 1; // 非零返回值表示错误 } // 3. 创建API客户端并查询 WeatherAPI client(apiKey); WeatherInfo weather client.fetchCurrentWeather(cityName); // 4. 处理结果并输出 if (!client.getLastError().empty()) { std::cerr 错误: client.getLastError() std::endl; return 1; } // 5. 美化输出 std::cout \n 实时天气查询 std::endl; std::cout 城市: weather.cityName std::endl; std::cout 天气: weather.text std::endl; std::cout 温度: weather.temperature °C std::endl; if (!weather.humidity.empty()) { std::cout 湿度: weather.humidity std::endl; } if (!weather.windDirection.empty() !weather.windScale.empty()) { std::cout 风力: weather.windDirection weather.windScale std::endl; } std::cout 更新: weather.lastUpdate std::endl; std::cout \n std::endl; return 0; // 成功返回0 }主程序注意事项API Key硬编码问题这是为了演示简单。在实际项目中绝对不要像这样把Key写在源码里。应该通过环境变量如SENIVERSE_API_KEY或一个不被版本控制的配置文件如config.ini来读取。命令行参数解析这里只处理了最简单的程序名 城市名格式。对于更复杂的参数如帮助-h、指定输出格式-j等可以考虑使用getoptPOSIX系统或第三方库如cxxopts。用户输入验证没有对cityName做任何清洗或编码直接传给了API。在生产环境中需要对用户输入进行验证和预处理。5. 编译、运行与测试代码写完了让我们把它跑起来。5.1 使用CMake构建项目在项目根目录CMakeLists.txt所在目录打开终端执行以下命令# 1. 创建一个构建目录并进入保持源码目录清洁 mkdir build cd build # 2. 运行cmake生成对应平台的构建文件Makefile或.sln等 cmake .. # 3. 执行编译 make -j4 # Linux/macOS使用make-j4表示用4个线程并行编译以加快速度在Windows上如果你使用Visual Studio可以在build目录下打开生成的.sln文件进行编译。或者使用CMake命令行指定生成器cmake -G MinGW Makefiles ..然后mingw32-make。编译成功后在build目录下会生成可执行文件weather_cliWindows下可能是weather_cli.exe。5.2 运行程序首先确保你已经将main.cpp中的YOUR_API_KEY_HERE替换为从心知天气官网获取的真实API Key。# 在build目录下 ./weather_cli Beijing如果一切顺利你将看到类似下面的输出 实时天气查询 城市: 北京 天气: 晴 温度: 25°C 湿度: 40% 风力: 东南风 3级 更新: 2023-10-27T14:50:0008:00 5.3 测试与调试技巧测试网络连通性如果程序卡住或报网络错误先用curl命令测试API是否可访问curl https://api.seniverse.com/v3/weather/now.json?keyYOUR_KEYlocationBeijing。这能帮你快速定位是代码问题还是网络/API问题。打印原始响应在调试parseJsonResponse函数时可以在解析前将jsonStr打印出来确认API返回的数据结构是否符合预期。这能有效解决90%的解析错误。处理中文编码在Windows命令行如CMD中直接运行中文字符可能显示为乱码。这是因为Windows控制台默认编码是GBK而我们的程序输出是UTF-8。一个解决办法是修改控制台代码页chcp 65001临时切换为UTF-8或者考虑在输出时进行编码转换比较复杂。在Linux/macOS的终端或Windows的现代终端如Windows Terminal中通常没有此问题。API调用频率限制免费API通常有调用次数限制如心知天气免费版每小时最多调用10次。如果你的程序突然获取不到数据并返回错误码请检查是否超限。6. 功能扩展与优化思路一个基础版本完成后我们可以从多个角度让它变得更实用、更健壮。6.1 支持更多天气数据心知天气的API返回的数据很丰富我们只解析了一部分。你可以轻松地扩展WeatherInfo结构体和parseJsonResponse函数加入更多字段如能见度visibility、气压pressure、体感温度feels_like以及未来几天的天气预报需要使用不同的API接口如/v3/weather/daily.json。6.2 添加配置文件支持硬编码API Key和API端点Base URL很不灵活。可以引入一个简单的配置文件比如使用libconfig、yaml-cpp或直接读写JSON文件。一个简单的config.json示例{ api_key: your_real_key_here, base_url: https://api.seniverse.com/v3/weather/now.json, language: zh-Hans, unit: c }程序启动时读取这个文件这样更换API Key或切换测试/生产环境就非常方便。6.3 实现更友好的命令行交互使用cxxopts这样的库来解析命令行参数可以轻松添加以下功能-h, --help显示帮助信息。-c, --city指定城市支持多个城市如-c Beijing -c Shanghai。-f, --format指定输出格式如json原始JSON、brief简要信息、full详细信息。-o, --output将结果输出到指定文件。6.4 加入缓存机制频繁查询同一城市的天气会对API造成不必要的请求也受限于调用频率。可以在本地实现一个简单的缓存将查询结果按城市名和当前时间戳保存到内存或一个小的本地数据库如SQLite中。下次查询时如果缓存存在且未过期比如10分钟内就直接使用缓存数据否则再发起网络请求。6.5 跨平台图形界面可选如果你不满足于命令行可以尝试用Qt或Dear ImGui为这个天气引擎套一个图形界面。核心的WeatherAPI类完全不需要改动只需新建一个GUI项目调用它的接口获取数据然后在窗口上展示出来。这是练习C GUI编程和模块化设计的好机会。7. 常见问题与解决方案实录在开发和教学过程中我遇到了不少典型问题这里汇总一下希望能帮你少走弯路。问题1编译时找不到httplib.h或json.hpp头文件。现象fatal error: httplib.h: No such file or directory原因编译器在标准路径和-I指定的路径中找不到头文件。解决检查CMakeLists.txt中的include_directories语句路径是否正确。确保头文件确实放在了include/和third_party/目录下。如果是手动编译没用CMake确保使用-I ./include -I ./third_party参数。问题2链接错误提示undefined reference toSSL相关函数。现象在Linux下编译成功但链接时失败报错如undefined reference toSSL_CTX_new。原因httplib启用了HTTPS支持默认但编译时没有链接OpenSSL库。解决确保系统已安装OpenSSL开发包如Ubuntu的libssl-dev。在CMakeLists.txt中正确链接ssl和crypto库正如我们之前做的那样。如果不需要HTTPS可以在包含httplib.h之前定义宏#define CPPHTTPLIB_OPENSSL_SUPPORT 0来禁用HTTPS支持然后使用HTTP连接需确认API支持HTTP。问题3程序运行后立即退出或没有任何输出。现象在Windows双击.exe文件窗口一闪而过。原因这是Windows控制台程序的典型行为。程序执行完就退出了。解决在命令行终端CMD, PowerShell, Git Bash中运行程序。或者在main函数末尾return 0;之前加上system(pause);仅Windows且不推荐用于生产代码。更好的方法是始终在终端里运行你的命令行程序。问题4查询中文城市名失败API返回错误。现象输入“上海”报错但输入“shanghai”可能成功。原因中文字符在URL中需要编码URL Encoding。httplib的Get方法接收的path参数可能没有自动编码查询参数中的中文。解决使用httplib::Params对象来构建查询参数它会自动处理编码。httplib::Params params; params.emplace(key, apiKey_); params.emplace(location, cityName); params.emplace(language, zh-Hans); params.emplace(unit, c); auto res cli.Get(/v3/weather/now.json, params, headers);或者手动对cityName进行URL编码。C标准库没有现成函数可以自己实现一个简单的或使用第三方库如libcurl的curl_easy_escape。问题5返回的JSON解析成功但某些字段为空。现象湿度、风力等字段显示为空。原因不同天气API返回的JSON字段名和结构可能不同。心知天气的实时天气接口/now.json返回的now对象中湿度和风力字段名可能是humidity、wind_direction、wind_scale但也可能因API版本变化而不同。解决仔细阅读你所使用的API的官方文档核对字段名。在调试时将API返回的完整JSON字符串打印出来直观地查看数据结构。在代码中使用contains()安全地检查字段是否存在避免程序崩溃。这个项目虽然代码量不大但完整地串联了C项目开发中的关键环节第三方库的选择与集成、网络请求、数据解析、错误处理、跨平台构建。你可以根据自己的兴趣选择上面提到的任何一个扩展方向进行深入把它打造成一个真正符合你自己需求的工具。编程的乐趣往往就在于从这样一个个小项目中看到自己的想法一步步变成现实。