C++通过WPS读取Excel:COM自动化接口实战与性能优化
简介这份资源面向在Windows平台下使用C进行桌面开发的程序员尤其是仍在使用VS2010与MFC框架、需要让程序读取XLSX表格数据的开发者。它围绕WPS Office提供的接口演示如何在不依赖Microsoft Office的情况下完成Excel文件的打开与数据解析适合具备一定C与MFC基础、希望打通办公软件自动化读取链路的中级学习者。压缩包共29个文件约19.6MB以12个h头文件与3个cpp源文件为核心涵盖CApplicationET、CWorkbook、CWorksheet、CRange等封装类另有sln解决方案、vcxproj工程文件、rc资源脚本及ico图标等构成一套可直接编译运行的完整MFC工程。目前已有1802人学习下载。读者可从中获得动态库加载与接口指针获取的完整代码、工作簿与单元格遍历的封装思路、数据绑定到界面控件的实现方式以及文件不存在、打开失败等异常的处理参考便于快速移植到自己的项目中。1. 用 C 通过 WPS 读取 Excel一条被低估的自动化路径很多做桌面工具、工业上位机或者数据采集软件的同行一提到用 C 读 Excel第一反应就是去找 libxlsxwriter、OpenXLSX 这类开源库或者干脆上 COM 调 Office。但真实场景里经常遇到一个尴尬客户机器上装的是 WPS 而不是微软 Office或者项目本身就跑在国产化环境里这时候你按 Office COM 写的那套代码直接歇菜。C 通过 WPS 读取 Excel 这件事本质上就是利用 WPS 提供的 COM 自动化接口让 C 程序像操作 Office 一样去驱动 WPS 表格组件把 .xlsx / .et 里的单元格数据读进内存。它适合谁适合那些需要在 Windows 桌面端做数据导入、报表解析、批量校验又必须兼容 WPS 环境的 C 开发者。这条路不算新但资料零散坑也集中下面把我自己踩过的完整路径拆开讲。2. 先搞清楚 WPS 的 COM 接口到底长什么样2.1 为什么不是直接读文件而是走自动化.xlsx 本质上是个 zip 包里面是 XML理论上你完全可以自己解压解析。但现实是业务方给你的表格里可能有合并单元格、公式、条件格式、隐藏行甚至多个 sheet 之间的引用。自己解析 XML 意味着你要重新实现一套电子表格的语义层工作量巨大且容易出错。走 COM 自动化的好处是WPS 自己把表格解析好了你只需要通过接口问它「第 3 行第 2 列是什么」它直接返回给你计算后的值。代价是必须本机安装 WPS而且进程间通信有开销。常见做法是数据量大、格式简单、追求极致性能的场景自己解析 XML格式复杂、需要拿到公式计算结果、需要兼容 WPS 的场景走 COM。2.2 WPS 暴露的 COM 对象模型WPS 的 COM 接口和微软 Office 的 Excel COM 接口高度相似核心对象层次是这样的Application 对象代表整个 WPS 应用通过它拿到 Workbooks 集合打开一个 Workbook再从 Workbook 拿到 Worksheets 集合选中一个 Worksheet最后通过 Cells(row, col) 或者 Range(A1) 拿到具体单元格。单元格对象的 Value 属性返回变体类型VARIANT你需要根据实际类型做转换。关键点在于WPS 的 ProgID 和 Office 不一样。Office 是 Excel.ApplicationWPS 通常是 KET.Application表格组件的 ProgID。如果你用 Excel.Application 去创建在只装了 WPS 的机器上会失败。所以代码里创建 COM 对象时ProgID 要写成 KET.Application或者更稳妥的做法是先尝试 Excel.Application失败再回退到 KET.Application。2.3 环境准备与项目配置在动手写代码之前有几件事必须确认。第一WPS 必须安装完整版某些精简版或者绿色版可能没有注册 COM 组件。第二在 Visual Studio 项目里如果你要用 #import 方式引入类型库需要找到 WPS 安装目录下的类型库文件通常叫 ketapi.dll 或者 wpsapi.dll 之类的具体名字随版本变化。更通用的做法是不用 #import直接用 IDispatch 接口做后期绑定这样不依赖类型库文件编译期不需要额外配置代价是调用时没有智能提示参数要自己拼。我一般推荐后期绑定因为部署到不同机器上时不用操心类型库版本匹配问题。// 后期绑定方式创建 WPS 表格应用对象 #include windows.h #include comdef.h #include iostream // 辅助函数将宽字符串转为 BSTR BSTR ToBSTR(const wchar_t* str) { return SysAllocString(str); } int main() { // 初始化 COM 库单线程套间模式 HRESULT hr CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); if (FAILED(hr)) { std::cerr COM 初始化失败 std::endl; return -1; } CLSID clsid; // 先尝试 WPS 表格的 ProgID hr CLSIDFromProgID(LKET.Application, clsid); if (FAILED(hr)) { // 回退尝试微软 Excel 的 ProgID hr CLSIDFromProgID(LExcel.Application, clsid); if (FAILED(hr)) { std::cerr 未找到可用的表格 COM 组件 std::endl; CoUninitialize(); return -1; } } IDispatch* pApp nullptr; hr CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, IID_IDispatch, (void**)pApp); if (FAILED(hr)) { std::cerr 创建表格应用对象失败 std::endl; CoUninitialize(); return -1; } // 后续通过 IDispatch::Invoke 调用方法和属性 // 此处省略具体调用下一章展开 pApp-Release(); CoUninitialize(); return 0; }这段代码做了三件事初始化 COM 环境、根据 ProgID 查找 CLSID、创建应用对象。参数说明CoInitializeEx 的第二个参数用 COINIT_APARTMENTTHREADED 是因为 COM 自动化对象通常要求单线程套间用多线程套间可能会遇到跨套间调用问题。CLSCTX_LOCAL_SERVER 表示启动一个独立的进程来承载 WPS 表格组件这样即使你的程序崩溃也不会把 WPS 主进程带崩。注意创建出来的 pApp 是 IDispatch 指针后面所有操作都要通过 Invoke 来调用需要自己准备 DISPID。3. 用 IDispatch 一步步把单元格数据读出来3.1 获取属性和方法的 DISPID后期绑定的核心是 IDispatch::Invoke而 Invoke 需要 DISPID。获取 DISPID 有两种方式GetIDsOfNames 通过名字查或者你自己根据类型库信息硬编码。显然用 GetIDsOfNames 更灵活。每次调用属性或方法前先用 GetIDsOfNames 拿到对应的 DISPID然后构造 DISPPARAMS 传参。属性获取用 DISPATCH_PROPERTYGET方法调用用 DISPATCH_METHOD。这里有个细节WPS 的 COM 接口里很多属性既可以当属性读也可以当方法调比如 Cells 属性你既可以当属性获取也可以当方法带参数调用。我一般统一用 PROPERTYGET 加参数的方式兼容性更好。// 辅助函数通过名字获取 DISPID DISPID GetDispID(IDispatch* pDisp, const wchar_t* name) { LPOLESTR names[1] { (LPOLESTR)name }; DISPID dispid; HRESULT hr pDisp-GetIDsOfNames(IID_NULL, names, 1, LOCALE_USER_DEFAULT, dispid); if (FAILED(hr)) { return DISPID_UNKNOWN; } return dispid; } // 辅助函数调用无参数属性获取 VARIANT GetProperty(IDispatch* pDisp, const wchar_t* name) { VARIANT result; VariantInit(result); DISPID dispid GetDispID(pDisp, name); if (dispid DISPID_UNKNOWN) return result; DISPPARAMS params { nullptr, nullptr, 0, 0 }; pDisp-Invoke(dispid, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYGET, params, result, nullptr, nullptr); return result; } // 辅助函数调用带一个参数的属性获取 VARIANT GetPropertyWithArg(IDispatch* pDisp, const wchar_t* name, VARIANT arg) { VARIANT result; VariantInit(result); DISPID dispid GetDispID(pDisp, name); if (dispid DISPID_UNKNOWN) return result; DISPPARAMS params; params.rgvarg arg; params.rgdispidNamedArgs nullptr; params.cArgs 1; params.cNamedArgs 0; pDisp-Invoke(dispid, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYGET, params, result, nullptr, nullptr); return result; }这两个辅助函数是后面所有操作的基础。GetProperty 用于获取无参属性比如 Application 的 Workbooks。GetPropertyWithArg 用于获取带参属性比如 Worksheets 的 Item 或者 Cells。参数说明DISPPARAMS 结构里 rgvarg 是参数数组注意 COM 的参数顺序是反的最后一个参数在数组最前面。cArgs 是参数个数。调用成功后 result 里就是返回值可能是 IDispatch 指针、BSTR、double 等需要根据 vt 字段判断类型。3.2 打开工作簿并定位工作表有了辅助函数打开工作簿就顺了。先拿到 Workbooks 属性然后调用 Open 方法传入文件路径。Open 方法的参数比较多但大部分可以传默认值。关键参数是 Filename其他如 UpdateLinks、ReadOnly、Password 等按需传。打开后拿到 Workbook 对象再通过 Worksheets 的 Item 属性按索引或名字拿到 Worksheet。注意索引从 1 开始不是 0。// 打开工作簿并获取第一个工作表 VARIANT vWorkbooks GetProperty(pApp, LWorkbooks); IDispatch* pWorkbooks vWorkbooks.pdispVal; // 准备 Open 方法的参数文件名 VARIANT vFilename; VariantInit(vFilename); vFilename.vt VT_BSTR; vFilename.bstrVal ToBSTR(LD:\\data\\test.xlsx); // 调用 Open 方法 DISPID dispidOpen GetDispID(pWorkbooks, LOpen); DISPPARAMS paramsOpen; paramsOpen.rgvarg vFilename; paramsOpen.rgdispidNamedArgs nullptr; paramsOpen.cArgs 1; paramsOpen.cNamedArgs 0; VARIANT vWorkbook; VariantInit(vWorkbook); pWorkbooks-Invoke(dispidOpen, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, paramsOpen, vWorkbook, nullptr, nullptr); IDispatch* pWorkbook vWorkbook.pdispVal; // 获取 Worksheets 集合 VARIANT vSheets GetProperty(pWorkbook, LWorksheets); IDispatch* pSheets vSheets.pdispVal; // 获取第一个工作表索引从 1 开始 VARIANT vIndex; VariantInit(vIndex); vIndex.vt VT_I4; vIndex.lVal 1; VARIANT vSheet GetPropertyWithArg(pSheets, LItem, vIndex); IDispatch* pSheet vSheet.pdispVal;这段代码里Open 方法的参数只传了文件名其他参数 WPS 会用默认值。实际项目中如果文件有密码需要额外传 Password 参数参数位置要查 WPS 的开发文档不同版本可能有差异。拿到 Worksheet 后就可以通过 Cells 属性读单元格了。Cells 接受两个参数行号和列号都是 1 起始。3.3 读取单元格的值并处理类型转换读单元格是整个流程里最琐碎的部分因为 Value 返回的是 VARIANT可能是 double、BSTR、bool、甚至错误值。你需要写一个健壮的转换函数把 VARIANT 转成你需要的类型。常见做法是如果 vt 是 VT_BSTR直接取 bstrVal如果是 VT_R8转成字符串或者保留为 double如果是 VT_BOOL转成 true/false如果是 VT_ERROR 或者 VT_EMPTY返回空字符串。注意 WPS 返回的字符串可能是 BSTR用完要 SysFreeString否则内存泄漏。// 将单元格 VARIANT 值转为 std::wstring std::wstring VariantToString(const VARIANT var) { switch (var.vt) { case VT_BSTR: return std::wstring(var.bstrVal, SysStringLen(var.bstrVal)); case VT_R8: { wchar_t buf[64]; swprintf_s(buf, L%.10g, var.dblVal); return buf; } case VT_I4: { wchar_t buf[32]; swprintf_s(buf, L%d, var.lVal); return buf; } case VT_BOOL: return var.boolVal VARIANT_TRUE ? LTRUE : LFALSE; case VT_EMPTY: case VT_NULL: return L; default: return L; } } // 读取指定行列的单元格值 std::wstring ReadCell(IDispatch* pSheet, int row, int col) { // 准备 Cells(row, col) 的两个参数注意顺序是反的 VARIANT args[2]; VariantInit(args[0]); args[0].vt VT_I4; args[0].lVal col; // 第二个参数放前面 VariantInit(args[1]); args[1].vt VT_I4; args[1].lVal row; // 第一个参数放后面 DISPID dispidCells GetDispID(pSheet, LCells); DISPPARAMS params; params.rgvarg args; params.rgdispidNamedArgs nullptr; params.cArgs 2; params.cNamedArgs 0; VARIANT vCell; VariantInit(vCell); pSheet-Invoke(dispidCells, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYGET, params, vCell, nullptr, nullptr); if (vCell.vt ! VT_DISPATCH || vCell.pdispVal nullptr) { return L; } // 拿到 Range 对象后读 Value 属性 VARIANT vValue GetProperty(vCell.pdispVal, LValue); std::wstring result VariantToString(vValue); // 清理 VariantClear(vValue); vCell.pdispVal-Release(); return result; }这段代码的关键点Cells 属性的两个参数在 DISPPARAMS 里是反序的列号在前、行号在后这是 COM 调用的通用规则。拿到 Range 对象后再读它的 Value 属性。VariantToString 里对 VT_R8 用了 %.10g 格式化避免浮点数出现一长串小数。注意每次 Invoke 返回的 VARIANT 如果包含 BSTR 或 IDispatch都要手动释放否则长时间运行会内存暴涨。我一般会在每个循环结束后统一清理或者用 RAII 封装一个 VariantGuard 类。4. 批量读取的性能优化与资源释放4.1 逐单元格读为什么慢如果你按上面的方式一个单元格一个单元格地读一千行十列就是一万次跨进程调用每次调用都要序列化参数、切换线程、等待返回耗时可能到几十秒。这不是 WPS 慢是 COM 后期绑定的固有开销。优化思路很直接减少调用次数。WPS 的 Range 对象支持一次读一大片区域返回一个二维数组的 VARIANT。你可以一次性把 A1 到 J1000 全部读进来然后在本地遍历数组。这样只需要一次跨进程调用速度提升几十倍。// 批量读取一个矩形区域返回二维数组 // 调用前确保 pSheet 有效 VARIANT ReadRange(IDispatch* pSheet, const wchar_t* rangeAddr) { VARIANT vRangeAddr; VariantInit(vRangeAddr); vRangeAddr.vt VT_BSTR; vRangeAddr.bstrVal ToBSTR(rangeAddr); DISPID dispidRange GetDispID(pSheet, LRange); DISPPARAMS params; params.rgvarg vRangeAddr; params.rgdispidNamedArgs nullptr; params.cArgs 1; params.cNamedArgs 0; VARIANT vRange; VariantInit(vRange); pSheet-Invoke(dispidRange, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYGET, params, vRange, nullptr, nullptr); if (vRange.vt ! VT_DISPATCH || vRange.pdispVal nullptr) { VariantClear(vRangeAddr); return vRange; } // 读 Value 属性返回二维数组 VARIANT vValues GetProperty(vRange.pdispVal, LValue); VariantClear(vRangeAddr); vRange.pdispVal-Release(); return vValues; }调用 ReadRange(pSheet, LA1:J1000) 后vValues 的 vt 会是 VT_ARRAY | VT_VARIANT里面是一个二维 SAFEARRAY。你需要用 SafeArrayGetElement 或者 SafeArrayAccessData 来遍历。注意 SAFEARRAY 的索引顺序WPS 返回的数组第一维是行第二维是列但 SafeArrayGetElement 的索引数组顺序要按 SafeArrayGetLBound 和 SafeArrayGetUBound 来确定。我一般先用 SafeArrayGetLBound 拿到下界然后双重循环读取。4.2 关闭工作簿与退出应用的顺序资源释放这块有个血泪经验如果你只 Release 了 Workbook 但没有调用 CloseWPS 进程会残留下次再创建可能拿到旧的实例导致读到脏数据。正确的顺序是先调用 Workbook 的 Close 方法参数传 false 表示不保存然后 Release Workbook 指针再调用 Application 的 Quit 方法最后 Release Application 指针。每一步都要检查返回值失败时记录日志。另外CoUninitialize 必须在所有 COM 对象释放之后调用否则会报错。// 按正确顺序关闭工作簿并退出应用 void CloseWorkbookAndQuit(IDispatch* pWorkbook, IDispatch* pApp) { if (pWorkbook) { // 调用 Close参数 false 表示不保存 VARIANT vSave; VariantInit(vSave); vSave.vt VT_BOOL; vSave.boolVal VARIANT_FALSE; DISPID dispidClose GetDispID(pWorkbook, LClose); DISPPARAMS params; params.rgvarg vSave; params.rgdispidNamedArgs nullptr; params.cArgs 1; params.cNamedArgs 0; pWorkbook-Invoke(dispidClose, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, params, nullptr, nullptr, nullptr); pWorkbook-Release(); } if (pApp) { // 调用 Quit 方法 DISPID dispidQuit GetDispID(pApp, LQuit); DISPPARAMS params { nullptr, nullptr, 0, 0 }; pApp-Invoke(dispidQuit, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, params, nullptr, nullptr, nullptr); pApp-Release(); } }这段代码里Close 方法的参数是 VARIANT_BOOL传 VARIANT_FALSE 表示不保存修改。Quit 方法没有参数直接传空 DISPPARAMS。注意 Invoke 的返回值这里传了 nullptr因为我们不关心返回结果。实际项目中建议还是传一个 VARIANT 接收检查一下 HRESULT方便排查问题。5. 避坑指南那些让我加班到凌晨的坑5.1 坑一ProgID 写死 Excel.Application 导致找不到组件现象在只装了 WPS 的机器上运行程序报「未找到可用的表格 COM 组件」或者直接崩溃。原因代码里硬编码了 Excel.Application而 WPS 注册的 ProgID 是 KET.Application。解决按第 2 章的方式先尝试 KET.Application失败再回退 Excel.Application。更稳妥的做法是把 ProgID 做成配置项让部署人员根据实际环境填写。5.2 坑二单元格值读出来是科学计数法或者精度丢失现象表格里明明是 1234567890123读出来变成 1.23457E12。原因VariantToString 里对 VT_R8 用了 %g 格式化默认精度只有 6 位。解决改用 %.15g 或者 %.17g保证 double 的完整精度。如果业务要求保留原始显示格式那就不要读 Value改读 Text 属性Text 返回的是格式化后的字符串。5.3 坑三WPS 进程残留导致文件被锁定现象程序退出后任务管理器里还有 WPS 进程再次运行程序时提示文件被占用。原因没有调用 Workbook 的 Close 方法或者 Close 调用失败但没有处理。解决严格按 4.2 的顺序释放并且在 Close 之前先把所有 Range 和 Worksheet 的指针 Release 掉。如果还是残留可以在程序启动时先枚举进程把残留的 WPS 进程杀掉但这属于兜底手段不建议作为常规方案。5.4 坑四多线程环境下 COM 调用崩溃现象在主线程里读 Excel 正常放到工作线程里就随机崩溃。原因COM 对象有套间Apartment概念在一个线程里创建的 COM 对象不能直接在另一个线程里使用除非做列集Marshal。解决要么把所有 COM 调用都放在同一个线程里要么在每个使用 COM 的线程里单独 CoInitializeEx 并创建自己的 Application 对象。我一般选择前者用一个专门的读取线程通过消息队列把结果传回主线程。5.5 坑五WPS 版本差异导致某些属性不存在现象在某个版本的 WPS 上运行正常换一个版本就报「参数不正确」或者「找不到成员」。原因WPS 的 COM 接口在不同版本间有细微差异某些属性或方法可能被重命名或移除。解决尽量使用最核心、最稳定的接口比如 Cells、Value、Worksheets、Item 这些从早期版本就存在的。如果必须用新接口在调用前先用 GetIDsOfNames 检查 DISPID 是否有效无效则走降级逻辑。6. 进阶技巧用类型库早期绑定提升开发效率后期绑定虽然灵活但写起来太啰嗦每个调用都要拼 DISPID 和 DISPPARAMS。如果你能确定目标环境可以用 #import 引入 WPS 的类型库让编译器生成包装类直接像调普通函数一样调 COM 方法。具体做法是在代码里写 #import progid:KET.Application 或者 #import 路径\ketapi.dll编译器会生成 .tlh 和 .tli 文件。之后就可以用 KET::ApplicationPtr 这样的智能指针调用 Open、Cells 等方法和属性都有智能提示代码量减少一半以上。但早期绑定有个部署问题编译时依赖类型库文件如果目标机器上的 WPS 版本和编译时不一致可能运行时报错。我的习惯是开发阶段用早期绑定提高效率发布前改成后期绑定或者用条件编译在 DEBUG 下用早期绑定RELEASE 下用后期绑定。这样既享受了开发便利又保证了部署兼容性。另外一个小技巧读大文件时先把 WPS 的 ScreenUpdating 设为 falseCalculation 设为手动读完再恢复。这样能避免 WPS 在后台频繁刷新界面和重算公式速度能再快一截。设置方法就是通过 Application 对象的对应属性赋值用 GetProperty 拿到属性后调 Put 或者直接用 Invoke 的 DISPATCH_PROPERTYPUT。这个习惯是我被一个十万行的报表折磨了一下午之后养成的希望帮到你。本文还有配套的精品资源点击获取

相关新闻

ant design vue上传组件itemRender自定义文件列表样式实现方案

ant design vue上传组件itemRender自定义文件列表样式实现方案

接手的管理后台项目越来越多,我发现最容易被“嫌弃”的组件就是上传。ant design vue的Upload组件功能确实全面,但默认文件列表的样式非常克制——一个图标、一个文件名、hover 时冒出两个小按钮,放在表单里勉强能看,放在需要展示…

2026/10/15 22:10:31 阅读更多 →
Flutter dio_http_formatter鸿蒙适配实战

Flutter dio_http_formatter鸿蒙适配实战

1. 项目背景与核心价值在移动端开发中,网络请求的调试和日志记录一直是开发者日常工作中的高频需求。Flutter生态中的dio作为主流网络请求库,其配套的dio_http_formatter插件通过美化输出HTTP请求和响应日志,大幅提升了开发调试效率。然而随着…

2026/10/15 22:10:31 阅读更多 →
skipfish Web资产测绘:新手渗透测试的结构化入门指南

skipfish Web资产测绘:新手渗透测试的结构化入门指南

1. 为什么跳过Nikto、OWASP ZAP,偏偏选skipfish做Web资产初筛?在某高校网络安全实训课上,我带过一批刚接触渗透测试的A同学。他们普遍卡在第一个环节:面对一个目标域名,不知道该从哪下手。有人直接上Burp Suite抓包&am…

2026/10/15 15:27:10 阅读更多 →

最新新闻

基于React Native鸿蒙的智能调度分配模块设计与实践

基于React Native鸿蒙的智能调度分配模块设计与实践

做服务调度类项目,最让人头大的从来不是页面能做得有多花哨,而是“这件事到底该派给谁、怎么快速判断那个人现在真能用”这一连串业务规则背后的逻辑。最近我负责了一个基于React Native鸿蒙跨平台实现的调度分配模块,核心需求很明确&#xf…

2026/10/15 22:10:40 阅读更多 →
临床小讲课高效备:用WorkBuddy快速生成主动脉夹层PPT

临床小讲课高效备:用WorkBuddy快速生成主动脉夹层PPT

1. 临时被点名做小讲课,为什么我第一时间想到的不是自己硬肝在临床科室待过的人都懂,主任临时点名让你第二天晨会做个小讲课,那种感觉比夜班收三个急诊还刺激。尤其是讲主动脉夹层这种心血管急危重症,内容深、进展快、指南更新频繁…

2026/10/15 22:10:40 阅读更多 →
PMP第8章项目质量管理实战:从规划到控制的全流程指南

PMP第8章项目质量管理实战:从规划到控制的全流程指南

1. 质量管理不是“质检员的事”:从三个真实翻车场景说起很多人考完PMP之后,对第8章的记忆就只剩下“规划质量管理、管理质量、控制质量”这三个过程组名称。真到了项目上,要么把质量管理等同于测试团队的事,要么觉得“质量”是个软…

2026/10/15 22:10:40 阅读更多 →
PAT甲级1104:从暴力到O(N)公式,贡献法破解所有连续子段求和与精度坑

PAT甲级1104:从暴力到O(N)公式,贡献法破解所有连续子段求和与精度坑

如果你刷过 PAT 甲级题库,一定见过这道“智力题”味的题目:1104 Sum of Number Segments (20分)。题面只有一句话,意思就是给一串正数,求所有连续子段各自的和,再把所有和加起来。听起来是个人都会做,三层循…

2026/10/15 22:10:40 阅读更多 →
万能BIOS刷新工具实战:SPI Flash刷写与校验救砖全攻略

万能BIOS刷新工具实战:SPI Flash刷写与校验救砖全攻略

简介:一款面向主板BIOS升级场景的刷新工具,适用于需要修复启动故障、提升硬件兼容性或更新固件版本的电脑用户与运维人员。工具宣称“万能”,实际兼容性仍需按主板型号核对,但覆盖常见主板刷新需求。资源包共4个文件,包…

2026/10/15 22:10:40 阅读更多 →
Unity Profiler性能分析实战:从真机采集到GC优化完整指南

Unity Profiler性能分析实战:从真机采集到GC优化完整指南

1. 为什么每个Unity开发者都该把Profiler当日常工具做Unity这几年,我见过太多项目在编辑器里跑得飞起,一打包到真机就卡成幻灯片。帧率从90掉到22,发热烫手,玩家骂声一片,团队却找不到瓶颈在哪。这种场景几乎每个中大型…

2026/10/15 22:09:39 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →