C语言高效解析CSV:状态机、内存映射与常见坑位排查
1. 从零开始为什么我会用C语言处理CSV文件很多人听到“C语言处理CSV”第一反应是这不是脱裤子放屁吗Python里pandas三行搞定Excel打开另存为不就行了何必用C去和文件指针、内存分配较劲这个想法我能理解但我确确实实遇到过必须用C的场景。一次是在嵌入式设备上做数据采集板子跑的是精简LinuxPython解释器挤不进那点Flash空间存储芯片里存了一批日志每次开机要解析CSV格式的传感器数据并计算均值绕不开C。另一次是处理一个几百兆的交通流数据文件用脚本语言读进去光内存就吃掉几个G最后服务器换成了C写的解析器内存占用直接掉了九成跑得还快一个数量级。真到这些场景C是最合理的路子没有之一。这篇文章我想把C语言处理CSV的完整思路理一遍包括三种典型读取方式的选型对比、一个能正确处理引号与逗号嵌套的解析状态机、写回和修改文件时的细节处理最后把我踩过的坑整理成一份排查表。适合的人有两类一类是像我做嵌入式或做后台服务、需要在高性能或受限环境下处理文件的同学另一类是刚学完C语言结构体和文件操作、想找个像样的综合练手项目的学生。基础要求不高懂指针、malloc、fopen这级就够了其余我会一层层拆开讲。说句实在话CSV这东西看起来就是个用逗号隔开的文本但它边界情况极多空字段、引号内含逗号、字段内换行、大小写分隔符每一个都是坑。把这套逻辑写明白你对C语言的理解会往上抬一个台阶。2. 方案选型三种读取方式的取舍别一上来就fscanf2.1 方案对比逐字符、逐行加字段拆分、内存映射我见过不少初学者写CSV读取上来就是fscanf按逗号分隔往里读结果某一行少个字段整个顺序全错位了三小时调不出来。CSV本质上是文本流最稳妥的做法不是靠格式化输入“碰运气”而是先读一整体行再对行做字段拆分。这里有几个基础选项值得摆出来对比一下方案实现思路优点缺点适用范围逐字符读取fgetc逐个字符判断状态可控性最强引号嵌套、换行都能精确处理代码量偏大单字符I/O调用多需要复杂解析规则的通用解析器逐行读取fgets取一行再用strtok或状态机切字段写法直观快调试容易无法处理字段内含换行的合法CSV95%以上的常规场景内存映射mmap把文件映射进内存再按指针扫描大文件性能最优代码逻辑像是操作字符数组跨平台不便32位系统下大文件受限几百兆乃至上G的大文件处理2.2 我为什么在大多数项目里选“逐行加自定义拆分”先把结论放在前面除非你要处理的文件里频繁出现字段内带换行的极端情况否则逐行读取加自行实现字段拆分是性价比最高的组合。为什么要自己实现拆分而不直接用strtok因为strtok会把连续两个逗号当成一个逗号处理空字段直接消失之后字段对齐就全乱套了。我在处理带空值的传感器数据时一行“1,,3.5”经过strtok拆完就只剩下三个不准确的根本没法恢复。此外strtok会修改原字符串把分隔符替换成\0这在后续想保留原始行做缓存时很麻烦。所以我宁可多写一个函数要的就是可控性。真实的CSV解析器包括各种知名开源库基本都跑在状态机上所谓状态机你可以理解成在处理每个字符时我们手里捏着一个“当前状态”根据这个状态决定接下来怎么办。比如遇到双引号时如果当前是在普通字段中就进入“引号内”状态之后逗号和换行都不再是分隔符直到再遇到一个双引号回到“普通字段”状态。这个逻辑其实不复杂但把流程捋顺比到处堆if判断要清晰得多。2.3 内存映射方案的适用边界如果文件超过一两百兆逐行fgets配合缓冲区复制确实存在CPU和时间开销。这时可以把文件整个映射到虚拟内存里直接在映射区域上做指针扫描相当于把文件当成一个巨大的字符数组。我在处理交通流数据时用过一次同样的逻辑耗时比fgets版本减少了大约百分之四十内存还省掉了重复的行拷贝。但mmap不是没有坑。不同平台的内存映射参数不同Windows下对应的实现机制和Unix不同代码通用性会有问题。嵌入式环境有的根本不开mmap有的文件系统实现得很简陋映射之后性能反而变差。所以除非你的项目明确要处理大数据量且目标平台确定否则一开始没必要碰mmap用方案二把逻辑跑通留一个优化接口就好。3. 核心解析逻辑手写一个能扛住引号和逗号的状态机3.1 前置准备定义行结构体和字段结构体在C里处理不定长的行和字段离不开动态内存。我习惯定义一个Field结构体保存单个字段的内容和长度再用一个Row结构体保存一行的字段数组。这样后续无论是按行遍历、修改字段还是写回文件都很顺手。typedef struct { char *data; size_t len; } Field; typedef struct { Field *fields; int count; char *raw_line; // 保留原始行调试和写回时有用 size_t raw_len; } Row;这里有个经验之谈不要用定长的char数组去存字段。比如你认为最长字段不会超过64字节结果源数据里出现一个超长注释字段直接溢出把后面的内存写坏了排查起来极其痛苦。用动态分配读取时先量出长度再分配或者直接用realloc逐步扩展虽然代码多几行但稳得多。3.2 逐字符状态解析伪代码和完整实现下面这个函数把一行文本拆成字段数组。它的核心逻辑是遍历每个字符并根据“是否在引号内”这个状态做出反应。我只讲关键点不做无意义的逐行复述。int parse_csv_line(char *line, Row *row) { if (!line || !row) return -1; row-fields NULL; row-count 0; row-raw_line line; row-raw_len strlen(line); char *field_start line; int in_quotes 0; int field_count 0; int capacity 8; row-fields malloc(sizeof(Field) * capacity); for (char *p line; ; p) { if (*p ) { if (in_quotes *(p 1) ) { // 两个连续双引号转义的双引号字符 p; continue; } in_quotes !in_quotes; continue; } if (*p , !in_quotes) { if (field_count capacity) { capacity * 2; row-fields realloc(row-fields, sizeof(Field) * capacity); } size_t len p - field_start; row-fields[field_count].data malloc(len 1); memcpy(row-fields[field_count].data, field_start, len); row-fields[field_count].data[len] \0; row-fields[field_count].len len; field_count; field_start p 1; continue; } if (*p \0 || (*p \n !in_quotes) || (*p \r !in_quotes)) { if (field_count capacity) { capacity * 2; row-fields realloc(row-fields, sizeof(Field) * capacity); } size_t len p - field_start; row-fields[field_count].data malloc(len 1); memcpy(row-fields[field_count].data, field_start, len); row-fields[field_count].data[len] \0; row-fields[field_count].len len; field_count; break; } } row-count field_count; return 0; }这段代码有几个地方我不想一笔带过第一是连续双引号的处理。CSV规范里如果一个字段本身包含双引号字符写入文件时会写成两个连续的双引号例如一个字段的内容是他说你好文件里会存成他说你好。上面的循环遇到第一个引号进入引号状态遇到第二个引号时本来该退出但我检查到下一个字符还是引号就知道这是一个转义出来的引号字符直接跳过到下一个再继续。第二是换行处理只有在非引号状态下遇到换行才表示行结束引号内部的换行是合法的字段内容所以不能在这一层断行这正是逐行fgets方案无法覆盖的场景。用的时候这样调FILE *fp fopen(data.csv, r); if (!fp) { perror(open file); return 1; } char line[8192]; Row row; while (fgets(line, sizeof(line), fp)) { line[strcspn(line, \r\n)] \0; if (parse_csv_line(line, row) ! 0) continue; // 直接打印第一列和第三列做验证 printf(col0:%s col2:%s\n, row.fields[0].data, row.fields[2].data); // 记得释放本行字段内存 for (int i 0; i row.count; i) free(row.fields[i].data); free(row.fields); } fclose(fp);3.3 为什么状态机比strtok更值得信任你可能会质疑这个状态机跟strtok比到底强在哪我直接给你展示一个实际案例。假设源文件里有这样一行编号,备注,数值 A001,备注里有个,逗号还有引号,3.14用strtok按逗号拆备注会被拆成两段数据列就错位了。而状态机因为知道引号内的逗号不算分隔符能正确得出三个字段编号是A001备注是完整的字符串数值是3.14。我处理过一批用户反馈数据里面大量存在这种备注带逗号、带引号的写法不用状态机的解析器第二天上线就崩了。这也是我在这篇里反复强调状态机的根本原因它不是炫技而是正经的工程需要。4. 完整实操从读取到清洗到写回的全套代码4.1 实现一个通用的CSV读取器框架单独一个解析函数离“能用”还有距离最好再包一层读取器维护文件句柄、行号、当前行缓冲这样调用方只需要一个next_row接口。typedef struct { FILE *fp; char line[8192]; int line_no; int has_next; } CSVReader; CSVReader *csv_open(const char *path) { CSVReader *r malloc(sizeof(CSVReader)); r-fp fopen(path, r); if (!r-fp) { free(r); return NULL; } r-line_no 0; r-has_next 1; return r; } int csv_next(CSVReader *r, Row *row) { while (r-has_next fgets(r-line, sizeof(r-line), r-fp)) { r-line_no; size_t len strlen(r-line); // 去掉尾部换行包括 \r\n\r\n 混合 while (len 0 (r-line[len-1] \n || r-line[len-1] \r)) { r-line[--len] \0; } if (parse_csv_line(r-line, row) ! 0) continue; return 1; } return 0; // 文件读完或出错 }这个封装让我在业务代码里不用在乎底层细节主循环只是简单的“只要还有下一行就处理”。如果你的CSV里字段可能内含换行那么上面的fgets策略就不够用了需要改成逐字符拼行判断标准是只有非引号状态下遇到换行才算一行结束。逐字符版本可以复用之前解析循环的思想把fgetc的字符拼进一个动态缓冲区当遇到换行且不在引号内时调用类似parse_csv_line的逻辑去做字段拆分。4.2 字段清洗去掉空格、处理前后空白实际数据往往不干净。常见情况是字段里混着前后空格比如一行 “ A ,B, C ”第一列的值是“ A ”而不是“A”做匹配时莫名失败。我习惯在解析字段时顺手做一个trim但这里要给你一个提醒CSV规范里空格是有效字符不应该无条件去掉。所以我的做法是提供一个可配置选项默认不处理需要时开启。具体代码不算复杂就是对字段start和end指针做前后遍历跳过空白字符。static void trim_field(char **start, size_t *len) { while (*len 0 ((*start)[0] || (*start)[0] \t)) { (*start); (*len)--; } while (*len 0 ((*start)[*len - 1] || (*start)[*len - 1] \t)) { (*len)--; } }调用时机放在parse_csv_line得到一个字段之后是否调用由配置开关控制。从工程角度说宁可多一个开关也不要写死因为你不知道数据源哪天会变成对空格敏感的类型。还有一个隐藏问题空字段。两个逗号连续出现时比如“A,,C”字段2的长度是0data指向一个空字符串这是合法的但如果你用strcmp去判断要注意区分“空字符串”和“NULL”。我的Field结构体里data永远不为NULL这样后续代码就不需要做双重判空。4.3 修改字段并写回文件重建行的正确姿势处理CSV经常需要改某几列再写回去。最粗暴的方式是读文件、找到对应行、原地改字节但这样会踩到行长度变化的雷你改短了后面内容要往前挪改长了后续数据会被覆盖。正确的思路是重建整个输出文件边读边解析修改需要动的字段再按规范格式把每一行序列化写到一个新文件全部写完后再替换原文件。序列化一个行时要判断字段是否需要加双引号只要字段内包含逗号、双引号、换行这三种字符之一就必须用引号包起来并且把内部的双引号替换成两个双引号。void csv_write_field(FILE *out, const char *field) { int need_quote 0; for (const char *p field; *p; p) { if (*p , || *p || *p \n || *p \r) { need_quote 1; break; } } if (!need_quote) { fputs(field, out); return; } fputc(, out); for (const char *p field; *p; p) { if (*p ) fputc(, out); fputc(*p, out); } fputc(, out); }配合这个函数写一行就是把所有字段依次输出中间补逗号最后补换行。这块代码不长但它是整个CSV写回环节最重要的部分我见过太多人只处理了逗号没处理引号结果写出去的文件跳过解析库读取直接报错。4.4 文件替换与备份别把原文件玩丢写回时我建议用临时文件加原子替换。流程是打开原文件同目录下的临时文件写入全部内容fclose后用rename覆盖原文件。这样做的价值在于如果程序中途挂了原文件还在数据不丢。FILE *out fopen(data.csv.tmp, w); // 写入全部分析结果 fclose(out); remove(data.csv); rename(data.csv.tmp, data.csv);在实际项目里修改数据的程序我还会多留一个带时间戳的备份文件。尤其接数据库前宁可多备份一次也不要让脏数据把原来的好数据覆盖掉。5. 常见问题与排查技巧我踩过的九个坑5.1 字段丢失和错位的元凶症状可能原因解决办法每行最后一个字段莫名消失平时fgets保留了尾部换行解析时把换行算进字段解析前行尾裁剪或解析循环里把换行当结束符字段错位越往后越乱用了strtok导致空字段丢失换成状态机解析确保空字段保留中文乱码文件编码是GBK你按UTF-8处理先确认文件编码用iconv转码后再解析行数对不上少了几行某字段内含换行被当成多行用支持引号内换行的逐字符解析器大文件内存暴涨一次性把所有行读进内存再处理改成流式处理逐行解析逐行释放字段数据多出看不见的字符行尾有\r被当成字段一部分把\r和\n都作为行结束条件读文件后没法修改内容直接用fwrite定位覆盖长了截断短了溢出重建整个输出文件内存分配报错或崩溃解析函数未释放字段内存或容量扩展写错封装结构体每行处理完统一free以缩进开头的字段匹配不上源文件字段前有空格开启trim选项或自行去除前后空白5.2 排查经验从一次真实故障说起有次处理一批日志文件共有几十万行前几千行解析正常到了某个点之后列数对不上比例越往后越高。我一开始怀疑数据本身有缺失打印出错行的原始内容后才发现那一行里有一个字段值大约是几千字里面包含换行符。用逐行fgets方式读取时这一行在中间被断成了两行之后所有字段自然错位。那次教训让我彻底改了设计凡是字段内容不可控的CSV一律用状态机逐字符读取判断行结束必须考虑引号状态。改完之后同样的文件跑一遍几十万行全部正确。另一个让我记忆犹新的问题是内存泄漏。解析器每处理一行都malloc字段内存但某次在主循环里漏掉了free跑一个几百兆的大文件内存占用以肉眼可见的速度涨最后触发系统OOM进程被杀。从那以后我给自己定了个规矩结构体内凡是动态分配的字段必须配套写一个release函数类型里带free的都要列出来逐行检查。5.3 性能调优的三个方向如果你的解析器在几万行数据上跑得慢优化方向依次排队第一个是把fgetc换成fread批量读入自己维护一个缓冲区减少系统调用次数第二个是在解析时直接使用mmap或者一次性把整个文件读入内存避免频繁的拷贝第三个是把malloc次数降下来每个字段都malloc是时间的无底洞可以考虑先用一个大buffer存行数据字段直接用指针指向buffer内部位置最后统一释放那一块内存。我实测过一个两百兆的CSV用逐行fgets加每字段malloc耗时大约18秒换成fread整块读入加字段指针指向原始buffer后耗时降到7秒。如果业务对性能还敏感可以继续优化字段指针的存储方式但对我来说这个量级已经够用了。6. 我最后再补几句体己话用C写CSV解析器表面上是个文件解析的小任务实际上它综合了动态内存管理、状态机设计、指针边界判断、文本编码意识这些东西恰好是C语言最重要的基本功。如果你刚学完C我建议真的动手把这篇里的状态机敲一遍再把写回函数补上跑一跑含引号、含换行的样本文件你能明显感觉到自己对指针和内存的掌控上了一个台阶。我自己现在的工作里虽然很多场景已经有更高效的脚本语言可用但一旦遇上资源紧张、数据量大的边缘场景写一个轻量的C解析器几乎成了条件反射。哪怕只是想提高脚本程序的运行效率把数据预处理环节下沉到C写成独立小工具再通过命令行和主程序对接也是一种非常稳妥的架构思路。CSV看起来简单真正实战过一遍你才明白越是“简单”的格式越能照出你对底层细节的把握程度。

相关新闻

用知识树搞定技术面试:从零散记忆到系统复习

用知识树搞定技术面试:从零散记忆到系统复习

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

2026/10/17 5:54:31 阅读更多 →
UE5.3崩溃根因:DX12 Feature Level 12_1与反作弊注入时序冲突

UE5.3崩溃根因:DX12 Feature Level 12_1与反作弊注入时序冲突

1. 项目概述:这不是游戏故障,而是UE5引擎与反作弊系统在特定硬件环境下的协同失焦“黎明杀机x尼尔”这个联动内容上线后,大量玩家集中反馈一类高度相似的崩溃现象:小蓝熊图标点击后卡在加载界面、刚进游戏主菜单就闪退、启动器显示…

2026/10/17 5:54:31 阅读更多 →
FPN特征金字塔网络详解:设计动机、代码实现与多尺度目标检测实战

FPN特征金字塔网络详解:设计动机、代码实现与多尺度目标检测实战

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

2026/10/17 5:54:31 阅读更多 →

最新新闻

Windows虚拟化排查指南:从BIOS到Hyper-V的彻底解决

Windows虚拟化排查指南:从BIOS到Hyper-V的彻底解决

这个标题一看就是被模拟器、虚拟机折腾过的人写出来的。说实话,Windows上这颗“虚拟化”的雷,我踩过不下几十次,帮身边朋友排查也排了无数轮。明明主板的开关开了,BIOS里也看到了,系统里却还是提示“CPU 虚拟化已禁用”…

2026/10/17 7:08:04 阅读更多 →
智能体互联协议选型指南:MCP、A2A、ANP对比与避坑实践

智能体互联协议选型指南:MCP、A2A、ANP对比与避坑实践

简介:围绕智能体通信协议MCP、A2A、ANP的演进背景与设计差异,这份PDF面向AI研究者、智能体开发者及关注智能体互联网趋势的企业管理者,系统梳理三类协议的定位、适用场景与选型思路。内容以ANP为主线展开,说明其作为面向智能体设计…

2026/10/17 7:08:04 阅读更多 →
174、让Agent看懂图表:视觉推理

174、让Agent看懂图表:视觉推理

这一篇笔记,得从上周调的一个生产问题说起。客户那边有个Agent,任务是盯着一堆行业报告里的柱状图,自动提取某个季度的营收数据。日志里看,模型明明把图“看”了,也输出了数字,可跟真实报表一对,差了整整一个数量级。最诡异的是,它给的结论跟图例里的单位完全对不上——…

2026/10/17 7:08:04 阅读更多 →
无人驾驶轨迹跟踪与模型预测控制:配套程序整理及调参经验

无人驾驶轨迹跟踪与模型预测控制:配套程序整理及调参经验

前几天我把手头这套无人驾驶车辆轨迹跟踪与模型预测控制第二版配套程序完整整理了一遍。整理之前我以为只是把文件分类,真正动手才发现,这个“配套程序”牵扯的东西远比想象中多:车辆模型用运动学还是动力学、预测时域取多少步、约束怎么设、…

2026/10/17 7:08:04 阅读更多 →
175、语音Agent:STTTTS与对话

175、语音Agent:STTTTS与对话

昨晚线上一个语音Agent在嘈杂环境下把“播放周杰伦的晴天”识别成了“播放周杰伦的情天”,用户重复了三遍,最后气的直接关了。我盯着日志里那句带“情天”的VAD切片,愣了半天。语音Agent这个东西,文档里写的都是“ASR识别-语义理解-TTS合成”的大闭环,但真正上线你会发现,…

2026/10/17 7:08:04 阅读更多 →
零基础网络安全攻防入门:从攻击者视角构建防御体系

零基础网络安全攻防入门:从攻击者视角构建防御体系

朋友,先问你三个场景,你大概率至少中过其中一个:收到一封“你的账户异常,请点击链接验证”的邮件,顺手就点了;某个平台账号密码被撞库,结果同密码的其他平台也一起失守;一台开了远程…

2026/10/17 7:07:04 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 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/16 21:54:38 阅读更多 →
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/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练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/16 14:23:36 阅读更多 →