news 2026/10/3 13:04:59

编译原理实验:手写词法分析器与语法分析器的完整避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
编译原理实验:手写词法分析器与语法分析器的完整避坑指南

简介:这是电子科技大学编译原理课程设计/课程作业的完整资料包,专注于词法分析器和语法分析器的设计与实现,面向正在学习编译器构造、需要完成类似实验的大学生。压缩包共8个文件,其中两个Python源码文件分别承载词法分析与语法分析的核心逻辑,另有文法定义、分词结果、解析树文本、错误报告和变量符号表等辅助文件,完整覆盖从源代码读入到中间表示生成的主要环节;整个压缩包仅4KB,体量虽小但结构清晰,便于逐文件研读。目前已有176人学习下载。借助这份资料,可以直观学习如何利用正则表达式切分关键字、标识符、运算符、常量等词法单元,以及如何依据上下文无关文法采用LL(1)或LR(1)策略构建抽象语法树;同时,错误报告和符号表文件也提供了排查语法错误、管理符号信息的实际思路,对理解编译器前端工作流程、独立完成课程设计或准备考试都很有帮助。

1. 别急着解压:先搞清楚这份资源里的词法分析器和语法分析器在替你干什么

期末前从课程平台拉下来一个“UESTC-编译原理-词法分析器-语法分析器.zip”,解压后一屏的 .c、.h、实验文档和测试样例,第一反应往往是懵的——这不是一个完整编译器,而是把编译前端拆成两个核心实验的课程包:词法分析器负责把源代码字符串切成带类型的Token流,语法分析器负责按文法把Token流组装成语法树或分析树。这份资源能解决的痛点很具体:让你在一学期内不依赖工具链,手写出一个能识别一门迷你语言的编译器前端,并为后续语义分析和中间代码生成留出接口。适合正在赶编译原理实验报告的学生,也适合想补一遍前端底子的从业者。先把“词法”和“语法”这两层漏斗的位置摆正,后面的代码才不会写着写着变成黑匣子。

2. 词法分析器:从字符流到Token流的第一个漏斗

2.1 词法分析器在分析什么:字符流到Token流的映射

词法分析器做的事情,本质是把源代码的裸字符序列char source[]变成一个带类型的Token数组。每个Token至少包含三样东西:类型(标识符、关键字、数字字面量、运算符、界符)、词素(lexeme,即原始文本片段)、以及位置信息(行号和列号)。位置信息看起来不起眼,但后面语法报错全靠它定位,我一般在一开始就把行列号设计进Token结构体里,省得后面返工。

教科书里词法分析的标准路线是“正则表达式 → NFA → DFA → 最小化DFA”,但在课程实验里,绝大多数同学不会真去构造状态转移表,而是用switch/case或if/else写一个简化状态机。这条路完全可行,核心要点就三个:最长匹配、关键字优先、回退字符的处理。理解这三个点,比背一遍子集构造算法更能在实验里救命。

2.2 手写一个最小状态机词法分析器:C语言可直接跑的版本

我见过很多版本的课程代码,结构都大同小异。下面这个是我按实验常见需求整理的最小实现,覆盖标识符、关键字、整数、浮点数、四则运算符和括号,足够支撑一个TINY语言前端。

#include <ctype.h> #include <stdio.h> #include <string.h> #define MAX_LEXEME 64 typedef enum { TK_IDENT, TK_IF, TK_ELSE, TK_WHILE, TK_RETURN, TK_INT_LIT, TK_FLOAT_LIT, TK_PLUS, TK_MINUS, TK_STAR, TK_SLASH, TK_LPAREN, TK_RPAREN, TK_LBRACE, TK_RBRACE, TK_SEMI, TK_EOF, TK_ERROR } TokenType; typedef struct { TokenType type; char lexeme[MAX_LEXEME]; int line; int col; } Token; static const char *type_names[] = { "IDENT", "IF", "ELSE", "WHILE", "RETURN", "INT_LIT", "FLOAT_LIT", "PLUS", "MINUS", "STAR", "SLASH", "LPAREN", "RPAREN", "LBRACE", "RBRACE", "SEMI", "EOF", "ERROR" }; Token get_token(FILE *fp, int *line, int *col) { int c; Token tok; memset(&tok, 0, sizeof(tok)); tok.line = *line; tok.col = *col; // 跳过空白与单行注释 for (;;) { c = fgetc(fp); if (c == '\n') { (*line)++; *col = 1; } else if (isspace(c)) { (*col)++; } else if (c == '/' && (c = fgetc(fp)) == '/') { while (c != '\n' && c != EOF) c = fgetc(fp); if (c == '\n') { (*line)++; *col = 1; } } else { ungetc(c, fp); break; } if (c == EOF) break; } c = fgetc(fp); if (c == EOF) { tok.type = TK_EOF; strcpy(tok.lexeme, "EOF"); return tok; } (*col)++; // 标识符与关键字:先完整读入,再查关键字表 if (isalpha(c) || c == '_') { int i = 0; while ((isalnum(c) || c == '_') && i < MAX_LEXEME - 1) { tok.lexeme[i++] = (char)c; c = fgetc(fp); (*col)++; } ungetc(c, fp); (*col)--; tok.lexeme[i] = '\0'; if (strcmp(tok.lexeme, "if") == 0) tok.type = TK_IF; else if (strcmp(tok.lexeme, "else") == 0) tok.type = TK_ELSE; else if (strcmp(tok.lexeme, "while") == 0) tok.type = TK_WHILE; else if (strcmp(tok.lexeme, "return") == 0) tok.type = TK_RETURN; else tok.type = TK_IDENT; return tok; } // 数字字面量:整数与浮点数合并识别 if (isdigit(c)) { int i = 0, is_float = 0; while (isdigit(c) && i < MAX_LEXEME - 1) { tok.lexeme[i++] = (char)c; c = fgetc(fp); (*col)++; } if (c == '.') { is_float = 1; tok.lexeme[i++] = (char)c; c = fgetc(fp); (*col)++; while (isdigit(c) && i < MAX_LEXEME - 1) { tok.lexeme[i++] = (char)c; c = fgetc(fp); (*col)++; } } ungetc(c, fp); (*col)--; tok.lexeme[i] = '\0'; tok.type = is_float ? TK_FLOAT_LIT : TK_INT_LIT; return tok; } // 单字符运算符与界符 switch (c) { case '+': tok.type = TK_PLUS; strcpy(tok.lexeme, "+"); break; case '-': tok.type = TK_MINUS; strcpy(tok.lexeme, "-"); break; case '*': tok.type = TK_STAR; strcpy(tok.lexeme, "*"); break; case '/': tok.type = TK_SLASH; strcpy(tok.lexeme, "/"); break; case '(': tok.type = TK_LPAREN; strcpy(tok.lexeme, "("); break; case ')': tok.type = TK_RPAREN; strcpy(tok.lexeme, ")"); break; case '{': tok.type = TK_LBRACE; strcpy(tok.lexeme, "{"); break; case '}': tok.type = TK_RBRACE; strcpy(tok.lexeme, "}"); break; case ';': tok.type = TK_SEMI; strcpy(tok.lexeme, ";"); break; default: tok.type = TK_ERROR; tok.lexeme[0] = (char)c; tok.lexeme[1] = '\0'; break; } return tok; }

这段代码的逻辑重心有两个。第一是“先完整读入,再查关键字”:进入字母分支后,循环把整个标识符吞完,再统一用strcmp比较关键字表。这样的顺序保证了ifVar被识别成一个标识符,而不是被拆成关键字if加标识符Var。第二是ungetc(c, fp)回退:当数字后面跟着的不是数字或小数点时,把多读的那个字符放回流中,让下一次get_token重新处理。回退时同时要(*col)--,否则列号会越报越偏。

MAX_LEXEME设成64,是针对课程实验里没有超长标识符的保守值。如果实验题规定标识符最长32,可以直接改小;但注意循环条件是i < MAX_LEXEME - 1,留了一个位置给结尾的'\0',这是新手最容易踩的缓冲区溢出点。另一个细节是浮点识别:这里只处理了123.456这种形态,没有处理.5这种省略整数部分的写法,实验要求如果明确要支持,需要在isdigit(c)分支前面再加一个c == '.'的判断。

2.3 用Flex生成词法分析器:另一种常见做法

手写状态机虽然直观,但遇到需要识别几十种正则模式时,代码会膨胀得很厉害。课程实验里很多同学会改用Flex,实验包通常也会允许这种提交方式。一个典型的lex.l片段长这样:

%{ #include "tokens.h" %} %option noyywrap %% "if" { return IF; } "else" { return ELSE; } "while" { return WHILE; } "return" { return RETURN; } [a-zA-Z_][a-zA-Z0-9_]* { return IDENT; } [0-9]+("."[0-9]*)? { return NUM; } "+" { return PLUS; } "-" { return MINUS; } "/*" { /* 处理多行注释 */ } "//".* ; /* 单行注释直接丢弃 */ [ \t\n]+ ; /* 空白直接丢弃 */ . { fprintf(stderr, "lex error at line %d\n", yylineno); } %%

Flex的好处是规则和动作分离,调试时改一条规则比改一整个switch省事。代价是你得额外搞清楚yylval、yylineno和头文件生成这些Flex特有的机制,环境配置本身就要花掉半天。我的建议是:如果实验报告里要求画出状态转换图,就手写状态机,方便对照;如果只验收可执行程序和测试通过,Flex更快。两条路在最终产物上没有本质区别,Token接口设计成一样的就好。

3. 语法分析器:把Token流钉成语法树

3.1 三种语法分析路线,课程实验选哪种不翻车

语法分析器的输入是词法分析器吐出的Token流,输出是语法树。严格说,语法树要表达“哪个产生式匹配了哪个输入串”,常见实现有三种:递归下降、LL(1)预测分析表、LR系列分析表。课程实验里最常见的是递归下降,其次是LL(1)。

递归下降的本质,是为文法里每个非终结符写一个函数,函数体里按产生式右侧逐个匹配终结符或调用其他非终结符函数。它的优点是代码结构和文法一一对应,出错时在哪一行报错一目了然,而且支持任意复杂的语义动作。缺点是左递归文法必须手工消除,优先级和结合性要靠函数调用层级来表达。

LL(1)预测分析表则把“下一个非终结符面对当前输入Token该选哪条产生式”预先算好,运行时用一张二维表加一个符号栈驱动。它的优点是逻辑更接近课本,但你需要手动求FIRST集和FOLLOW集,表一旦算错,运行时错误极其诡异,调试起来比递归下降痛苦得多。

LR系列的表达能力最强,但课程设计课时的强度下不太现实:状态簇、ACTION表、GOTO表,一个实验报告就能写四十页。除非题目明确要求做SLR(1)分析表的生成器,否则我不建议把这个列为第一目标。

3.2 递归下降实现表达式文法:消除左递归后的C代码

表达式文法必须先消除左递归。以加减乘除为例,标准写法是把左递归拆成循环:

expr -> term expr_tail expr_tail -> + term expr_tail | - term expr_tail | ε term -> factor term_tail term_tail -> * factor term_tail | / factor term_tail | ε factor -> ( expr ) | NUM | IDENT

对应的C实现如下,为了演示输出效果,这里在归约时打印后缀式指令,方便肉眼验证语法树结构。

typedef struct { Token lookahead; int eof_flag; } Parser; void advance(Parser *p) { if (!p->eof_flag) { *p = get_token(...); if (p->lookahead.type == TK_EOF) p->eof_flag = 1; } } int accept(Parser *p, TokenType t) { if (p->lookahead.type == t) { advance(p); return 1; } return 0; } int expect(Parser *p, TokenType t) { if (accept(p, t)) return 0; fprintf(stderr, "line %d col %d: expected %s, got %s\n", p->lookahead.line, p->lookahead.col, type_names[t], type_names[p->lookahead.type]); return -1; } void parse_expr(Parser *p) { parse_term(p); while (p->lookahead.type == TK_PLUS || p->lookahead.type == TK_MINUS) { int op = p->lookahead.type; advance(p); parse_term(p); printf(op == TK_PLUS ? "ADD\n" : "SUB\n"); } } void parse_term(Parser *p) { parse_factor(p); while (p->lookahead.type == TK_STAR || p->lookahead.type == TK_SLASH) { int op = p->lookahead.type; advance(p); parse_factor(p); printf(op == TK_STAR ? "MUL\n" : "DIV\n"); } } void parse_factor(Parser *p) { if (accept(p, TK_INT_LIT) || accept(p, TK_FLOAT_LIT)) { printf("PUSH %s\n", p->lookahead.lexeme); } else if (accept(p, TK_IDENT)) { printf("PUSH IDENT %s\n", p->lookahead.lexeme); } else if (accept(p, TK_LPAREN)) { parse_expr(p); expect(p, TK_RPAREN); } else { fprintf(stderr, "line %d: unexpected token %s\n", p->lookahead.line, type_names[p->lookahead.type]); } }

代码里最关键的两个函数是accept和expect。accept负责“看当前Token是否匹配”,匹配就消费掉并前移,不匹配就返回0而不消费;expect在accept失败时打印带行列号的错误信息,并返回-1。所有语法错误都通过expect这一条路出来,错误信息格式就统一了——这是比在每一层函数里各写一套报错更省心的做法。

parse_expr里用while而不是if,正是对应消除左递归后expr_tail的“零次或多次循环”。加减是左结合,所以每次匹配到运算符后立即递归下降右侧的term并输出指令,这个输出顺序天然就是左结合的后缀式。如果把while改成if,同级的+和-只能识别一个,第二个运算符会直接触发错误分支。

3.3 如果非要走LL(1)表格路线:FIRST、FOLLOW集和预测分析表

有些实验题直接给定文法,要求“构造LL(1)预测分析表”。这时候上面的递归下降代码不能直接用,得先把FIRST和FOLLOW求对。对上面那个表达式文法,两个集合长这样:

非终结符FIRSTFOLLOW
expr( NUM IDENT$ )
expr_tail+ - ε$ )
term( NUM IDENT+ - $ )
term_tail* / ε+ - $ )
factor( NUM IDENT* / + - $ )

预测分析表按“非终结符 × 终结符 → 产生式”填充。比如expr_tail遇到+时选+ term expr_tail,遇到)或$时选ε。运行时维护符号栈,栈顶是终结符就匹配输入,栈顶是非终结符就查表。这个方案一旦表算错,错误表现为“期望某Token但栈顶不匹配”,排查时只能一行行对照FIRST和FOLLOW的推导过程,非常消磨耐心。我的建议是:除非题目明确要求构造预测分析表,否则用递归下降完成语法分析,FIRST与FOLLOW集合只作为理论分析写在报告里就够了。

4. 词法到语法联动:接口定义清楚,联调才不玄学

4.1 接口设计:让词法分析器成为语法分析器的数据源

很多同学的词法分析器单独跑没问题,语法分析器单独测试也没问题,一联调就崩。根源是两边各写各的,Token结构不统一,或者语法分析器内部自己又封装了一遍词法逻辑。正确做法是先定接口再写实现:词法分析器只暴露一个函数get_token,每次调用返回下一个Token;语法分析器只依赖这个函数,不关心Token是从文件读的还是从字符串读的。

typedef Token (*TokenProvider)(void *ctx);

让语法分析器持有这个函数指针,联调时词法器可以把文件句柄塞进ctx,测试时也可以换成从内存字符串取Token的假实现。这样单元测试不用写临时文件,直接喂字符串就能验证语法逻辑。

4.2 主程序联调:读文件、跑词法、跑语法一条龙

下面这个主程序,把前面两段代码串起来。

typedef struct { FILE *fp; int line; int col; } LexerCtx; Token next_token(void *ctx) { LexerCtx *lc = (LexerCtx *)ctx; return get_token(lc->fp, &lc->line, &lc->col); } void parse_program(Parser *p) { while (!p->eof_flag) { if (p->lookahead.type == TK_ERROR) { fprintf(stderr, "lex error: %s at line %d col %d\n", p->lookahead.lexeme, p->lookahead.line, p->lookahead.col); advance(p); continue; } if (p->lookahead.type == TK_SEMI) { advance(p); continue; } parse_expr(p); if (expect(p, TK_SEMI) != 0) { // 错误恢复:跳到下一个分号再继续 while (!p->eof_flag && p->lookahead.type != TK_SEMI) advance(p); if (p->lookahead.type == TK_SEMI) advance(p); } } } int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "usage: %s source.txt\n", argv[0]); return 1; } FILE *fp = fopen(argv[1], "r"); if (!fp) { perror("open failed"); return 1; } LexerCtx lc = {fp, 1, 1}; Parser p; memset(&p, 0, sizeof(p)); p.lookahead = next_token(&lc); parse_program(&p); fclose(fp); return 0; }

这段代码示范了三个联调要点。第一,LexerCtx把文件指针、行号、列号打包成一个上下文,next_token把它转成TokenProvider接口,语法分析器完全不需要知道底层是文件还是字符串。第二,parse_program里对TK_ERROR做了容错,词法错误打印后消费掉Token继续跑,避免一错就停导致剩下全部是误报。第三,语法错误后的恢复策略是“跳到下一个分号”,这对应语句边界的同步,是编译器教科书里说的panic mode恢复的简版,用在课程实验里足够了。

4.3 错误处理的收益:行列号准确,排错才不靠猜

联调阶段最怕的是“错误信息给了,但行列号对不上”,你会花大量时间在代码里加打印来追踪Token到底读到了哪个位置。要避免这个问题,词法阶段就要把line和col维护准确,尤其是ungetc回退时列号要跟着回退,注释跳过时换行要加行号。语法阶段报错统一走expect,把“预期的Token类型”和“实际收到的Token类型”都打出来,这两个信息组合起来基本能定位到文法哪条产生式写错了。

很多课程实验的测试脚本是按输出文本比对判分的,错误信息格式可能不算分,但人眼排查依赖它。我在联调时习惯先写一个只有一条语句的最小文件,比如a = 1;,跑通后再逐步加长。这个习惯看起来笨,但能直接把词法层和语法层的问题隔离,比一上来跑整个官方样例再回头猜哪里错了快得多。

5. 编译原理实验避坑:5个让分析器翻车的细节

5.1 第一行就词法报错,列号还跳了十几位

现象:输入文件第一行是if (a) return 1;,词法分析器输出的错误却说unexpected character at line 1 col 14,而且后面的Token全乱了。

原因:ungetc回退和列号不同步。常见写法是读完数字后ungetc(c, fp)但忘了(*col)--,导致接下来每个Token的列号都比实际偏大几位。另一个更隐蔽的原因是空白跳过循环里,isspace(c)和c == '\n'两个分支同时命中时,\n被isspace先吃掉,列号没有重置为1。

解决:把isspace判断放到c == '\n'之后,并确保每个分支里line和col的维护都同步。建议写一个小函数advance_char统一处理列号增减,而不是在每个分支里手动维护。

5.2 关键字全变成IDENT,或者ifVar被拆成if和Var

现象:输入里明明写了while,Token类型却是IDENT;反过来,变量名ifCount被报成关键字。

原因:两类问题其实是一个方向相反的bug。前者是词法循环在读标识符时遇到非字母数字就停,但比较关键字时用的是截断后的字符串;后者是边读边逐字符匹配关键字,读ifCount时先撞上if就直接返回了关键字Token。

解决:统一“先完整读入整个标识符,再整体查关键字表”的顺序。循环条件用isalnum(c) || c == '_'吞完所有字符,存进lexeme,然后再用strcmp从头到尾比较。这样ifCount永远是一个IDENT,而单独出现的if才会被识别成关键字。网上流传的清华大学出版社编译原理第三版第二章习题答案可以用来验证这些概念题,但实验代码千万别照抄网上片段,查重和逻辑陷阱都是坑。

5.3 单行注释跳过之后,下一行第一个字符神秘消失

现象:文件第一行是// comment\nint a;,跳过注释后,int前面的i字符丢了,词法输出变成nt。

原因:注释判断的写法通常是c == '/' && (c = fgetc(fp)) == '/',这个表达式在遇到/后已经读走了下一个字符。如果下一个字符也是/,则进入注释循环;但如果下一个字符不是/(比如是*或普通字符),这个字符被读走之后没有ungetc回退,就永远丢了。

解决:遇到/后不要立即吞掉下一个字符,而是先c = fgetc(fp),判断后再决定回退还是进入注释。同时,注释循环里遇到\n要(*line)++、*col = 1,否则注释后的代码行号全部错位。

5.4 字符串里的分号、括号和//被当成普通代码处理

现象:样例里有一行print("hello; // world");,词法分析器在分号处截断,后面的//被当成注释,导致整个字符串后面的代码全被吞了。

原因:状态机里没有字符串状态。所有字符一视同仁地走标识符、数字、符号分支,字符串内部的字符自然就流到了错误的分支里。

解决:在词法主循环之前增加字符串分支。读到"时,进入in_string状态,循环读取直到下一个未转义的"为止,中间遇到\n可以报“未闭合字符串”错误。字符串内容整体作为一个TK_STRINGToken返回,内部的分号、注释符号、括号都不再参与词法分析。如果课程实验的语言不支持字符串,这一步可以跳过,但很多实验的print语句是带字符串的,提前做了能少掉一大半测试错误。

5.5 语法分析器死循环,控制台一直没有输出

现象:输入一个语法错误文件后,程序不退出不报错,CPU占用率拉满。

原因:语法错误触发后,accept返回0但没有消费Token,而parse_expr的while循环又不会主动跳过当前Token。比如当前Token是TK_RPAREN,parse_factor里三个分支都匹配不上,但也没有advance,于是parse_expr的while在同一个Token上反复调用parse_term,形成死循环。

解决:在每个“不匹配”分支里先打印错误,再强制advance跳过当前Token。更稳妥的做法是4.2里演示过的错误恢复:语法错误发生时,循环跳过Token直到遇到分号或右大括号这类语句边界,再恢复解析。这个机制实现起来不到二十行,但能让分析器在任何输入下都保证终止,这是实验判分时一个很重要的隐性指标。

6. 验证三件套:从复现官方样例到构造自己的测试用例

拿到这份zip后,先别急着改代码。我的验证习惯是准备三组文件:第一组是官方给的合法样例,用来确认基本流程通不通;第二组是我自己构造的语法错误样例,比如a = ;、if (a { }、return 1(缺分号),用来验证报错信息和错误恢复;第三组是边界样例,包括空文件、只有注释的文件、超长标识符、连续多个运算符。三组都过,再谈提交。

一个很实用的验证技巧是给词法分析器加一个--dump-tokens命令行选项,跑完把每个Token的类型、词素、行列号打印成表格。肉眼扫一遍Token序列,比单步调试快得多。语法树的验证则用3.2里那种后缀式输出:输入1 + 2 * 3,输出应当是PUSH 1、PUSH 2、PUSH 3、MUL、ADD,顺序不对就意味着优先级或左结合没实现对。

最后再提一个我自己的习惯:每次改完词法或语法代码,先用同一个坏文件跑一遍,确认报错行为没有退化。这个习惯不止一次帮我抓到了“改好一个分支、弄坏另一个分支”的回归问题。整个实验做完,你会发现词法分析器和语法分析器看着是两栋楼,地基却是同一套接口设计和错误处理思路。希望这些踩坑记录能帮你在ddl之前少走几段弯路,把这份UESTC实验包真正变成自己的东西。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 13:03:43

一个大厂校招生的 AI Coding 工作流:我如何把 AI 接入完整开发流程

我最开始只是从 GPT 网页复制代码 我第一次真正把 AI 用到写代码里&#xff0c;并不是 Codex&#xff0c;也不是现在常见的 Coding Agent。 那时候的流程很简单&#xff1a; 遇到问题&#xff0c;打开 ChatGPT。 把代码复制进去&#xff0c;把报错复制进去&#xff0c;再补上一…

作者头像 李华
网站建设 2026/10/3 13:03:40

学工平台管理系统

✅作者简介&#xff1a;合肥自友科技 &#x1f4cc;核心产品&#xff1a;智慧校园平台(包括教工管理、学工管理、教务管理、考务管理、后勤管理、德育管理、资产管理、公寓管理、实习管理、就业管理、离校管理、科研平台、档案管理、学生平台等26个子平台) 。公司所有人员均有多…

作者头像 李华
网站建设 2026/10/3 13:01:32

MiniMind-O快速推理:一条命令让0.1B小模型听懂语音并开口说话

MiniMind-O快速推理&#xff1a;一条命令让0.1B小模型听懂语音并开口说话 【免费下载链接】minimind-o &#x1f399;️ A 0.1B Omni model trained from scratch, capable of listening, speaking, and seeing! 项目地址: https://gitcode.com/gh_mirrors/mi/minimind-o …

作者头像 李华
网站建设 2026/10/3 13:00:52

中学家校互联系统源码 Java+SpringBoot+Vue3 前后分离

一、关键词中学家校互联系统&#xff0c;中学校园家校沟通平台&#xff0c;中学家校互通信息服务系统二、作品包含源码数据库全套环境和工具资源本地部署教程三、项目技术前端技术&#xff1a;Html、Css、Js、Vue3、Element-plus后端技术&#xff1a;Java、SpringBoot2、MyBati…

作者头像 李华
网站建设 2026/10/3 12:57:12

LSTM/RNN海浪波高预测实战:NC数据处理与双模型对比

简介&#xff1a;本资源是一套基于Python实现的海浪波高时间序列预测源码&#xff0c;面向海洋工程、气象预报及机器学习初学者与实践者&#xff0c;解决小样本站点环境下波高动态建模与短期预报问题。压缩包共2个文件&#xff08;1个.nc格式实测气象数据文件含风速与波高时序&…

作者头像 李华