简介这份面向数据工程师与ETL初学者的Kettle 9.3资料文档围绕开源ETL工具Pentaho Data Integration的快速认知与版本获取展开适合需要搭建数据抽取、转换、加载流程或希望厘清Transformation与Job分工的开发者参考。压缩包仅含1个docx文件约14KB篇幅轻量便于集中阅读与检索内容将Kettle的跨平台运行、图形化工作流设计、多数据源支持、数据清洗与转换聚合等组件能力以及云计算存储集成等要点做了归纳。文档还梳理了9.3版本更新关注点、Spoon客户端的使用脉络、转换与作业在测试部署监控环节的思路并给出百度云获取线索与提取码方便读者按需取得安装包。已有5743人学习或下载可作为版本选择、概念梳理、工具选型和后续实践前的入门导读。1. 拖出来的管道也是程序Kettle 把「怎么做」编译成了什么很多人第一次打开 Spoon 都会有同一个错觉左边一排图标中间一块画布把「表输入」拖进来、连到「表输出」点一下运行数据就动了。看起来像在画流程图实际上你画的是一份可执行元数据——Kettle 这类 ETL抽取、转换、加载工具真正跑起来的从来不是那张图而是图背后被序列化成 XML 的 Step 与 Hop 描述。这条边界一旦想通后面所有「为什么这个步骤会卡住」「为什么把行集改大就快了」「为什么作业里两个转换不能并行」的问题都有了统一的解释口径。从网盘拿到的 9.3 安装包解开后那一堆 jar 和 plugins 目录本质上就是一台把元数据翻译成线程图的解释器。搞懂这台解释器比记住一百个步骤的用法更值钱。2. 元数据即程序.ktr 文件里到底存了什么2.1 一份 Transformation 的 XML 骨架Transformation 存成.ktr本质是一个 XML 文档。用文本编辑器打开去掉几百行画布坐标之后剩下的东西少得惊人一段全局配置、一张 hop 清单、一组 step 定义。下面是我按真实结构精简过的骨架注释标了每个字段在执行期的作用?xml version1.0 encodingUTF-8? transformation info namedaily_order_clean/name description订单明细清洗/description !-- 每个行集Step 之间的缓冲区能放多少行默认 10000 -- size_rowset10000/size_rowset !-- 队列空/满时的轮询等待毫秒数直接决定空转 CPU 占用 -- sleep_time_empty50/sleep_time_empty sleep_time_full50/sleep_time_full trans_typeNormal/trans_type /info !-- 图结构谁连谁就靠这一段 -- order hopfrom读取订单表/fromto过滤无效行/toenabledtrue/enabled/hop hopfrom过滤无效行/fromto字段标准化/toenabledtrue/enabled/hop /order step name读取订单表/name typeTableInput/type sqlSELECT id, amount, created_at FROM t_order WHERE dt ?/sql /step step name过滤无效行/name typeFilterRows/type !-- 条件表达式是字符串运行时才交给表达式引擎解析 -- condition![CDATA[amount gt; 0]]/condition send_true_to字段标准化/send_true_to send_false_to写错误表/send_false_to /step /transformation三个要点值得盯住。第一order里的 hop 是单向的「谁写给谁」它不是控制流而是数据流向声明真正的执行顺序由运行时按依赖关系推导跟你在画布上拖的先后顺序无关。第二step里的type是个字符串标识对应 plugins 目录下的一个 StepMeta 实现类Kettle 靠它做反射实例化——这意味着装一个插件包就能多出一批步骤类型无需改动主程序。第三FilterRows的 true/false 分支写在 step 内部而不是 hop 上所以一条 hop 在 XML 里可以出现多次同名 hop 会被当成两个不同的数据出口。画布坐标那部分GUI节点下的 xloc、yloc对执行完全没有影响。我做过一次血泪实验手工删掉整个 GUI 段用命令行跑结果一模一样只是再用 Spoon 打开时所有步骤叠在左上角。这个细节反向证明了设计意图——图形界面只是元数据的编辑器和渲染器不是执行器的一部分。2.2 Step 与 Hop图结构怎么变成可执行描述在 Spoon 里每拖一个步骤内存中的TransMeta就多一个StepMeta每连一根线就多一个HopMeta。点保存整个对象树被序列化成上面那段 XML。点运行Trans对象拿到TransMeta后做三件事给每个StepMeta反射出一个BaseStep实例、按 hop 给相邻步骤之间创建RowSet、然后逐个start()。这个「先建图、再连边、最后起线程」的顺序解释了 Kettle 里一个很常见的困惑为什么改了上游步骤的字段下游会立刻报「找不到字段」。因为字段描述RowMeta不是编译期确定的而是在init()阶段由上游步骤推给下游的。上游还没init完下游只能拿到空描述这时候任何按字段名取值的配置都会翻车。所以调试字段问题时看日志里第一个报错的步骤而不是最后一个。2.3 为什么用 XML 而不是代码选 XML 做存储格式是这个工具能活二十年的关键取舍。文本格式意味着可以 diff、可以进版本库、可以用脚本批量生成——数据团队经常要按几十张表的模板生成几十个转换直接拿模板套字符串替换字段名就行比调 API 快得多。开源软件的另一面好处在这里体现得最明显格式是公开的你可以写程序生成.ktr也可以在 CI 里跑一遍 XML schema 校验把「步骤名重复」「hop 指向不存在的步骤」这类错误挡在提交阶段。代价同样明显。XML 没有类型系统所有配置都是字符串写错一个字段名不会在保存时报错只会在运行时抛一句语焉不详的异常。而且不同大版本之间节点结构会演进手工拼 XML 的脚本在升级时经常需要跟着改。我一般会这么理解把.ktr当成「给机器读的配置文件」而不是「给人写的代码」凡是需要人反复手改的地方都应该用 Spoon 编辑或者用生成器产出别硬刚。3. 行流模型Step 之间为什么用「行集」而不是函数调用3.1 RowSet一个有界阻塞队列如果让你设计这个引擎最直觉的做法是让上游步骤算完一批数据直接调用下游步骤的处理方法像普通函数那样层层嵌套。Kettle 没这么干它在两个步骤之间插了一个叫 RowSet 的东西——有界阻塞队列。这个选择决定了整个工具的性能特征也决定了它所有的坑长什么样。有界队列带来两个直接后果。一是解耦上游不知道下游是谁只管往队列里放下游不知道数据从哪来只管从队列里取。所以你可以随时在中间插一个「过滤」或者「采样」步骤而不影响两端。二是背压队列满了上游的putRow就阻塞住不再生产。这比「无限缓冲直到 OOM」要体面得多但它也意味着整条管道的吞吐由最慢的那一步决定慢步骤会把所有上游线程一起拖住。生产环境里看到「日志停在某个步骤、CPU 掉到零、内存也不涨」八成就是队列满了在等下游。3.2 每个 Step 一个线程run 循环与结束信号BaseStep继承自Thread每个步骤实例就是一根线程run()里是一个while循环取一行、处理、往下游放。下面这段是我按这套语义写的简化模型用来说明三个关键契约不是源码本身// 简化模型RowSet 的语义等价物 final class BlockingRowSet { private final BlockingQueueObject[] buffer; private volatile boolean done; // 上游是否已宣告结束 BlockingRowSet(int capacity) { // capacity 对应 size_rowset this.buffer new ArrayBlockingQueue(capacity); } // 上游调用队列满则阻塞形成背压不会撑爆堆 void putRow(Object[] row) throws InterruptedException { buffer.put(row); } // 下游调用返回 null 是唯一的「没有更多数据」信号 Object[] getRow() throws InterruptedException { Object[] row buffer.poll(50, TimeUnit.MILLISECONDS); // 对应 sleep_time_empty if (row null done buffer.isEmpty()) { return null; } return row; // 可能为 null 但尚未结束继续轮询 } void setDone() { this.done true; } // 对应 setOutputDone() }三个契约分别是第一null不代表「暂时没数据」而代表「永远没数据了」所以判断结束必须同时满足done和队列已空两个条件否则会提前截断数据。第二done标志必须由上游在所有行都放完之后设置顺序反了就会丢尾部数据。第三起始步骤没有输入 hop 的那种不参与循环init完直接跑一次processRow然后置done这就是为什么「生成记录」这类步骤只输出一批。sleep_time_empty和sleep_time_full这两个毫秒值常被忽略。它们控制轮询的粒度调小会让空闲时 CPU 上去调大则会在数据稀疏时带来延迟累积。批处理场景下 50 毫秒左右是够用的如果是低延迟的流式摄取这两个值直接影响端到端延迟值得单独压测。3.3 行元数据与惰性转换队列里传的不是 Map 也不是 JSON而是Object[]配一份RowMeta描述每个下标的字段名、类型、长度、格式。这个设计非常「数据库味儿」位置定址、类型前置、没有键名查找开销。好处是百万行级别的吞吐下省掉了大量哈希计算坏处是字段顺序一旦错位数据就会串到别的列上——而且不会报错因为类型可能恰好兼容。所以任何手工拼 RowMeta 的插件都要对顺序极其小心。类型系统里有个容易被忽视的优化从文件或数据库读进来的字段可以先不解析保持字节形态往下传直到某个步骤真正需要计算时才转成具体类型。这个惰性策略让「读进来直接写出去」的管道几乎不产生解析开销。但它也会让类型错误延后爆发——某列在源端是字符串、在目标端要转数字问题可能到写库那一刻才暴露。排查这类问题的办法是在中间插一个「字段选择」步骤显式声明目标类型把隐式转换变成显式转换错误就会提前到那一步报出来。3.4 并行度副本与分区一个步骤默认只有一份实例。想加速可以给它配多个副本Kettle 会在上游把行轮流分发给各个副本等于线程级并行。听上去很美但要注意副本之间不共享状态你在步骤里做累加、去重、写同一个目标表多个副本会把结果搞乱。要不要开副本判断标准很简单——这一步是不是纯函数式的逐行转换是就开不是就别碰。4. Transformation 与 Job控制流与数据流为什么必须分开4.1 Job 的执行语义Job 存成.kjb结构上和 Transformation 是两套东西。它也有节点entry和连边hop但 hop 上多了求值条件可以是无条件、也可以是一个表达式的结果。执行时 Job 是单线程的从起点 entry 开始跑跑完看它的出边条件决定下一步走哪条一步一步往下挪。没有行流没有队列节点之间传递的是一份「结果」对象退出码、影响行数、写进结果集的变量。这个「单线程顺序走图」的模型解释了很多行为。为什么 Job 里两个转换可以并行因为有个专门的「并行执行」节点它才去起多个线程。为什么 Job 里的循环能做因为条件 hop 可以指回上游节点形成环——这在 Transformation 里是不允许的.ktr的 hop 必须是无环的引擎在启动阶段就会做拓扑检查。为什么 Job 的错误处理要显式配因为它没有行级概念失败就是节点失败你要么用条件 hop 拦住要么配置失败时的跳转没有「跳过坏行继续」这种自动行为。4.2 两个引擎的差异对照维度Transformation.ktrJob.kjb执行单元StepEntry并发模型每个 Step 一根线程天然并行单线程顺序推进靠专用节点起并发数据载体行集有界队列 RowMeta结果对象、变量图结构必须无环允许条件回指构成循环结束条件所有行集排空且所有步骤置 done走到没有出边的节点典型用途清洗、关联、聚合、装载调度、依赖、异常分支、文件搬运这张表里最值得记住的是「图结构」那一行。很多人想在 Transformation 里做重试循环绕一大圈最后发现根本连不上就是因为引擎只接受有向无环图。要做循环正确的位置是 Job 层用条件 hop 指回去一次循环代表一次完整的转换执行。4.3 错误行要去哪Transformation 的错误处理粒度很细大多数步骤右键就能配「错误处理」把解析失败、类型不匹配的行导到另一个输出同时附带错误码和错误描述字段。这套机制的价值在于它把「坏数据」变成了一等公民——你不用让整批任务失败而是让坏行流到一张错误表里好行继续走。但要注意错误流本身也是一条普通 hop它接的下游步骤同样会阻塞、同样占队列所以错误表写入慢一样能反压整条管道。Job 层的错误处理则是另一套逻辑节点失败 → 看失败 hop 有没有配 → 配了就走异常分支没配就整个作业挂掉。这两层要配合使用我常见的组合是Transformation 内部把可预期的脏数据导到错误流Job 层兜住不可预期的异常连不上库、文件不存在并触发告警。4.4 命令行入口与调度图形界面不是唯一的运行方式安装目录里带了两个命令行入口一个跑转换、一个跑作业。它们接受参数文件、可以覆盖变量是接进调度系统最干净的方式# 跑一个转换-file 指定 ktr-level 控制日志粒度-param 传命名参数 ./pan.sh -file/opt/etl/daily_order_clean.ktr \ -levelDetailed \ -param:LOAD_DATE2024-06-01 \ -logfile/var/log/etl/order_clean.log # 跑一个作业-level 同样适用作业里的变量可从这里注入 ./kitchen.sh -file/opt/etl/daily_job.kjb \ -levelBasic \ -param:ENVprod \ -logfile/var/log/etl/daily_job.log-level这个参数在排查时是救命稻草默认级别不会打印每个步骤的行数统计调到详细级别才能看到每根行集的实际流量判断到底是哪个步骤吞了数据。-logfile建议永远显式指定让调度系统接管日志落盘否则标准输出被重定向后时间戳会乱。变量注入这一条尤其重要——把日期、环境、路径都做成参数同一个.ktr就能在测试和生产之间复用不用维护两份几乎一样的文件。5. 避坑与排查那些让管道「玄学失败」的细节5.1 现象任务卡住不动CPU 为零内存平稳原因几乎总是行集满导致的背压。某个下游步骤在处理一批数据时变慢——可能是在等一个网络超时、可能是目标表上有锁、也可能只是单条 SQL 没有走索引。因为上游的putRow是阻塞的整条链会在几秒内全线停摆日志上看不出任何异常只有最后一条时间戳。解决思路分三步。先确认瓶颈位置把日志调到详细级别看哪个步骤的行数不再增长它就是慢的那个。再看它的下游写库类步骤要检查目标端有没有长事务锁着表查询类步骤要检查 SQL 的执行计划。最后才是调参——把行集调小并不会让它变快只会让背压更早发生真正要做的是给外部调用加超时连接串上的 socket 超时、语句超时让失败快速暴露而不是无限等待。5.2 现象关联之后数据条数暴涨或字段全空多半出在排序合并类的关联上。这类关联为了省内存要求两侧输入按键有序它不像数据库那样自己建哈希表而是双指针同时往下扫。如果输入没排序两边指针错位结果要么匹配不上字段全空要么重复匹配条数暴涨而且不会报任何错——它认为你保证了有序你没保证它就这么错着跑完。解决方式有两个方向。要么在关联前显式加排序步骤两侧都按关联键升序代价是排序会把整批数据落盘要么换成流式查找类的步骤它会在内存里维护一份小表做哈希匹配前提是小表能装进内存。我的习惯是小表几百 MB 以内用流式查找大表对拍才用排序合并而且排序步骤一定要紧挨着关联中间别插任何会打乱顺序的步骤。5.3 现象中文变问号日期变空值字符集问题的根源在连接层。数据库连接串没有显式声明编码时驱动会按服务端默认值猜源端和目标端猜出不同结果就出现乱码。日期变空则通常是格式串对不上源数据是2024/06/01步骤里配的是yyyy-MM-dd解析失败又不报错直接给个空值往下传。排查办法是先采样再改配置。用「表输入」加个 limit 拉一百行出来预览看字段的实际形态再回到配置里把格式串和编码都写死。连接串上把编码参数显式加上别依赖默认值。另外注意字段长度目标表某列定义得比源数据短超长部分会被静默截断这类问题不会在转换阶段报错只会在下游对账时发现数字对不上。5.4 现象金额算出来差几分钱这是类型选错造成的。整数类型存小数会截断浮点类型做大数累加会有二进制精度误差两个都不适合金额。做聚合、求和、比对的字段应该在读取后立刻用「字段选择」步骤显式转成高精度十进制类型再做运算。另一个隐藏来源是中间步骤的隐式转换。某个步骤输出的是字符串下游做数值比较时引擎自动转了一次这次转换用的可能是默认精度。所以我一般在管道入口统一做一次类型规整把每个字段的最终类型定死后面所有步骤都按这个约定走别指望引擎替你猜。5.5 现象作业连跑多次后连接数暴涨Job 里串行执行多个转换时每个转换都会独立建立自己的数据库连接池转换结束理论上会释放但如果某个步骤在异常分支里没走到清理逻辑连接就漏了。跑几天之后数据库端连接数打满新任务全部超时——而这个现象在单次手工测试里永远复现不出来。对策是给每个转换显式配连接池的生命周期参数别用默认的「跟随转换」。同时在 Job 层加一个收尾节点无论成功失败都执行一遍资源清理。有条件的话在监控侧加一条连接数曲线比等告警靠谱得多。6. 设计取舍把「怎么做」藏起来代价是什么元数据驱动这套设计最大的收益是把「描述意图」和「实现执行」彻底分开了。你声明要什么字段、怎么过滤、往哪写引擎负责起线程、建队列、管背压、处理结束信号。这让不懂并发的人也能写出并行度不错的管道也让同一份.ktr能被图形界面、命令行、调度系统三种方式驱动这是它能活这么久的根本原因。代价也很清楚。第一表达能力有天花板一旦逻辑复杂到需要状态机、需要跨行窗口、需要自定义算法可视化配置就变成了枷锁只能退回到写自定义代码步骤而这时候元数据的所有好处可视化、可 diff、可生成全部失效你得到的只是一段被塞进 XML 里的源码字符串。第二性能调优的杠杆很少能动的无非是行集大小、副本数、线程优先级真正的瓶颈往往在外部系统工具层能做的只是让失败更快暴露。第三状态在内存里这对弹性伸缩很不友好——容器化部署时一次扩缩容就意味着正在跑的行集全部丢失必须靠外部调度做重试而重试又要求你的管道是幂等的。云上跑这类工具真正的难点从来不是连不连得上对象存储而是怎么让「有状态的行流」和「随时会被杀掉的实例」共存。验证一套管道到底靠不靠谱我的做法很朴素小数据量先对拍用几百行数据跑一遍把源表、目标表、错误表三边的条数做成恒等式——进来的行数等于写成功的行数加上被拒绝的行数差一条都要查清楚。这个对账逻辑本身也做成一个作业节点每次上线前跑一遍。从那以后我每接一条新管道都强制走一遍行数守恒对账不然不敢挂到调度上。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
Evp破解版.rar安全分析:从压缩包识别到隔离验证的完整指南 简介:这份资源面向需要处理服务端解压与修改场景的技术人员,提供了一款专用的破解版解压工具,主要解决常规解压软件无法正确还原或修改特定服务端压缩包的问题。资源以 rar 压缩包形式分发,整体约 1.02MB,体积轻量便于… · 2026/11/4 2:44:18
滑模控制实现攻击时间与角度双约束制导的MATLAB仿真 简介:这份资源面向导弹与飞行器制导方向的研究生、科研人员及工程技术人员,提供一套基于滑模控制、同时兼顾攻击时间与攻击角度约束的制导律 MATLAB 仿真实现,可用于理解滑动表面设计、控制器构造与约束嵌入的完整流程,并在此基础… · 2026/11/4 2:44:18
优化Hermes Agent的数据库查询性能:执行计划分析与优化全指南 优化Hermes Agent的数据库查询性能:执行计划分析与优化全指南
在现代AI应用开发中,数据库查询性能直接影响应用响应速度和用户体验。Hermes Agent作为一款强大的AI代理框架,其内部使用SQLite数据库(如~/.hermes/state.dbÿ… · 2026/11/4 6:31:25
Hermes Agent与物联网设备安全:认证、加密与固件更新全攻略 Hermes Agent与物联网设备安全:认证、加密与固件更新全攻略
在物联网(IoT)快速发展的今天,设备安全已成为智能家居和工业控制领域的核心挑战。Hermes Agent作为一款强大的开源智能代理框架,通过其模块化设计和灵活的技… · 2026/11/4 6:31:25
Hermes Agent容器编排自动化:CI/CD与GitOps集成 Hermes Agent容器编排自动化:CI/CD与GitOps集成
Hermes Agent是一款强大的容器编排自动化工具,能够无缝集成CI/CD与GitOps流程,为开发团队提供高效、可靠的容器管理解决方案。通过自动化容器的构建、部署和运维,Hermes Agent帮助… · 2026/11/4 6:31:25
Hermes Agent与区块链智能合约部署:自动化与监控全攻略 Hermes Agent与区块链智能合约部署:自动化与监控全攻略
在区块链技术快速发展的今天,智能合约的部署与监控已成为开发流程中的关键环节。Hermes Agent作为一款强大的自动化工具,能够显著简化智能合约的部署流程并提供实时监控能力࿰… · 2026/11/4 6:31:25
探索Hermes Agent的跨模态检索评估:精度与召回率分析 探索Hermes Agent的跨模态检索评估:精度与召回率分析
Hermes Agent是一款功能强大的AI代理框架,提供了丰富的工具调用能力和多模态处理功能。本文将深入探讨Hermes Agent在跨模态检索任务中的评估方法,重点分析精度与召回率这两个关键指标&a… · 2026/11/4 6:31:25
优化Hermes Agent的内存分配:内存泄漏检测与修复完整指南 优化Hermes Agent的内存分配:内存泄漏检测与修复完整指南
在使用Hermes Agent进行AI任务处理时,内存管理是确保系统稳定运行的关键环节。本文将详细介绍如何识别、定位和修复Hermes Agent中的内存泄漏问题,帮助开发者构建更高效、更可靠的AI… · 2026/11/4 6:30:25
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
AI辅助技术栈评估:提升研发决策效率60% 1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17
从生成Token到直接决策:Jev如何重塑AI Agent工程实践 1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17
32G统一内存跑大模型:内存账、TPS实测与端云分流 想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46