Poller模块的设计与实现
前言在上一文中我们完成了Channel模块的代码设计与实现。通过Channel我们优雅地将文件描述符fd、感兴趣的事件掩码以及对应的业务回调函数绑定在了一起。但是当我们审视Channel的代码时会发现它存在一个致命的“不足”它仅仅是一个内存中的状态管理器它无法真正与操作系统底层进行交互。具体来说当上层代码调用EnableRead()时Channel只是在内部将_events的EPOLLIN标志位置为了 1。但是Linux 内核的epoll红黑树根本不知道这个 fd 的状态发生了改变。Channel自身并没有能力去调用epoll_ctl系统调用也无法主动去执行epoll_wait来感知底层事件的发生。这就引出了我们下一个必须实现的模块——Poller。如果说Channel是网络库中的“事件描述者”那么Poller就是真正的“底层执行者”。我们需要Poller来封装底层的 I/O 多路复用机制如epoll它负责维护epoll实例接收Channel的事件变更请求并真正调用epoll_ctl去更新内核的监控状态。调用epoll_wait阻塞等待事件并在事件就绪时将底层返回的事件精准地“回填”给对应的Channel。接下来我们将深入探讨Poller模块的设计与实现看看它是如何打通Channel与操作系统底层之间的壁垒的。Poller的设计对于一个 Poller 模块我们实现它的核心目的是为了封装底层的 I/O 多路复用机制说的明白点目的就是实现描述符IO事件的监控手段就是通过epoll来进行管理。既然Channel负责管理单个 fd 的事件那么Poller就需要负责管理所有Channel的监控状态。其功能有二添加/修改描述符的事件监控(不存在则添加存在则修改)移除描述符的事件监控那么我们要对哪些行为进行管理呢至少有如下三件事我们需要进行管理创建底层的 epoll 实例epoll_create。向外提供更新事件的接口让外部向 epoll 模型中添加、修改或删除关心的事件集合。开始监控事件将就绪的事件设置回对应的 Channel 里面并返回活跃的 Channel 列表。在底层epoll 只认识文件描述符fd和事件掩码events但我们的上层只认识 Channel 对象。当epoll_wait检测到某些 fd 就绪之后我们需要将就绪的事件添加到对应的 Channel 中而这个过程就需要通过 fd 来找到对应的 Channel。因此我们需要搞一个 fd 和 Channel 的管理器也就是哈希表std::unordered_mapint, Channel*来存储两者之间的映射关系。并且还需要创建一个epoll实例充当操作句柄以及一个struct epoll_event数组监控所有保持活跃的事件。#define MAX_EPOLLEVENTS 1024 class Poller { private: int _epfd; struct epoll_event _evs[MAX_EPOLLEVENTS]; std::unordered_mapint, Channel * _channels; private: // 对epoll的直接操作 void Update(Channel *channel, int op); // 检查这个文件描述符fd是不是第一次出现 bool HasChannel(Channel *channel); public: Poller(); // 添加或修改监控事件 void UpdateEvent(Channel *channel); // 移除监控事件 void RemoveEvent(Channel *channel); // 开始监控返回活跃连接 void Poll(std::vectorChannel* active); };这就是一个整体的Poller框架紧接着我们就来实现他。Poller的实现Update函数接口是我们对epoll_ctl的封装外部传入op选项从而控制update实现不同的功能// 对epoll的直接操作 void Update(Channel *channel, int op) { int fdchannel-Fd(); struct epoll_event ev; ev.data.fdfd; ev.eventschannel-Events(); int retepoll_ctl(_epfd,op,fd,ev); if(ret0) { ERR_LOG(EPOLL_CTL FAILED!); abort();//退出程序 } return; } // 检查这个文件描述符fd是不是第一次出现 bool HasChannel(Channel *channel) { auto it_channels.find(channel-Fd()); if(it_channels.end()) { return false; } return true; }我们这里的update只需要当一个负责调用ctl的工具人就行event事件如何变化应该是Channel内部自己的事情就像是他们自己变了之后我们负责盖章同意。UpdateEvent与RemoveEvent两个接口就是update的两种具体实现分别对应不同场景下的使用// 添加或修改监控事件 void UpdateEvent(Channel *channel) { //先去寻找之前出现过吗出现过就是修改没出现就是新增 bool retHasChannel(channel); if(ret) { return Update(channel,EPOLL_CTL_MOD); } _channels.insert(std::make_pair(channel-Fd(), channel)); return Update(channel,EPOLL_CTL_ADD); } // 移除监控事件 void RemoveEvent(Channel *channel) { auto it_channels.find(channel-Fd()); if(it!_channels.end()) { _channels.erase(it); Update(channel, EPOLL_CTL_DEL); // 只有存在时才去底层删除 } }以上两个接口就是细化了我们使用epoll的两种情况我们仍然继续使用和不再继续使用。细化功能接口之后要实现的就是Poll功能开始监控并通过输出型参数返回活跃的连接// 开始监控返回活跃连接 void Poll(std::vectorChannel * *active) { int nfds epoll_wait(_epfd, _evs, MAX_EPOLLEVENTS, -1); //这里使用阻塞等待就可以了 if (nfds 0) { if (errno EINTR)// 被信号打断不是真错直接返回重新 wait { return; } ERR_LOG(EPOLL WAIT FAILED:%s\n, strerror(errno)); abort(); } for (int i 0; i nfds; i) { auto it _channels.find(_evs[i].data.fd); assert(it ! _channels.end()); it-second-SetREvents(_evs[i].events); // 设置实际就绪的事件这里就更新了Channel中的关注的事件_events active-push_back(it-second); } }在epoll_wait中timeout参数传-1代表无限期阻塞永久等待。我们可以对比一下三种传参方式传0非阻塞模式内核会瞬间返回。如果没有事件nfds为 0。这意味着你的EventLoop会陷入一个死循环不停地疯狂调用epoll_wait把 CPU 占用率直接拉到 100%这就是所谓的“忙轮询”这是绝对不可取的。传 0超时模式比如传1000毫秒。内核最多等 1 秒就返回。这通常用于需要定期执行某些后台任务的场景。但如果你的服务器长时间没有请求它会每秒醒来一次造成不必要的上下文切换和电量消耗。传-1无限期阻塞模式如果没有连接发来数据程序就会安心地“睡”在这里完全不消耗任何 CPU 资源。只有当内核检测到有 fd 触发了事件或者有信号打断EINTR时它才会醒来返回。那么你可能会想“如果它一直睡着那我怎么往里面添加新的监听套接字ListenFd呢”这就引出了网络库设计中最经典的“唤醒机制Wakeup”。在网络库中EventLoop内部通常会创建一个隐藏的管道eventfd或pipe并且把这个管道的读端也注册到epoll中。当你想唤醒这个正在-1睡眠的epoll_wait时只需要往这个管道里写入哪怕 1 个字节的垃圾数据内核就会立刻触发可读事件epoll_wait就会瞬间醒来然后你就可以安全地去添加新的连接或处理新的任务了。最后将构造与析构函数补充完整Poller() { _epfd epoll_create(MAX_EPOLLEVENTS); // 这里的传入参数MAX_EPOLLEVENTS其实是无意义的epollcreate的参数早就失去了意义 if (_epfd 0) { ERR_LOG(EPOLL CREATE FAILED!); abort(); // 退出程序 } } ~Poller() { close(_epfd); }我们的Poller模块的代码基本上就是这样但是我们之前的Channel的代码不是说了有缺陷需要后面与其他模块代码整合一下吗这里我们就可以尝试整合一下了因为这样可以方便我们做一个简单的功能测试Channel源代码class Channel { private: int _fd; // 当前Channel对应的连接的文件描述符 uint32_t _events; // 当前需要监控的事件 uint32_t _revents; // 当前连接触发的事件 using EventCallBack std::functionvoid(); EventCallBack _read_callback; // 可读事件被触发的回调函数 EventCallBack _write_callback; // 可写事件被触发的回调函数 EventCallBack _error_callback; // 错误事件被触发的回调函数 EventCallBack _close_callback; // 连接断开事件被触发的回调函数 EventCallBack _event_callback; // 任意事件被触发的回调函数 public: Channel(int fd) : _fd(fd), _events(0), _revents(0) {} int Fd() { return _fd; } void SetREvents(uint32_t events) { _revents events; } // 设置实际就绪的事件 uint32_t Events() { return _events; } // 当前是否监控了可读 bool ReadAble() { return _events EPOLLIN; } // 当前是否监控了可写 bool WriteAble() { return _events EPOLLOUT; } // 启动读事件监控 void EnableRead() { _events | EPOLLIN; /*后边还会添加到EventLoop事件监控中去*/ } // 启动写事件监控 void EnableWrite() { _events | EPOLLOUT; /*后边还会添加到EventLoop事件监控中去*/ } // 关闭读事件监控 void DisableRead() { _events ~EPOLLIN; /*后边还会修改到EventLoop事件监控中去*/ } // 关闭写事件监控 void DisableWrite() { _events ~EPOLLOUT; /*后边还会修改到EventLoop事件监控中去*/ } // 关闭所有事件监控 void DisableAll() { _events 0; } // 移除事件监控 void Remove() { /*后边还会调用EventLoop接口来移除监控*/ } // 事件处理一旦连接触发了事件就调用这个函数自己触发了什么事件自己怎么处理自己决定 void HandleEvent() // 这个接口是用来分发的上层调用这个就行了不需要挨个调用读写关闭接口 { if ((_revents EPOLLIN) || (_revents EPOLLRDHUP) || (_revents EPOLLPRI)) { if (_read_callback) _read_callback(); } if (_revents EPOLLOUT) { if (_write_callback) _write_callback(); } // 我们这里的读写回调倘若遇见对端关闭的问题不可以在回调中销毁对象否则会出现野指针问题 // 除非使用其他方案进行了特殊处理比如sharedptr等维护安全性 if (_revents EPOLLERR) { if (_error_callback) _error_callback(); } else if (_revents EPOLLHUP) // 实际上不管是EPOLLERR还是EPOLLHUP我们实际的处理都会关闭并移除连接 { if (_close_callback) _close_callback(); } if (_event_callback) _event_callback(); } };怎么整合呢我们这里可以新增一个接口update因为我们之前的操作比如_events ~EPOLLIN;这些操作只是修改了Channel对象内部的一个变量_events。此时Linux 内核里的 epoll 红黑树根本不知道你的意图变了。调用Update()的唯一目的就是立刻把这个变化同步给内核。我们在Channel中新增一个类成员参数Poller对象Poller * _poller;相应的Channel的构造函数也需要改成Channel(Poller * poller,int fd) : _fd(fd), _events(0), _revents(0),_poller(poller) {}那么在update与remove中我们就通过_poller成员调用对应的接口比如updateevent和removeevent就可以了void Update(){return _poller-UpdateEvent(this);} void Remove() { return _poller-RemoveEvent(this); }并在事件监控修改之后调用update与remove// 启动读事件监控 void EnableRead() { _events | EPOLLIN;Update(); } // 启动写事件监控 void EnableWrite() { _events | EPOLLOUT; Update(); } // 关闭读事件监控 void DisableRead() { _events ~EPOLLIN; Update(); } // 关闭写事件监控 void DisableWrite() { _events ~EPOLLOUT; Update(); } // 关闭所有事件监控 void DisableAll() { _events 0;Update(); }这里还有一个小细节由于我们这里用到了poller的接口函数所以如果只是这样的话是会报错的。都在同一个文件的情况下只是用前置声明是不够的啊我虽然声明了但是也只是告诉他有这个类并没有告诉编译器有什么成员所以编译器是会报错的。所以我们应该把update与remove单独拿到poller类的后面单独定义实现在Channel类中我们只需要声明就行void Channel::Update() { return _poller-UpdateEvent(this); } void Channel::Remove() { return _poller-RemoveEvent(this); }另外对于Channel中我们还应该增加各种接口方便外部设置各种回调void SetReadCallback(const EventCallback cb) { _read_callback cb; } void SetWriteCallback(const EventCallback cb) { _write_callback cb; } void SetErrorCallback(const EventCallback cb) { _error_callback cb; } void SetCloseCallback(const EventCallback cb) { _close_callback cb; } void SetEventCallback(const EventCallback cb) { _event_callback cb; }最后二者的具体代码应该如下class Poller; class Channel { private: int _fd; // 当前Channel对应的连接的文件描述符 uint32_t _events; // 当前需要监控的事件 uint32_t _revents; // 当前连接触发的事件 Poller *_poller; using EventCallBack std::functionvoid(); EventCallBack _read_callback; // 可读事件被触发的回调函数 EventCallBack _write_callback; // 可写事件被触发的回调函数 EventCallBack _error_callback; // 错误事件被触发的回调函数 EventCallBack _close_callback; // 连接断开事件被触发的回调函数 EventCallBack _event_callback; // 任意事件被触发的回调函数 public: Channel(Poller *poller, int fd) : _fd(fd), _events(0), _revents(0), _poller(poller) {} int Fd() { return _fd; } void SetREvents(uint32_t events) { _revents events; } // 设置实际就绪的事件 uint32_t Events() { return _events; } void SetReadCallback(const EventCallBack cb) { _read_callback cb; } void SetWriteCallback(const EventCallBack cb) { _write_callback cb; } void SetErrorCallback(const EventCallBack cb) { _error_callback cb; } void SetCloseCallback(const EventCallBack cb) { _close_callback cb; } void SetEventCallback(const EventCallBack cb) { _event_callback cb; } // 当前是否监控了可读 bool ReadAble() { return _events EPOLLIN; } // 当前是否监控了可写 bool WriteAble() { return _events EPOLLOUT; } // 启动读事件监控 void EnableRead() { _events | EPOLLIN; Update(); } // 启动写事件监控 void EnableWrite() { _events | EPOLLOUT; Update(); } // 关闭读事件监控 void DisableRead() { _events ~EPOLLIN; Update(); } // 关闭写事件监控 void DisableWrite() { _events ~EPOLLOUT; Update(); } // 关闭所有事件监控 void DisableAll() { _events 0; Update(); } // 移除事件监控 void Update(); void Remove(); // 事件处理一旦连接触发了事件就调用这个函数自己触发了什么事件自己怎么处理自己决定 void HandleEvent() // 这个接口是用来分发的上层调用这个就行了不需要挨个调用读写关闭接口 { if ((_revents EPOLLIN) || (_revents EPOLLRDHUP) || (_revents EPOLLPRI)) { if (_read_callback) _read_callback(); } if (_revents EPOLLOUT) { if (_write_callback) _write_callback(); } // 我们这里的读写回调倘若遇见对端关闭的问题不可以在回调中销毁对象否则会出现野指针问题 // 除非使用其他方案进行了特殊处理比如sharedptr等维护安全性 if (_revents EPOLLERR) { if (_error_callback) _error_callback(); } else if (_revents EPOLLHUP) // 实际上不管是EPOLLERR还是EPOLLHUP我们实际的处理都会关闭并移除连接 { if (_close_callback) _close_callback(); } if (_event_callback) _event_callback(); } }; #define MAX_EPOLLEVENTS 1024 class Poller { private: int _epfd; struct epoll_event _evs[MAX_EPOLLEVENTS]; std::unordered_mapint, Channel * _channels; private: // 对epoll的直接操作 void Update(Channel *channel, int op) { int fd channel-Fd(); struct epoll_event ev; ev.data.fd fd; ev.events channel-Events(); int ret epoll_ctl(_epfd, op, fd, ev); if (ret 0) { ERR_LOG(EPOLL_CTL FAILED!); abort(); // 退出程序 } return; } // 检查这个文件描述符fd是不是第一次出现 bool HasChannel(Channel *channel) { auto it _channels.find(channel-Fd()); if (it _channels.end()) { return false; } return true; } public: Poller() { _epfd epoll_create(MAX_EPOLLEVENTS); // 这里的传入参数MAX_EPOLLEVENTS其实是无意义的epollcreate的参数早就失去了意义 if (_epfd 0) { ERR_LOG(EPOLL CREATE FAILED!); abort(); // 退出程序 } } // 添加或修改监控事件 void UpdateEvent(Channel *channel) { // 先去寻找之前出现过吗出现过就是修改没出现就是新增 bool ret HasChannel(channel); if (ret) { return Update(channel, EPOLL_CTL_MOD); } _channels.insert(std::make_pair(channel-Fd(), channel)); return Update(channel, EPOLL_CTL_ADD); } // 移除监控事件 void RemoveEvent(Channel *channel) { auto it _channels.find(channel-Fd()); if (it ! _channels.end()) { _channels.erase(it); Update(channel, EPOLL_CTL_DEL); // 只有存在时才去底层删除 } } // 开始监控返回活跃连接 void Poll(std::vectorChannel * *active) { int nfds epoll_wait(_epfd, _evs, MAX_EPOLLEVENTS, -1); // 这里使用阻塞等待就可以了 if (nfds 0) { if (errno EINTR) // 被信号打断不是真错直接返回重新 wait { return; } ERR_LOG(EPOLL WAIT FAILED:%s\n, strerror(errno)); abort(); } for (int i 0; i nfds; i) { auto it _channels.find(_evs[i].data.fd); assert(it ! _channels.end()); it-second-SetREvents(_evs[i].events); // 设置实际就绪的事件这里就更新了Channel中的关注的事件_events active-push_back(it-second); } return; } ~Poller() { close(_epfd); } }; void Channel::Update() { return _poller-UpdateEvent(this); } void Channel::Remove() { return _poller-RemoveEvent(this); }

相关新闻

3步快速集成Android离线人脸识别SDK的完整指南

3步快速集成Android离线人脸识别SDK的完整指南

3步快速集成Android离线人脸识别SDK的完整指南 【免费下载链接】FaceAISDK_Android Android on_device Face Recognition 、 Liveness detection and 1:N & M:N Face Search SDK 端侧可离线人脸识别 活体检测 以及1:N M:N 人脸搜索SDK 项目地址: https://gitcode.com/Gi…

2026/10/3 21:40:21 阅读更多 →
Spring Boot容错机制:@Retryable与@ConcurrencyLimit实战解析

Spring Boot容错机制:@Retryable与@ConcurrencyLimit实战解析

1. Spring Boot容错机制演进与内置注解价值在分布式系统开发中,服务间的调用失败和并发控制是每个开发者必须面对的挑战。传统方案通常需要引入额外的库(如Resilience4j、Hystrix)或手动编写重试逻辑,这不仅增加了项目复杂度&…

2026/10/11 17:22:00 阅读更多 →
Windows安装错误2502/2503终极解决方案:AtlasOS一键修复完全指南

Windows安装错误2502/2503终极解决方案:AtlasOS一键修复完全指南

Windows安装错误2502/2503终极解决方案:AtlasOS一键修复完全指南 【免费下载链接】Atlas 🚀 An open and lightweight modification to Windows, designed to optimize performance, privacy and usability. 项目地址: https://gitcode.com/GitHub_Tre…

2026/10/13 3:23:47 阅读更多 →

最新新闻

SGI-STL空间配置器源码剖析:两级配置器与内存池设计

SGI-STL空间配置器源码剖析:两级配置器与内存池设计

第一次翻开SGI-STL源码时&#xff0c;大部分人都会在<stl_alloc.h>和<stl_uninitialized.h>面前停下来。这两个文件不像vector、list那样能让你立刻看到数据结构的样子&#xff0c;它们做的是更底层的事&#xff1a;在还没构造任何对象之前&#xff0c;先把裸内存准…

2026/10/13 9:01:55 阅读更多 →
基于 SQL Server 的学生选课系统数据库设计:从 ER 图到并发控制完整指南

基于 SQL Server 的学生选课系统数据库设计:从 ER 图到并发控制完整指南

简介&#xff1a;基于 SQL Server 的学生选课系统数据库设计源码与配套文档&#xff0c;面向需要完成课程设计、期末大作业的高校学生&#xff0c;尤其是 SQL Server 初学者。压缩包共 5 个文件&#xff0c;约 138KB&#xff0c;包含 SQL 脚本、Word 详细设计文档、README 说明…

2026/10/13 9:01:55 阅读更多 →
Halcon HObject 转 OpenCVSharp Mat 零拷贝实战与避坑指南

Halcon HObject 转 OpenCVSharp Mat 零拷贝实战与避坑指南

/* 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 9:01:55 阅读更多 →
SQL Server选课系统数据库设计:事务、约束与并发控制实践

SQL Server选课系统数据库设计:事务、约束与并发控制实践

简介&#xff1a;一套基于SQL Server的学生选课系统数据库设计完整方案&#xff0c;面向数据库课程设计、期末大作业以及需要快速上手数据库项目的初学者。项目内含可执行SQL脚本、详细设计文档、Markdown说明与架构截图&#xff0c;覆盖数据表、关系、存储过程等核心内容&…

2026/10/13 9:01:55 阅读更多 →
WinCC用户归档实战:从配方批次追溯到SQL直连的完整案例

WinCC用户归档实战:从配方批次追溯到SQL直连的完整案例

/* 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 9:01:55 阅读更多 →
Java Pattern.compile 原理与性能优化:从编译到复用 Matcher 的实践

Java Pattern.compile 原理与性能优化:从编译到复用 Matcher 的实践

/* 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 9:00:51 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目&#xff0c;我最大的感受是&#xff0c;大部分坑不是出在技术前沿&#xff0c;而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节&#xff0c;最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了&#xff0c;很难不碰到舵机控制这道坎。做智能小车要转方向&#xff0c;做机械臂要抬关节&#xff0c;做云台要偏航俯仰&#xff0c;背后都是同一个东西在干活&#xff1a;舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”&#xff1f;——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台&#xff1f;或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜&#xff1f;又或者&#xff0c;在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 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/12 21:05:09 阅读更多 →
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/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练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/12 18:11:17 阅读更多 →