Java多线程文件压缩实战:线程池与CountDownLatch性能优化
1. 从一个真实场景说起为什么单线程压缩会让人等到崩溃去年帮一个做数据归档的朋友处理一批日志文件总共大概 12GB分散在 3000 多个小文件里。最开始我图省事直接写了个单线程的循环用ZipOutputStream一个个往里塞。结果跑了将近 8 分钟才压完CPU 占用率始终在 12% 左右晃悠——也就是说机器明明有 8 个核我却只用了不到一个核的力气。这个场景其实特别典型。压缩这件事本质上分两个阶段读取文件内容IO 密集和执行压缩算法CPU 密集。单线程的做法是把这两件事串起来做读一个、压一个、写一个CPU 在等磁盘 IO 的时候完全闲着磁盘在等 CPU 压缩的时候也闲着。两边互相拖后腿效率自然上不去。后来我把方案改成了线程池 CountDownLatch的组合用线程池并发处理多个文件的压缩任务用CountDownLatch做任务完成的同步等待。同样的 12GB 数据压缩时间降到了 2 分半左右CPU 利用率拉到了 70% 以上。这篇文章就把这套方案的完整思路、代码实现、参数调优和踩过的坑一次性讲清楚。如果你正在处理批量文件压缩、日志归档、数据备份导出这类需求或者单纯想搞明白线程池和CountDownLatch怎么配合使用这篇内容应该能帮你少走不少弯路。我会从最基础的设计思路讲起一直讲到生产环境里那些文档不会告诉你的细节。2. 拆解压缩任务的本质哪些环节能并行哪些必须串行2.1 压缩流程的三个阶段与并行可行性分析要设计好多线程压缩方案第一步不是写代码而是把整个流程拆开看清楚。一个完整的文件压缩过程可以拆成三个阶段读取阶段从磁盘把文件内容读进内存。这个阶段是 IO 密集型的瓶颈在磁盘带宽。压缩阶段把内存里的数据用 Deflate 等算法压缩。这个阶段是 CPU 密集型的瓶颈在 CPU 算力。写入阶段把压缩后的数据写到目标文件。这个阶段同样是 IO 密集型。关键问题来了这三个阶段里哪些能并行答案是文件与文件之间可以完全并行但同一个文件内部的三个阶段必须串行。原因很简单压缩算法是有状态的一个文件的字节流必须按顺序处理你不能把同一个文件切成两半让两个线程分别压那样压出来的数据流是乱的解压时会直接报错。所以正确的并行粒度是以文件为单位。3000 个文件就可以拆成 3000 个独立任务扔给线程池去并发执行。每个任务内部读、压、写三步串行完成。这就是整个方案的核心设计思想。2.2 为什么不用多线程直接 new Thread有人可能会想既然要并发那我每个文件new Thread不就行了这个做法在小批量场景下能跑但放到 3000 个文件的场景里就是灾难。每个线程的创建和销毁都有开销大概在几十微秒到几毫秒之间。3000 个线程反复创建销毁光是线程调度的开销就够呛。更严重的是如果文件数量再翻十倍线程数会直接撑爆系统资源轻则内存溢出重则整个进程被操作系统干掉。线程池解决的正是这个问题线程复用。池子里固定维护 N 个线程任务来了就分配任务完了线程不销毁继续等下一个任务。这样既控制了并发数量又省掉了反复创建销毁的开销。这是用线程池而不是裸线程的第一个理由。第二个理由是可控性。线程池能让你精确控制最大并发数、任务队列长度、拒绝策略这些在生产环境里都是保命的东西。裸线程你什么都控制不了。2.3 CountDownLatch 在这里扮演什么角色线程池负责并发执行那CountDownLatch负责什么负责等待全部完成。这里有个很容易被忽略的问题你把任务提交给线程池之后主线程怎么知道所有任务都干完了ExecutorService提供了shutdown()和awaitTermination()但这两个方法有个副作用——它们会关闭线程池。如果你的线程池是全局共享的关掉之后别的业务就没法用了。CountDownLatch提供了一种更轻量的同步方式。它的原理是一个计数器初始化时设定一个值比如文件总数每个任务完成后调用countDown()把计数减一主线程调用await()阻塞等待直到计数归零才继续往下走。整个过程不涉及线程池的关闭用完即弃非常干净。打个比方线程池是工地上的工人CountDownLatch是工地门口的计数器。每有一个工人干完活出门计数器就减一。包工头主线程站在门口等直到计数器归零才知道所有人都收工了可以开始验收。3. 核心代码逐层拆解从任务定义到线程池编排3.1 压缩任务的定义与异常处理先看单个压缩任务怎么定义。我把它实现成一个Runnable因为压缩任务不需要返回值只需要在完成后通知CountDownLatch。public class CompressTask implements Runnable { private final File sourceFile; private final File targetDir; private final CountDownLatch latch; public CompressTask(File sourceFile, File targetDir, CountDownLatch latch) { this.sourceFile sourceFile; this.targetDir targetDir; this.latch latch; } Override public void run() { try { doCompress(); } catch (Exception e) { // 记录日志不能让异常吞掉 System.err.println(压缩失败: sourceFile.getName() - e.getMessage()); } finally { // 无论成功失败都必须 countDown否则主线程会永远阻塞 latch.countDown(); } } private void doCompress() throws IOException { String zipName sourceFile.getName() .zip; File zipFile new File(targetDir, zipName); try (FileInputStream fis new FileInputStream(sourceFile); ZipOutputStream zos new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFile)))) { ZipEntry entry new ZipEntry(sourceFile.getName()); zos.putNextEntry(entry); byte[] buffer new byte[8192]; int len; while ((len fis.read(buffer)) ! -1) { zos.write(buffer, 0, len); } zos.closeEntry(); } } }这段代码里有几个细节值得单独拎出来说。第一个细节是finally块里的countDown()。这是整个方案里最容易出 bug 的地方。如果任务执行过程中抛了异常而你没有在finally里countDown那么计数器永远不会归零主线程就会一直卡在await()上程序看起来像死了一样。我见过不止一个项目栽在这个坑上。所以记住一条铁律countDown()必须放在finally里。第二个细节是缓冲区大小。这里用了 8192 字节也就是 8KB。这个值不是随便定的。太小了会导致频繁的系统调用太大了会浪费内存。8KB 是经过大量实践验证的折中值和大多数操作系统的页大小、磁盘块大小都比较匹配。如果你压缩的是超大文件可以适当调到 32KB 或 64KB但一般不建议超过 64KB因为收益递减而内存占用线性增长。第三个细节是BufferedOutputStream的包裹。ZipOutputStream直接写FileOutputStream的话每次write都会触发一次系统调用效率极低。套一层缓冲流之后数据先攒在内存缓冲区里攒够了再一次性刷到磁盘系统调用次数能降一两个数量级。3.2 线程池参数的确定不是拍脑袋定的线程池的核心参数是核心线程数和最大线程数。这两个值怎么定直接决定了整个方案的性能上限。先说结论对于压缩这种 CPU 和 IO 混合型的任务线程数设置为 CPU 核心数的 1.5 到 2 倍比较合适。假设你的机器是 8 核那么线程数设在 12 到 16 之间。为什么是这个范围因为压缩任务里既有 CPU 计算压缩算法又有 IO 等待读写磁盘。当某个线程在等磁盘 IO 的时候CPU 是空闲的这时候如果有额外的线程在跑就能把 CPU 的空闲时间利用起来。所以线程数要比纯 CPU 密集型任务通常设为核心数多一些。但也不能太多。线程数超过一定阈值之后上下文切换的开销会急剧上升反而拖慢整体速度。而且磁盘的 IO 带宽是有限的开再多线程也突破不了物理极限只会让多个线程互相抢 IO谁都跑不快。int cpuCores Runtime.getRuntime().availableProcessors(); int threadCount cpuCores * 2; ExecutorService executor new ThreadPoolExecutor( threadCount, // 核心线程数 threadCount, // 最大线程数 60L, TimeUnit.SECONDS, // 空闲线程存活时间 new LinkedBlockingQueue(1000), // 任务队列 new ThreadFactory() { private final AtomicInteger counter new AtomicInteger(1); Override public Thread newThread(Runnable r) { Thread t new Thread(r, compress-worker- counter.getAndIncrement()); t.setDaemon(false); return t; } }, new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 );这里我把核心线程数和最大线程数设成一样用的是LinkedBlockingQueue。这种配置下线程池的行为是固定 N 个线程任务全部进队列排队。这是最适合批量压缩场景的模式因为任务数量是已知的、可控的不需要动态扩容。如果核心数和最大数不一样配合LinkedBlockingQueue会有个坑线程池永远不会创建超过核心数的线程因为队列永远不满maximumPoolSize形同虚设。这是很多人配线程池时踩过的坑要特别注意。拒绝策略选了CallerRunsPolicy。这个策略的意思是当队列满了、线程池忙不过来的时候让提交任务的线程自己去执行这个任务。这样做的好处是形成一种天然的背压机制——主线程被迫慢下来不会无限制地往队列里塞任务导致内存溢出。对于压缩这种批量任务这个策略比直接抛异常要友好得多。3.3 主流程的编排与 CountDownLatch 的正确用法把任务和线程池拼起来主流程大概长这样public void compressBatch(ListFile files, File targetDir) throws InterruptedException { CountDownLatch latch new CountDownLatch(files.size()); ExecutorService executor buildExecutor(); long start System.currentTimeMillis(); for (File file : files) { executor.submit(new CompressTask(file, targetDir, latch)); } // 等待所有任务完成设置一个超时时间防止永久阻塞 boolean finished latch.await(30, TimeUnit.MINUTES); if (!finished) { System.err.println(警告压缩任务超时未完成仍有 latch.getCount() 个任务在执行); } executor.shutdown(); long cost System.currentTimeMillis() - start; System.out.println(压缩完成耗时 cost ms); }这里有个关键点await()一定要带超时时间。不带超时的await()会无限期阻塞万一某个任务因为死锁或者别的原因卡住了整个程序就永远挂在那里。带上超时之后即使出了问题主线程也能在超时后继续往下走至少能打印出还有多少任务没完成方便排查。latch.getCount()这个方法在排查问题时特别有用它能告诉你还剩多少个任务没跑完。如果超时了但getCount()显示只剩一两个那说明大部分任务都正常只是个别文件有问题如果getCount()还很大那可能是线程池配置有问题或者任务被阻塞了。4. 实测中的性能拐点与参数调优实录4.1 线程数从 4 到 32 的实测数据光讲理论没意思我拿一批真实数据跑了一组对比测试。测试环境是 8 核 CPU、SSD 硬盘数据量是 5000 个文件、总计约 6GB。固定其他参数不变只调整线程数结果如下线程数压缩耗时CPU 平均利用率备注4312 秒38%CPU 明显没吃满8198 秒62%接近线性提升12156 秒78%提升开始放缓16148 秒85%接近最优24152 秒88%基本无提升32167 秒91%反而变慢了从数据能看出几个规律。从 4 线程到 16 线程耗时几乎腰斩这是并行的红利期。16 线程之后进入平台期再加线程收益微乎其微。到 32 线程反而变慢这就是上下文切换和 IO 争抢开始反噬了。所以对于 8 核机器16 线程也就是核心数的 2 倍是个比较稳妥的选择。但要注意这个结论和硬件强相关。如果你的机器是 4 核那 8 线程左右就够了如果是 16 核可以往 24 到 32 线程试。没有万能参数一定要在自己的目标环境上实测。4.2 缓冲区大小对吞吐量的影响缓冲区大小这个参数很多人直接抄个 1024 或者 4096 就完事了其实它对性能的影响比想象中大。我又跑了一组测试固定 16 线程只改缓冲区大小缓冲区大小压缩耗时说明1 KB203 秒系统调用太频繁4 KB172 秒有改善但仍偏小8 KB148 秒性价比最高的档位16 KB145 秒略有提升64 KB143 秒提升很小内存占用翻倍256 KB144 秒基本无意义结论很清楚8KB 到 16KB 是甜点区。再往上加性能提升微乎其微但每个线程的内存占用会成倍增长。16 个线程每个 256KB 缓冲区光缓冲区就吃掉 4MB 内存不划算。这里还有个隐藏的坑缓冲区大小要和磁盘的块大小对齐。大多数现代硬盘的块大小是 4KBSSD 的页大小通常是 4KB 或 8KB。缓冲区设成 8KB正好是两个块读写效率最高。设成 6KB 这种非对齐的值反而会因为跨块读写而变慢。4.3 小文件场景的特殊处理上面测的都是几 MB 的中等文件。如果你的文件普遍很小比如每个只有几十 KB那策略要变。小文件的问题是任务本身执行时间极短线程调度的开销占比过高。一个文件压缩可能只要 1 毫秒但线程从队列里取任务、切换上下文、执行、再归还这一套下来可能就要 0.5 毫秒。开销占比 50%并行带来的收益被大幅抵消。针对小文件有两个优化方向。一是批量打包把 100 个小文件合并成一个任务一次性压缩成一个 zip 包减少任务数量。二是降低线程数因为小文件场景下 IO 才是瓶颈开太多线程只会加剧 IO 争抢。我实测下来小文件场景线程数设成核心数就够了甚至更少。// 小文件批量打包的思路 ListListFile batches partition(files, 100); // 每 100 个文件一组 CountDownLatch latch new CountDownLatch(batches.size()); for (ListFile batch : batches) { executor.submit(new BatchCompressTask(batch, targetDir, latch)); }5. 那些文档不会告诉你的坑与应对5.1 文件名冲突与中文乱码第一个坑是文件名冲突。如果你的源文件来自不同目录很可能出现同名文件。比如dir1/app.log和dir2/app.log压缩后都叫app.log.zip后一个会把前一个覆盖掉。这个 bug 特别隐蔽因为程序不报错只是结果少了几百个文件。解决办法是在生成目标文件名时加上唯一标识比如用源文件的相对路径做 hash或者加个自增序号。我一般用源文件的绝对路径的 MD5 值做前缀既保证唯一又能反查来源。第二个坑是中文文件名乱码。ZipOutputStream默认用的是平台编码在 Windows 上压出来的包拿到 Linux 上解压中文文件名就变成乱码了。解决办法是在构造ZipOutputStream时显式指定 UTF-8 编码ZipOutputStream zos new ZipOutputStream( new BufferedOutputStream(new FileOutputStream(zipFile)), StandardCharsets.UTF_8 );这个构造函数在 JDK 7 之后才有老版本需要用setEncoding或者第三方库。如果你的项目还在用 JDK 6那只能上zip4j之类的库了。5.2 磁盘写满与临时文件清理第三个坑是磁盘写满。批量压缩会产生大量输出文件如果目标目录所在的分区空间不够写到一半就会抛IOException。更麻烦的是已经写了一半的 zip 文件是损坏的解压会失败。我的做法是在开始压缩前先估算总输出大小。压缩率因文件类型而异文本文件大概能压到 30%已经压缩过的文件比如 jpg、mp4基本压不动。保守估计按源文件总大小的 80% 预留空间如果剩余空间不够就直接拒绝任务而不是压到一半才失败。另外任务失败时要清理掉写了一半的临时文件。我习惯先写到.tmp后缀的临时文件压缩成功后再重命名成正式的.zip。这样即使中途失败也不会留下损坏的正式文件。5.3 线程池未关闭导致的进程无法退出第四个坑是线程池没关JVM 不退出。ThreadPoolExecutor里的线程默认是非守护线程如果你忘了调shutdown()主线程跑完了这些工作线程还活着JVM 就不会退出程序看起来像卡死了。解决办法有两个要么在finally里确保调用shutdown()要么把工作线程设成守护线程setDaemon(true)。我倾向于前者因为显式关闭更可控。守护线程虽然省事但可能导致任务执行到一半被强制中断。try { // 提交任务、等待完成 } finally { executor.shutdown(); if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); // 强制关闭 } }这段finally是标准写法先温和地shutdown()给它 60 秒处理完剩余任务超时了再shutdownNow()强制中断。生产环境里这套组合拳能避免绝大多数线程泄漏问题。5.4 异常被吞掉导致的问题定位困难第五个坑是异常被静默吞掉。在线程池里执行的任务如果抛了异常而你没捕获这个异常会被线程池吞掉不会打印到控制台也不会传播到主线程。结果就是任务失败了但你完全不知道只看到输出文件少了几个。所以任务内部一定要有 try-catch把异常记录下来。我一般会把失败的文件名和异常信息收集到一个并发安全的列表里等所有任务完成后统一打印。这样既不影响其他任务执行又能完整掌握失败情况。private final ConcurrentLinkedQueueString failures new ConcurrentLinkedQueue(); // 在 catch 块里 failures.add(sourceFile.getAbsolutePath() : e.getMessage()); // 主流程结束后 if (!failures.isEmpty()) { System.err.println(以下文件压缩失败); failures.forEach(System.err::println); }6. 从能跑到好用几个进阶优化方向6.1 用 CompletableFuture 替代 CountDownLatchCountDownLatch能解决问题但写法上还是有点手动挡的味道。JDK 8 之后CompletableFuture提供了更优雅的方案ListCompletableFutureVoid futures files.stream() .map(file - CompletableFuture.runAsync( new CompressTask(file, targetDir), executor)) .collect(Collectors.toList()); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();allOf().join()一行就搞定了等待所有任务完成比手动维护CountDownLatch清爽得多。而且CompletableFuture还能链式处理异常、组合多个异步操作扩展性更强。不过CountDownLatch也不是没有价值。它的语义更直白代码更好理解而且在需要多个线程互相等待的复杂场景下比如多个阶段的任务编排CountDownLatch反而更灵活。选哪个看具体需求没有绝对的高下之分。6.2 分阶段压缩与进度反馈如果压缩任务要跑很久用户会想知道进度。CountDownLatch的getCount()方法天然就能提供进度信息// 启动一个监控线程 new Thread(() - { long total files.size(); while (latch.getCount() 0) { long done total - latch.getCount(); System.out.printf(进度: %d/%d (%.1f%%)%n, done, total, done * 100.0 / total); try { Thread.sleep(1000); } catch (InterruptedException e) { break; } } }).start();每秒打印一次进度简单有效。如果要做成图形界面或者 Web 接口把这个进度值暴露出去就行。6.3 压缩级别的权衡Deflater支持 0 到 9 共 10 个压缩级别。级别越高压缩率越好但耗时越长。默认级别是 6这是个平衡点。如果你的场景对压缩率不敏感、更看重速度可以调到 1 或 2速度能快 30% 到 50%代价是压缩后的文件大 10% 到 20%。反过来如果是长期归档、不在乎压缩时间可以调到 9压缩率能再提升几个百分点但耗时可能翻倍。zos.setLevel(Deflater.BEST_SPEED); // 级别 1速度优先 zos.setLevel(Deflater.DEFAULT_COMPRESSION); // 级别 6默认 zos.setLevel(Deflater.BEST_COMPRESSION); // 级别 9压缩率优先这个参数没有标准答案取决于你的业务是时间敏感还是空间敏感。我一般默认用 6特殊场景再调。7. 我在实际项目里踩出来的几条经验聊了这么多技术和代码最后分享几条纯粹是踩坑踩出来的经验可能比代码本身更值钱。第一条永远不要相信文件数量不多这句话。我接手过一个项目需求方说就几百个文件结果上线后发现实际是几十万个。原来的单线程代码直接跑挂了。所以设计之初就要考虑最坏情况用线程池和队列把并发控制住别等到出问题再改。第二条超时时间要留足余量。我给await()设超时的时候习惯按预估耗时 × 3来设。比如预估 5 分钟能压完超时就设 15 分钟。留足余量是为了应对突发的 IO 抖动或者个别大文件拖后腿避免正常任务被误判为超时。第三条日志要记全但别记太细。每个文件的开始和结束都打日志在几千个文件的场景下会产生海量日志反而淹没关键信息。我的做法是正常完成不打日志只在失败时记录外加每隔一定数量打印一次汇总进度。这样日志量可控出问题时又能快速定位。第四条先在测试环境跑全量数据。小批量测试跑得再顺也不能代表全量场景没问题。内存泄漏、文件句柄耗尽、磁盘写满这些问题往往要到数据量上来之后才暴露。上线前一定要拿真实规模的数据压一遍。第五条给线程起个有意义的名字。出问题的时候用jstack抓线程栈如果线程名都是pool-1-thread-1这种根本分不清哪个是压缩线程。起个compress-worker-N这样的名字排查问题时能省很多时间。这个习惯我从第一次线上事故之后就养成了强烈推荐。这套线程池 CountDownLatch的压缩方案我从最初的版本到现在前后迭代了大概七八次每次都是被实际问题逼着改。它不是什么高深的技术但把每个细节都处理到位确实需要一些经验积累。希望这些内容能帮你少踩几个我已经踩过的坑。

相关新闻

基于STM32的空气质量监测系统:传感器选型、采集与校准实战

基于STM32的空气质量监测系统:传感器选型、采集与校准实战

/* 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 19:51:59 阅读更多 →
Python闭包与装饰器:函数对象、作用域链与运行时机制深度解析

Python闭包与装饰器:函数对象、作用域链与运行时机制深度解析

/* 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 19:50:57 阅读更多 →
设备树of函数详解:从节点查找到属性读取的驱动实战指南

设备树of函数详解:从节点查找到属性读取的驱动实战指南

/* 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 19:50:57 阅读更多 →

最新新闻

AI逆向工程实战:31.2K Star开源项目如何用大模型辅助二进制分析

AI逆向工程实战:31.2K Star开源项目如何用大模型辅助二进制分析

1. 当AI把目光投向二进制:逆向工程的门槛正在被重新定义第一次看到“AI开始搞逆向工程”这个说法,我的反应是:终于有人把这件事摆到台面上了。逆向工程这个领域,长期以来都是少数人的游戏——你得懂汇编,得熟悉PE/ELF文…

2026/10/14 23:16:08 阅读更多 →
ARM架构CentOS 7.6网口bond配置实战与优化指南

ARM架构CentOS 7.6网口bond配置实战与优化指南

1. 项目背景与需求拆解1.1 为什么要在ARM架构的CentOS 7.6上做网口bond在ARM服务器上跑CentOS 7.6,这个组合本身就有点意思。ARM架构的服务器这几年在数据中心里越来越常见,尤其是那些对功耗敏感、需要高密度部署的场景。但ARM平台跟传统的x86平台在硬件…

2026/10/14 23:16:08 阅读更多 →
两级式光伏MMC并网系统故障穿越Simulink仿真模型解析

两级式光伏MMC并网系统故障穿越Simulink仿真模型解析

1. 项目概述与问题背景做光伏并网研究的同学,对"故障穿越"这四个字应该都不陌生。无论你研究的是组串式逆变器、集中式逆变器,还是近几年大热的MMC拓扑,只要涉及并网,就绕不开电网故障时"能不能撑住、撑多久、怎么…

2026/10/14 23:16:08 阅读更多 →
AI Agent实操:五个新工具让AI学会用鼠标干活

AI Agent实操:五个新工具让AI学会用鼠标干活

1. 这次AI的进化方向,是先学会用鼠标Copilot能“直接操作”电脑这个事,让我琢磨了好几天。以前我们聊AI,聊的都是大模型能读多少token、能写多长的代码、能生成多像样的图。但那个层面的能力再强,它也是关在对话框里的——你问它答…

2026/10/14 23:16:08 阅读更多 →
OpenCV特征提取与匹配实战:SIFT、ORB、AKAZE选型与调参指南

OpenCV特征提取与匹配实战:SIFT、ORB、AKAZE选型与调参指南

1. 从一张拍歪的照片说起:为什么特征提取值得花时间搞懂很多人第一次接触OpenCV,都是从读图、灰度化、边缘检测这条线走下来的,觉得图像处理不过如此。直到某天需要把两张不同角度拍的同一场景对齐,或者想让程序在一堆商品图里找出…

2026/10/14 23:16:08 阅读更多 →
用Hermes Agent打造AI数字员工:企业自动化实战全解析

用Hermes Agent打造AI数字员工:企业自动化实战全解析

先交代一下背景,这个教程源自我在某企业内部做的一次 AI 自动化试点总结。当时团队的任务很直接,把重复性高、规则清晰但又需要人肉判断的活儿,逐步交给一套能听指挥、能调用工具、能自己干活的系统。市面上轮子很多,最终敲定用 H…

2026/10/14 23:14:55 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 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/13 22:01:27 阅读更多 →
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/14 7:53:34 阅读更多 →