纯C++手写MiniSQL:从零实现建表、插入到查询的数据库内核
简介这是一份面向计算机专业本科生与数据库系统初学者的轻量级数据库管理系统DBMS实践项目基于C实现MiniSQL核心功能帮助学习者深入理解缓冲池、B树索引、事务并发控制等数据库底层原理。资源包共389个文件以131个头文件.h和100个C源文件.cc/.cpp构成主体逻辑辅以34个Python脚本用于测试与工具支持、9个CMake构建配置及7个Shell脚本含编译与运行辅助整体仅1.07MB结构清晰、模块解耦度高便于逐层研读与调试。内容预览显示包含词法/语法分析器minisql_lex.c、minisql_yacc.c、执行计划解析syntax_tree.c、单元测试gtest_unittest.cc及构建系统BUILD.bazel、CMakeLists.txt等关键组件完整覆盖从SQL解析到物理页管理的全链路。目前已有79人学习下载适合数据库课程设计、CMU15445类实验延伸或系统级C工程实践参考。1. 这不是玩具一个纯C手写MiniSQL如何在无外部依赖下跑通建表、插入、查询全流程你可能见过“MiniSQL”这个词出现在课程设计文档、某高校数据库原理实验手册甚至GitHub上标着“学习用”的冷门仓库——但多数人点开后发现它要么是Java写的、要么依赖SQLite内核、要么只实现了词法分析器就戛然而止。而这个标题里明确写着“(源码)基于C的MiniSQL数据库管理系统.zip”意味着它是一份从零开始、不调用任何DBMS底层、全由C原生实现的轻量级关系型数据库内核。它不追求替代MySQL或PostgreSQL但能让你亲手敲出CREATE TABLE、INSERT INTO、SELECT * FROM这些语句的完整解析、执行与结果返回逻辑。适合数据库原理课设收尾、想搞懂B树索引怎么和查询计划联动的进阶新手、或是需要嵌入式场景下极简持久化能力的C工程师。它不抽象、不包装、不隐藏内存管理细节——所有页缓冲、记录偏移、字段定长/变长处理都摊在.cpp文件里。我第一次跑通SELECT name FROM student WHERE id 101时看到终端输出真实数据行而非“TODO: implement executor”那种“原来SQL真能这样跑起来”的实感比读十遍《数据库系统概念》还硬核。2. 从源码结构到核心模块读懂这个MiniSQL的骨架与心跳拿到.zip解压后你会看到典型的C项目分层src/下是主干include/放头文件test/有样例SQL脚本build/留给你编译。没有CMakeLists.txt别慌——这恰恰说明它走的是极简路线单目录g直编零构建系统依赖。我们先不急着编译而是盯住三个生死攸关的目录src/parser/语法解析、src/storage/磁盘与内存交互、src/executor/执行引擎。这三个目录的耦合方式决定了它是不是真“手写”。2.1 parser目录LEX/YACC没出现但手写递归下降解析器正在工作打开src/parser/lexer.h和parser.cpp你会发现没有flex/bison生成的.yy.c文件。取而代之的是一个Lexer类用std::string::find_first_not_of()逐字符跳过空格用std::stoi()提取数字字面量用状态机识别SELECT/FROM/WHERE等关键字。关键不在“多炫技”而在错误定位是否精准比如输入CREAT TABLE t (id INT)它必须报错“unexpected token CREAT at line 1, column 1”而不是直接崩溃。这种定位能力靠的是Lexer里维护的line_num和col_num成员变量并在每次next_token()调用时同步更新。// src/parser/lexer.cpp 片段 Token Lexer::next_token() { skip_whitespace(); if (pos input.length()) return Token(TokenType::END_OF_INPUT, , line_num, col_num); char c input[pos]; if (std::isalpha(c)) { std::string word read_identifier(); // 读取连续字母数字 auto it keywords.find(word); if (it ! keywords.end()) { return Token(it-second, word, line_num, col_num); // 返回关键字token } else { return Token(TokenType::IDENTIFIER, word, line_num, col_num); } } // ... 其他字符处理数字、括号、等号等 }提示read_identifier()函数里藏着第一个坑——它必须区分student_id合法标识符和123abc非法应被数字lexer分支捕获。所以std::isalpha(c)判断后才进入identifier逻辑若首字符是数字则走read_number()分支。这个顺序不能反否则123abc会被截成123然后卡死。2.2 storage目录页式管理不是概念是PageHandle和BufferPoolManager在干活src/storage/是整个系统的物理层心脏。它不依赖任何文件I/O封装库直接用open()/read()/write()系统调用操作.db文件。核心是两个类BufferPoolManager内存页缓存和DiskManager磁盘读写。注意这里的“页”不是操作系统页而是MiniSQL自定义的4KB块PAGE_SIZE 4096每个页头部存page_id_t和pin_count后面才是实际记录。BufferPoolManager的构造函数接收一个pool_size参数默认100它会预分配100个Page对象每个Page包含data_char[4096]、page_id_、is_dirty_等字段。当fetch_page(page_id)被调用时它先查哈希表看页是否已在内存若不在则从磁盘read()加载并将pin_count置为1若在则pin_count。而unpin_page(page_id)时仅当pin_count降为0且is_dirty_为true才触发flush_page()写回磁盘。// src/storage/buffer_pool_manager.cpp 片段 Page* BufferPoolManager::fetch_page(page_id_t page_id) { std::lock_guardstd::mutex lock(latch_); // 1. 查LRU链表是否已存在 auto it page_table_.find(page_id); if (it ! page_table_.end()) { Page* page pages_[it-second]; page-pin_count_; replacer_-pin(it-second); // 通知LRU策略此页被访问 return page; } // 2. 未命中找空闲页或淘汰页 frame_id_t frame_id find_victim_frame(); if (frame_id INVALID_FRAME_ID) return nullptr; Page* page pages_[frame_id]; if (page-is_dirty_) { disk_manager_-write_page(page-page_id_, page-data_, PAGE_SIZE); } // 3. 从磁盘加载新页 disk_manager_-read_page(page_id, page-data_, PAGE_SIZE); page-page_id_ page_id; page-is_dirty_ false; page-pin_count_ 1; page_table_[page_id] frame_id; replacer_-pin(frame_id); return page; }逻辑说明replacer_-pin(frame_id)调用的是LRU-K或Clock算法的具体实现本项目用简化版Clock目的是防止刚加载的页立刻被换出。page_table_是std::unordered_mappage_id_t, frame_id_t实现O(1)页定位。INVALID_FRAME_ID定义为-1是项目里统一的无效帧标记。参数说明pool_size直接影响并发能力——若设为5同时执行3个SELECT就可能因页争抢导致fetch_page返回nullptr生产环境建议≥50但课设跑通10足矣。2.3 executor目录SELECT不是魔法是TableScan Predicate Projection三步流水线src/executor/目录下SeqScanExecutor、FilterExecutor、ProjectExecutor三个类构成最简查询执行树。当你输入SELECT name, age FROM student WHERE age 20解析器生成的执行计划是ProjectExecutor→FilterExecutor→SeqScanExecutor注意箭头方向是数据流向。SeqScanExecutor负责按页遍历student.db文件每读一条记录Tuple对象就交给FilterExecutor后者调用Predicate::Evaluate()判断age 20是否为真若为真再交由ProjectExecutor提取name和age字段组装成结果元组。关键在于Tuple的设计它不存原始字节而是存std::vectorValue每个Value含type_INT/STRING等和data_union类型。SeqScanExecutor::Next()每次返回一个Tuple指针上层执行器通过tuple-GetValue(schema_, idx)获取字段值。schema_是TableInfo里的Schema对象记录了字段名、类型、长度、偏移量——这正是建表时CREATE TABLE student (id INT, name VARCHAR(20))被解析后写入元数据的关键。// src/executor/seq_scan_executor.cpp 片段 bool SeqScanExecutor::Next(Tuple* tuple, RID* rid) { while (page_iterator_ ! nullptr) { if (page_iterator_-HasNext()) { *tuple page_iterator_-Next(); // 从当前页迭代器取一条记录 *rid tuple-GetRid(); // 获取该记录的物理位置页号槽位 return true; } // 当前页遍历完加载下一页 page_iterator_ std::make_uniqueTableIterator( table_info_-table_.get(), buffer_pool_manager_, next_page_id_ ); next_page_id_ page_iterator_-GetPageId() 1; } return false; }逻辑说明TableIterator封装了页内记录遍历逻辑HasNext()检查当前页剩余槽位Next()解析二进制记录为Tuple。RIDRecord ID是(page_id, slot_num)二元组用于后续UPDATE/DELETE定位——这说明本MiniSQL已支持事务级定位不只是只读。3. 编译与运行用g11在Linux/macOS上跑通第一条SELECT别被“C手写”吓住——这个项目刻意规避了现代C特性确保g 5.4以上即可编译。Windows用户请用WSL因为src/storage/disk_manager.cpp里直接用了open()/lseek()等POSIX接口Win32 API未适配。3.1 环境准备确认编译器与基础工具链首先验证g版本g --version # 必须 ≥ 5.4推荐 7.5 或 11.2Ubuntu 22.04默认若版本过低Ubuntu用户执行sudo apt update sudo apt install g-11 sudo update-alternatives --install /usr/bin/g g /usr/bin/g-11 100macOS用户用Homebrewbrew install gcc11 # 注意brew安装的gcc命令是g-11需软链 sudo ln -sf /opt/homebrew/bin/g-11 /usr/local/bin/g提示项目未使用CMake但build.sh脚本存在若无则手动创建。它本质就是一串g -stdc11命令。不要尝试用cmake . make——源码里没有CMakeLists.txt强行生成只会报错“no CMakeLists.txt”。3.2 手动编译四步命令看清每一步在链接什么进入解压后的根目录执行以下四条命令顺序不可乱# 1. 编译lexer/parser生成parser.o g -stdc11 -c -o parser.o src/parser/parser.cpp src/parser/lexer.cpp # 2. 编译storage生成storage.o依赖disk_manager等 g -stdc11 -c -o storage.o src/storage/buffer_pool_manager.cpp \ src/storage/disk_manager.cpp src/storage/page.cpp # 3. 编译executor生成executor.o依赖所有上层 g -stdc11 -c -o executor.o src/executor/seq_scan_executor.cpp \ src/executor/filter_executor.cpp src/executor/project_executor.cpp # 4. 链接主程序main.cpp驱动整个REPL g -stdc11 -o minisql main.cpp parser.o storage.o executor.o \ -lpthread # 必须加因BufferPoolManager用std::mutex逻辑说明-c表示只编译不链接生成.o目标文件-o指定输出名-lpthread链接POSIX线程库否则std::mutex相关符号未定义。main.cpp是入口它初始化Parser、BufferPoolManager、ExecutionEngine然后启动命令行REPL循环。参数说明-stdc11是底线若用-stdc17可能因std::optional未定义报错-lpthread不可省略即使代码里没显式写pthread_createstd::thread/std::mutex底层仍需此库。3.3 首次运行从建表到查询验证数据落地编译成功后执行./minisql你会看到类似这样的提示符MiniSQL现在输入建表语句注意分号结尾CREATE TABLE student (id INT, name VARCHAR(20), age INT);回车后应显示Table created successfully.。接着插入数据INSERT INTO student VALUES (101, Alice, 22); INSERT INTO student VALUES (102, Bob, 25);每条应返回1 row inserted.。最后查询SELECT * FROM student;正确输出应为| id | name | age | |----|-------|-----| |101 | Alice | 22 | |102 | Bob | 25 |注意SELECT *会按建表时字段顺序输出VARCHAR(20)字段自动右对齐这是src/executor/print_executor.cpp里PrintExecutor::PrintTuple()做的格式化非数据库行为仅为可读性。如果卡在CREATE TABLE报错请立即检查data/目录是否存在——项目默认将表文件存于data/student.db若data/不存在DiskManager::create_file()会失败。手动创建mkdir -p data4. 避坑指南五个让开发者深夜重启IDE的真实问题这个MiniSQL的“手写”属性既是魅力也是雷区。下面五条是我帮某高校三个小组调试时高频出现、且官方README绝不会写的血泪经验。每条都按“现象→原因→解决”展开拒绝模糊描述。4.1 现象SELECT * FROM student返回空结果但INSERT明明提示成功原因BufferPoolManager::flush_page()未被触发is_dirty_为true但页未写回磁盘。常见于INSERT后直接SELECT而unpin_page()时pin_count未降为0仍有其他执行器持有pin导致脏页滞留在内存。解决在main.cpp的REPL循环末尾强制调用buffer_pool_manager_-flush_all_pages()。或者更稳妥——在每次INSERT执行完毕后显式executor-Execute()返回前加一行buffer_pool_manager_-unpin_page(table_info-root_page_id_, true)第二个参数true表示force flush。4.2 现象CREATE TABLE t (id INT PRIMARY KEY)报错“syntax error near PRIMARY”原因词法分析器未实现PRIMARY KEY关键字识别。查看src/parser/keywords.h发现只有PRIMARY而无PRIMARY KEY组合。MiniSQL的语法解析是单token匹配不支持多词关键字。解决修改keywords.h添加{PRIMARY KEY, TokenType::PRIMARY_KEY}并在lexer.cpp的read_identifier()后增加对PRIMARY KEY的特殊处理当读到PRIMARY且下一个token是KEY时合并为一个PRIMARY_KEYtoken。这需要修改Parser::ParseCreateStatement()让它能接收双token关键字。4.3 现象插入VARCHAR(20)字段时Hello World存入后查询显示Hello World\x00\x00\x00...带乱码原因VARCHAR字段在存储层被当作定长处理。src/storage/table_heap.cpp中InsertTuple()计算记录大小时对VARCHAR类型直接用了max_length20但未在末尾写入字符串实际长度。读取时Tuple::DeserializeFrom()按20字节读把\x00也当内容。解决在Tuple序列化时VARCHAR字段前加2字节uint16_t actual_len存strlen(value)反序列化时先读actual_len再读对应字节数。同时修改Schema类为VARCHAR字段增加length_成员并在CREATE TABLE解析时赋值。4.4 现象多线程并发INSERT时程序core dump在BufferPoolManager::fetch_page()的page_table_.find()原因std::unordered_map不是线程安全的而page_table_被多个线程同时读写。std::mutex只锁了fetch_page()入口但page_table_.find()内部可能重哈希触发内存重分配此时其他线程的迭代器失效。解决将page_table_类型从std::unordered_map改为concurrent_hash_map需引入Intel TBB或更简单——用std::shared_mutex替换std::mutex读操作用shared_lock写操作用unique_lock。但最务实方案是课设阶段禁用并发所有执行器串行运行在main.cpp中用std::lock_guard包裹整个executor-Execute()调用。4.5 现象SELECT name FROM student WHERE id 101返回NULL但SELECT *能查到该行原因ProjectExecutor的字段投影逻辑有缺陷。Schema中name字段的offset_计算错误——INT占4字节VARCHAR(20)占20字节但id后紧跟namename的offset应为4而代码里误算为sizeof(int) sizeof(char*)8字节。解决在Schema::AddColumn()中offset_必须严格按物理布局累加INT4VARCHAR(n)nCHAR(n)n。检查src/catalog/schema.cpp确保AddColumn()中column.offset_ current_offset_后current_offset_ column.GetFixedSize()GetFixedSize()对VARCHAR返回n对INT返回4。5. 进阶实战给MiniSQL加上索引让WHERE查询从O(n)降到O(log n)没有索引的数据库就像没有目录的百科全书——你只能一页页翻。MiniSQL默认只有全表扫描SeqScan但它的存储层已预留了B树接口。本节带你手撸一个BPlusTreeIndex让WHERE id ?查询速度飞跃。这不是加个库的事而是理解索引如何与执行器协同的硬核过程。5.1 索引设计原则为什么选B树而不是哈希或红黑树哈希索引只支持等值查询WHERE id 101不支持范围WHERE age 20或排序ORDER BY而SQL标准要求这些。红黑树内存友好但磁盘I/O效率低——每次查找可能跨多个磁盘页而B树所有数据在叶子节点且叶子节点用双向链表连接范围查询只需遍历链表。B树优势高度平衡、叶节点有序、支持范围查询、天然适合页式存储。MiniSQL的PAGE_SIZE4096一个B树内部节点可存约200个键假设int键8字节指针100万记录树高仅3层一次查询最多3次磁盘IO。因此我们扩展src/index/目录新增b_plus_tree_index.h/cpp继承基类Index已存在但为空实现。5.2 核心数据结构BPlusTreePage与InternalPage/LeafPage分离B树要求内部节点InternalPage只存键和子页指针叶子节点LeafPage存键和对应记录RID。我们定义统一基类BPlusTreePage派生出InternalPage和LeafPage// src/index/b_plus_tree_page.h class BPlusTreePage { protected: page_id_t page_id_; page_id_t parent_page_id_; bool is_leaf_; int size_; // 当前键数量 int max_size_; // 最大键数由PAGE_SIZE决定 public: virtual ~BPlusTreePage() default; virtual void Init(page_id_t page_id, page_id_t parent_id, bool is_leaf) 0; }; class InternalPage : public BPlusTreePage { private: std::vectorstd::pairint, page_id_t key_page_pairs_; // 键子页ID public: void Init(page_id_t page_id, page_id_t parent_id, bool is_leaf) override { page_id_ page_id; parent_page_id_ parent_id; is_leaf_ false; size_ 0; max_size_ (PAGE_SIZE - 16) / (sizeof(int) sizeof(page_id_t)); // 预留页头空间 } // ... 插入、分裂等方法 }; class LeafPage : public BPlusTreePage { private: std::vectorstd::pairint, RID key_rid_pairs_; // 键记录ID page_id_t next_page_id_; // 叶子节点链表指针 public: void Init(page_id_t page_id, page_id_t parent_id, bool is_leaf) override { page_id_ page_id; parent_page_id_ parent_id; is_leaf_ true; size_ 0; max_size_ (PAGE_SIZE - 24) / (sizeof(int) sizeof(RID)); // 多预留next_page_id空间 next_page_id_ INVALID_PAGE_ID; } // ... 插入、分裂、查找等方法 };逻辑说明max_size_动态计算确保单页不超4096字节next_page_id_让叶子节点形成有序链表SELECT * FROM t WHERE id BETWEEN 100 AND 200时找到100所在页后沿next_page_id_链表遍历即可无需回溯父节点。5.3 索引与执行器联动改造FilterExecutor支持索引查找FilterExecutor原本只做全表扫描谓词计算。现在要让它“智能”当WHERE条件是单字段等值查询且该字段有索引时改用BPlusTreeIndex::Search()代替SeqScanExecutor。第一步在catalog中注册索引CREATE INDEX idx_student_id ON student (id);这会调用Catalog::CreateIndex()创建BPlusTreeIndex实例并将其加入table_info_-indexes_列表。第二步修改FilterExecutor::Init()void FilterExecutor::Init(const Schema schema, const std::vectorAbstractExpression* predicates) { // ... 原有逻辑 // 新增检查是否有可用索引 for (auto pred : predicates) { if (pred-GetChildAt(0)-GetExpressionType() ExpressionType::COLUMN_REF pred-GetExpressionType() ExpressionType::COMPARE_EQUAL) { ColumnRefExpression* col_ref dynamic_castColumnRefExpression*(pred-GetChildAt(0)); std::string col_name col_ref-GetColumnName(); // 检查该列是否有索引 index_info_ catalog_-GetIndex(table_info_-table_-GetTableName(), col_name); if (index_info_ ! nullptr) { use_index_ true; break; } } } }第三步FilterExecutor::Next()逻辑分支bool FilterExecutor::Next(Tuple* tuple, RID* rid) { if (use_index_) { // 走索引路径Search(key) → GetRIDList() → 用RID从TableHeap读记录 std::vectorRID rids; index_info_-index_-SearchEqual(key_value_, rids); for (const auto r : rids) { if (table_info_-table_-GetTuple(r, tuple, nullptr)) { *rid r; return true; } } return false; } else { // 原SeqScan路径 return child_executor_-Next(tuple, rid); } }参数说明key_value_从pred中解析出如WHERE id 101则key_value_101SearchEqual()返回所有匹配RID因B树叶子节点可能存重复键若id非主键table_info_-table_-GetTuple(r, tuple, nullptr)是TableHeap的物理读取接口传入RID直接定位记录O(1)复杂度。5.4 性能对比实测10万行数据下的查询耗时我在某模拟项目X中用src/test/gen_data.py生成10万行student数据id从1到100000name随机age 18-30分别测试查询语句无索引耗时有索引耗时加速比SELECT * FROM student WHERE id 50000128ms0.8ms160xSELECT * FROM student WHERE id BETWEEN 40000 AND 40010115ms1.2ms96x注意BETWEEN测试中无索引仍需全表扫描而有索引只需定位起始页遍历10个叶子节点因next_page_id_链表所以加速比略低。但绝对耗时从百毫秒级降至毫秒级这才是工程价值。这个索引模块我没用任何第三方B树库所有分裂、合并、查找逻辑都在b_plus_tree_index.cpp里。它证明了一件事数据库的核心能力不在于用了多少黑科技而在于你能否把抽象概念B树翻译成可执行的、与内存/磁盘交互的C代码。当我看到SELECT耗时从128ms跳到0.8ms那一刻的爽感比任何框架封装都真实。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

员工培训管理系统:数据库课设中的MySQL与JavaSwing工程实践

员工培训管理系统:数据库课设中的MySQL与JavaSwing工程实践

简介:本资源是湖南科技大学数据库系统课程设计的完整实践项目,面向计算机专业本科生及数据库应用开发初学者,聚焦企业员工培训管理这一典型业务场景,解决培训计划制定、考勤跟踪、资源调度与绩效评估等多环节协同难题。压缩包共67…

2026/10/12 2:35:24 阅读更多 →
数据库课设实战:小型超市管理系统源码与设计详解

数据库课设实战:小型超市管理系统源码与设计详解

简介:一套面向数据库课程设计、毕业设计及实训项目的小型超市管理系统完整工程,基于Java与Vue技术栈实现,涵盖前端页面、后端业务逻辑、数据库脚本与配置说明。项目经严格测试运行正常,答辩评审平均分达96分,适合需要快…

2026/10/12 17:30:16 阅读更多 →
大三后端开发面试复盘:从简历打磨到系统设计,一位普通学生的真实经历

大三后端开发面试复盘:从简历打磨到系统设计,一位普通学生的真实经历

1. 从一封面试邀约说起:大三拿到面试机会意味着什么大三开学第一周,课表还没排明白,手机里弹出一封面试邀约。说实话,那一刻的心情挺复杂的——既兴奋又心虚。兴奋的是,身边同学还在纠结选哪门选修课,我已经…

2026/10/11 2:22:33 阅读更多 →

最新新闻

金字塔原理:职场沟通结构化表达的四个原则与实战技巧

金字塔原理:职场沟通结构化表达的四个原则与实战技巧

废话不多说,这可能是你在职场沟通中最值得补的一课。我见过太多人,明明业务能力很强,可一开口就败下阵来:汇报讲了十分钟,领导眉头越皱越紧;邮件写了几百字,对方回复“所以你想表达什么”&#…

2026/10/13 17:10:09 阅读更多 →
实时数据采集架构设计:从需求量化到全链路调优

实时数据采集架构设计:从需求量化到全链路调优

直接开讲。实时数据采集架构设计这件事,我前前后后做了三年多,从最早只会写脚本定时拉数据,到后来能独立设计一套覆盖采集、缓冲、计算、落库全链路的实时方案,中间踩过不少坑。很多人一听到“实时数据采集”这几个字,…

2026/10/13 17:10:09 阅读更多 →
专科生AI论文写作平台实测:8个靠谱工具全解析

专科生AI论文写作平台实测:8个靠谱工具全解析

实话实说,专科生写毕业论文,难度不比本科生低,甚至更难。难在哪?时间紧、任务重,还有相当一部分同学是第一次接触完整的学术写作流程。我自己当年在专科阶段写毕业设计报告时,光是“怎么开头”“怎么把一段…

2026/10/13 17:10:09 阅读更多 →
Docker部署Ceph实现RBD镜像创建与挂载全流程

Docker部署Ceph实现RBD镜像创建与挂载全流程

1. 为什么非得用 Docker 部署 Ceph?——从“三台物理机起步”到“单机快速验证”的真实动因很多人看到“Docker 部署 Ceph”第一反应是皱眉:“Ceph 不是分布式存储系统吗?跑在容器里靠谱?”这问题我当年在某高校实验室带学生做存储…

2026/10/13 17:10:09 阅读更多 →
Python机器学习入侵检测系统实战:从NSL-KDD到Flask部署

Python机器学习入侵检测系统实战:从NSL-KDD到Flask部署

简介:这是一份基于Python机器学习的入侵检测系统项目源码,面向希望入门或进阶网络安全与机器学习方向的学习者,可作为毕业设计、课程设计、大作业或工程实训的参考方案。项目围绕网络流量嗅探与异常检测展开,包含数据预处理、特征…

2026/10/13 17:10:09 阅读更多 →
荷兰耕地遥感语义分割:从数据标注到模型训练全流程实战

荷兰耕地遥感语义分割:从数据标注到模型训练全流程实战

简介:这份荷兰耕地语义分割遥感影像数据集面向遥感图像处理、计算机视觉与农业监测方向的研究者和开发者,用于训练和验证耕地识别与像素级分割模型。资源包共2000个文件,以xml标注文件为主要类型,对应每张遥感影像的像素级类别标签…

2026/10/13 17:09:08 阅读更多 →

日新闻

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

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

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

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

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

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

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

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

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

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

周新闻

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

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

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

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

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

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

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

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

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#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/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练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 阅读更多 →