ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Markdown 批量上传飞书:AST 解析与 OpenAPI 工程化实践

Markdown 批量上传飞书:AST 解析与 OpenAPI 工程化实践 1. 为什么会有“批量上传 Markdown 到飞书”这个需求事情的起点往往很朴素本地攒了一堆 Markdown 文件可能是技术笔记、项目文档、周报归档也可能是从别的知识库导出的几百篇存量内容现在团队统一把飞书云文档作为知识沉淀的地方于是问题来了——一个个手动复制粘贴把标题、正文、图片、表格挨个搬过去几十篇还能忍几百篇就是纯粹的体力活而且还容易漏、容易错、容易格式崩。我最早接触这个需求是帮一个十来人的小团队做知识库迁移。他们本地有大约四百多篇 Markdown来源很杂有 Obsidian 导出的有 Typora 写的有从代码仓库docs/目录里扒出来的还有一批是 Jupyter Notebook 转出来的。最初的想法很天真觉得飞书云文档支持 Markdown 粘贴那就打开一篇、全选、复制、粘贴、保存收工。实际做到第十篇就崩溃了——图片全部变成裂图因为 Markdown 里的图片是本地相对路径粘贴过去飞书读不到代码块的高亮语言丢失表格勉强能看但列宽乱掉带数学公式的文档直接变成一堆美元符号。所以这个项目的核心命题其实不是“上传”而是如何在批量场景下把 Markdown 的结构、资源、元信息都完整地、可重复地映射到飞书云文档里。这里有三个关键词必须先拆开讲清楚不然后面所有的技术选型都是空中楼阁。第一个是飞书。飞书云文档本身有一套 OpenAPI文档、表格、多维表格、云空间文件夹都有对应的接口。它不是一个“接收 Markdown 就完事”的黑盒而是一套需要你先建文档、再往里面塞块Block的结构化系统。理解这一点非常关键因为它直接决定了你不能简单地“POST 一个 Markdown 字符串上去”。第二个是Markdown。很多人以为 Markdown 就是“带点符号的纯文本”但在批量迁移场景里它是一门有语法、有嵌套、有资源依赖的标记语言。标题、列表、引用、代码块、表格、图片、链接、数学公式每一种在转换时都有坑。尤其是嵌套列表和表格里塞代码这两个是转换器的照妖镜。第三个是批量上传。批量意味着你要处理的是文件集合而不是单个文件。这就带出了目录遍历、命名冲突、失败重试、限流退避、进度记录、断点续传这一整套工程问题。单个文件上传你可以手动兜底批量上传你必须有自动化容错否则传到第两百篇挂掉你会想砸键盘。适合看这篇内容的人大概分三类一是手里有几十到几千篇 Markdown 需要往飞书搬的开发者或知识库管理员二是想用飞书 OpenAPI 做文档自动化的工程师三是单纯想搞清楚“Markdown 到富文本到底会发生什么”的写作者。不管你是哪一类只要涉及批量下面的思路和坑基本都躲不掉。我个人的判断是这个需求看起来是“格式转换”问题实际上 70% 的工作量在工程化处理30% 才在转换本身。下面我按这个比重来展开。2. 整体方案设计三种路线我为什么选了 API 直传在动手之前我把能想到的路线都试了一遍最后才收敛到“飞书 OpenAPI 直传”这条路上。把三条路线摆出来对比你就能明白为什么另外两条在批量场景下会翻车。2.1 路线一手工粘贴小批量可用批量必死这条路线的逻辑最简单Markdown 编辑器里全选复制飞书云文档里粘贴飞书会自动识别部分 Markdown 语法并渲染成富文本。十篇以内确实能用而且不用写一行代码。但它的问题在批量场景下是致命的。第一图片无法自动上传。Markdown 里的图片是![](./images/a.png)这种相对路径粘贴到飞书时飞书只会把它当成一段文本或者一个无效链接不会去读你本地的文件。你必须手动拖拽图片进去一篇文档里如果有二十张图你就得拖二十次。第二无法保留文档层级。你本地是docs/目录下一堆子文件夹粘贴过去全平铺在云空间根目录还得手动建文件夹、手动移动。第三没有任何可追溯性。传了哪些、漏了哪些、传错没有全靠人脑记。所以手工粘贴这条路线我给的定位是“应急手段”不是“项目方案”。如果你只有个位数文档用它一旦上两位数立刻换路线。2.2 路线二第三方转换工具能用但不可控市面上有不少“Markdown 转飞书文档”的工具有的是浏览器插件有的是桌面软件思路大同小异读取本地 Markdown调用飞书接口创建文档把内容塞进去。这类工具的好处是开箱即用坏处是可控性差。我实际用过两个遇到的问题是共性的一是转换规则不透明某个语法它怎么处理的你不知道出了格式问题只能干瞪眼二是图片处理策略固定通常要求你把图片先传到某个图床或者手动指定上传方式本地相对路径它不会帮你智能解析三是批量能力弱很多工具一次只能处理一篇或者批量时没有进度反馈、没有失败重试。你传三百篇中间挂了五十篇它告诉你“完成”你根本不知道该补哪五十篇。工具适合“我懒得写代码且文档结构简单、图片不多”的场景。但只要你追求可重复、可审计、可定制工具就会成为天花板。2.3 路线三飞书 OpenAPI 直传工程量大但完全可控这是我最终选择的路线核心步骤是四步遍历本地 Markdown 文件 → 解析 Markdown 结构 → 通过飞书 API 创建云文档并写入内容块 → 处理图片等资源的上传与替换。选它的理由很直接。第一可重复。所有转换规则写在我自己的代码里同一批文件传十次结果一致。出了问题我能定位到具体是哪个语法、哪一行代码处理错了。第二可审计。每篇文档处理完我可以记录“文件路径 → 飞书文档 ID → 状态”生成一份迁移清单。传完四百篇我能精确知道每一篇的去向。第三可容错。飞书的接口有频率限制批量上传必然会遇到限流直传方案里我可以加退避重试网络抖动导致的失败我可以记录后重跑而不用从头再来。第四可定制。比如团队要求每篇文档必须带统一的头部信息负责人、最后更新时间、标签我可以在创建文档时自动注入比如某些内部链接需要重写成飞书链接我可以在转换阶段批量替换。这些是工具给不了的。代价当然也有你得处理 Markdown 解析和块结构映射。这是这个项目最硬核的部分也是后面几章的重点。但我还是那句话批量场景下前期多写的代码会在后期省下十倍的时间。2.4 一个容易忽略的前置决策文档往哪放在写代码之前有个决策必须先定下来这些文档在飞书云空间里的组织结构是什么样。我的建议是保留本地的目录结构。本地的docs/guide/install.md和docs/api/auth.md在飞书里也应该有对应的文件夹层级。原因是知识库的价值很大一部分在“可浏览的层级”上如果你全平铺到根目录三百篇文档堆在一起检索体验会非常糟。实现方式有两种一种是在飞书云空间里先用 API 创建对应文件夹再在文件夹下创建文档另一种是文档先创建出来最后再批量移动到目标文件夹。我更推荐前者因为前者在断点续传时状态更清晰——“这个文件夹下应该有哪些文档”是可以对照检查的后者则需要额外维护一份“文档 ID → 目标文件夹”的映射表。定了这个之后整个方案的骨架就清楚了目录树遍历 → 逐目录建文件夹 → 逐文件解析 → 建文档 → 写块 → 传图片 → 记录状态。下面逐个环节拆解。3. Markdown 解析与飞书块结构映射的核心细节这一章是整个项目的技术核心。如果说前面的方案选型是“战略”那这一章就是“战术”直接决定你的文档传过去之后能不能看。我先说一个结论不要试图写一个正则表达式一把梭Markdown 的嵌套结构用正则会让你在第三层嵌套列表上彻底迷失。3.1 为什么必须先把 Markdown 解析成 ASTAST 就是抽象语法树说白了就是把 Markdown 从“一串文本”变成“一棵有层级关系的对象树”。标题是一个节点它下面的段落是它的子节点列表是一个父节点列表项是子节点列表项里如果还有子列表那又是一层子节点。飞书的文档块结构本身就是一棵树每个块有类型标题、段落、列表、代码块、引用、表格等和子块。所以Markdown AST 和飞书块树在结构上是天然对应的你要做的就是把前者的节点类型映射到后者的节点类型。有了 AST嵌套问题自动解决——因为树本身就是递归结构你写一个递归函数就能处理任意深度的嵌套。我用的是 JavaScript 生态里的unified体系具体是remark-parse做解析、remark-gfm支持 GFM 扩展语法表格、任务列表、删除线解析出来是一棵标准的 mdast 树。Python 生态里对应的是markdown-it-py或者mistune思路完全一样。选哪个看你团队的技术栈我要强调的是解析和渲染分离解析只负责把文本变成树渲染只负责把树的节点翻译成飞书块两者不要耦合在一起。这里有个实测经验remark对 CommonMark 的覆盖已经很好了但中文排版里常见的“全角标点 半角符号混用”它是不管语义的纯按字符解析这没问题。真正要注意的是换行语义。Markdown 里一个换行是软换行两个换行才是新段落。很多人在写 Markdown 时习惯性地在一句话中间敲回车到了飞书那边就变成一个莫名其妙的换行。我的处理方式是解析阶段尊重原始语义但在渲染段落块时把软换行统一处理成空格或者保留为换行这个策略要在项目开始时定好因为它影响全文的观感。3.2 块类型的映射表这是你要背下来的东西下面这张表是我实践中总结的核心映射关系你把它存下来写渲染逻辑的时候照着填就行。Markdown 语法mdast 节点类型飞书块类型关键注意点# 标题headingheading1~heading9飞书标题层级有限超出的降级为加粗段落普通段落paragraphtext软换行处理策略需提前定- 列表list / listItembullet嵌套靠父子块关系表达1. 列表list (ordered)ordered起始序号和对齐方式要单独设 引用blockquotequote_container引用内可嵌套其他块codecodecode语言标识要传否则高亮丢失表格tabletable这是最容易翻车的一类![](img)imageimage必须先上传拿 token[文本](url)linktext (含链接)链接是文本块的属性不是独立块**加粗**strongtext (加粗属性)行内样式随文本块走- [ ] 任务listItem (checked)todo需要显式设置完成状态这张表里绝大多数映射都是直接的但有三个地方必须单独拎出来讲因为它们是坑的高发区。3.3 表格映射为什么它是转换器的照妖镜Markdown 表格看起来简单实际转换时问题最多。飞书的表格块是一个独立的容器结构它有自己的行列定义单元格里还可以放文本块。而 Markdown 表格在 AST 里是table → tableRow → tableCell → 文本节点这种层级两者要对应起来需要仔细处理。我踩过的坑有这么几个。第一个坑是合并单元格。标准 Markdown 没有合并单元格的概念但飞书表格有。如果你想让“表头跨列”这种效果Markdown 表达不了转换时会直接按普通单元格处理视觉上突兀。解决办法是接受这个限制或者在特殊文档里手动调整。第二个坑是单元格里的换行和代码。Markdown 表格单元格里如果塞了br或者反引号代码解析出来是行内节点但飞书单元格对行内块的支持有边界。我的处理方式是单元格内只保留纯文本和加粗代码标记降级为普通文本牺牲一点格式换取稳定性。第三个坑是列宽。飞书表格的列宽需要一个初始值如果你不设它会按默认值来长文本列会挤成一团。我一般按内容长度估算一个比例短列如状态列给 80 到 100描述类长列给 300 以上具体在块结构里通过列宽参数设置。顺带说一句经常有人问“Markdown 表格怎么转 Excel”。如果你的目的其实是数据加工那更推荐的路径是先把 Markdown 表格解析成二维数组再用表格类接口直接写入飞书电子表格或多维表格而不是走文档里的表格块。文档表格适合“展示”电子表格适合“运算”这个区别要先想清楚。3.4 代码块映射语言标识不能丢代码块在 mdast 里是code节点带一个lang属性。飞书的代码块支持语言标识传对了才有语法高亮。我见过不少人转换后代码全是灰白色就是因为lang没传或者传了飞书不认识的值。处理逻辑是读取lang做一层映射表把常见别名归一到飞书支持的值。比如js归一成javascriptts归一成typescriptsh归一成shellyml归一成yaml。空lang就传plaintext别留空。还有个细节代码块的内容不要做任何 Markdown 转义处理。你代码里如果写了**bold**那是代码的一部分转义了就错了。渲染代码块时应该原样传递字符串这是和段落渲染最大的区别。3.5 图片处理本地路径必须先上传换 token这是批量上传里最容易导致“半失败”的环节。Markdown 里![](./images/screenshot.png)这个相对路径在飞书眼里毫无意义。正确流程是解析出图片路径相对于当前 Markdown 文件所在目录解析成绝对路径判断路径类型本地文件、网络 URL、还是 base64 内联本地文件先上传到飞书云空间或图片上传接口拿到一个file_token或image_key用这个 token 构造飞书图片块替换掉原来的 image 节点。这里有几个实操要点。网络图片要么直接让飞书去拉如果接口支持要么你先下载到本地临时目录再上传我偏向后者因为可靠且能统一处理。base64 内联图片要先解码成二进制再上传。路径大小写在 macOS 上不敏感在 Linux 上敏感如果你的脚本跑在服务器上一定要确保路径大小写和实际文件一致否则在本地能跑通上了服务器就找不到图。另一个血泪教训图片上传要复用。同一张图可能在多篇文档里出现比如统一的架构图如果你每篇都传一次不仅慢还会在云空间里产生一堆重复文件。我的做法是维护一个“图片路径 → token”的缓存表同一路径的上传结果复用批量和重跑都能省一大截时间。4. 完整的批量上传工程实现前面两章讲的是“怎么转换”这一章讲“怎么跑起来、跑得稳”。单个文档的转换逻辑你写对了不代表批量跑四百篇就顺利因为批量会放大所有偶发问题。4.1 目录遍历与任务队列的构建第一步是把本地文件集合变成一个可追踪的任务列表。我的做法是先扫描目标目录收集所有.md和.markdown文件为每个文件生成一条任务记录包含源文件绝对路径、相对于根目录的路径用来还原目录结构、目标文件夹路径、状态字段待处理、处理中、成功、失败、失败原因。这个任务列表建议落盘成一个 JSON 或 CSV 文件而不是只放内存。原因很简单跑到一半进程崩了或者你手动 CtrlC 了重启后能接着上次的状态继续而不是从头再来。我在第一次做迁移时就吃了这个亏内存里的状态一丢四百篇传了一半根本不知道传了哪些只能全部重传白白多花一个多小时。遍历时要注意两个细节。一是忽略规则node_modules、.git、dist、附件目录这些不该传的目录要排除否则你的任务列表会膨胀出几百个无关文件。二是排序按路径字典序排序让每次运行的顺序一致便于对照日志排查问题。4.2 限流、退避与并发控制飞书的接口是有频率限制的具体阈值随接口和租户情况而变你不需要背下来但你必须假设它会限流并做好应对。我遇到的情况是短时间密集创建文档时某些请求会返回限流错误。处理策略分三层。第一层是串行保底。批量任务我默认用较低的并发度比如同时处理 2 到 3 篇文档。并发太高不仅容易触发限流也会让你的日志乱成一锅粥不利于排查。第二层是退避重试。遇到限流错误时不要立即重试那样只会火上浇油。我的做法是指数退避第一次失败等 1 秒第二次等 2 秒第三次等 4 秒最多重试到 5 次或 30 秒上限。对于明确的限流响应可以读响应头里的建议等待时间按它说的来。第三层是断点续跑。这是兜底。无论你的重试多完善总会有个别文档因为某种原因失败。关键在于失败不影响整体失败的任务留在任务列表里标记为失败等整批跑完后我专门跑一轮“补传”只处理失败的任务。注意退避重试一定要有上限。无限重试在遇到持续性错误比如权限问题、token 过期时会把你的进程卡死日志还刷得飞快。4.3 一个可运行的骨架代码结构下面给出我常用的代码骨架以 Node.js 为例Python 思路一致。这里重点展示流程的组织方式而不是某个具体接口的调用细节因为接口参数可能会变但工程结构是稳定的。// 1. 构建任务列表 async function buildTaskList(rootDir) { const tasks []; const files await walkDir(rootDir, { ignore: [.git, node_modules, assets] }); for (const absPath of files) { if (!/\.(md|markdown)$/i.test(absPath)) continue; tasks.push({ absPath, relPath: path.relative(rootDir, absPath), status: pending, docId: null, error: null, }); } return tasks.sort((a, b) a.relPath.localeCompare(b.relPath)); } // 2. 并发控制 断点续跑 async function runBatch(tasks, concurrency 2) { const queue tasks.filter(t t.status ! success); const running []; for (const task of queue) { const p processOne(task) .then(() { task.status success; }) .catch(err { task.status failed; task.error err.message; }) .finally(() saveTasks(tasks)); // 每篇都落盘最怕状态丢 running.push(p); if (running.length concurrency) { await Promise.race(running); // 清理已完成的 promise for (let i running.length - 1; i 0; i--) { if (running[i].settled) running.splice(i, 1); } } } await Promise.allSettled(running); } // 3. 单篇处理解析 - 建文档 - 写块 - 传图 async function processOne(task) { const raw await fs.readFile(task.absPath, utf8); const ast parseMarkdown(raw); // mdast const docDir await ensureFolder(task); // 确保目标文件夹存在 const doc await createDoc(docDir, titleOf(task)); const blocks await renderToBlocks(ast, path.dirname(task.absPath)); await writeBlocks(doc.docId, blocks); task.docId doc.docId; }这段骨架里有三个设计点值得强调。一是saveTasks放在finally里无论成功失败都落盘这是断点续跑的基础。二是ensureFolder要幂等同一个目录被处理多次只应该创建一个文件夹实现上可以用一个内存 Map 缓存“目录路径 → folder_token”避免重复请求。三是renderToBlocks返回的是块列表飞书写入时可以批量写入减少请求数。4.4 文档头部信息的统一注入团队知识库通常有统一规范比如每篇文档开头要有“维护人、最后更新时间、适用版本、标签”。批量场景下这是一个巨大的优势——你可以在创建文档时自动注入这些信息而不是让每个人手动填。实现思路是在渲染块列表之前先构造一段“头部块”插入到块列表最前面。维护人可以从 Git 提交记录里读也可以从文件名规范里解析实在不行就留空字段等人工补。标签可以按目录名自动打比如所有docs/api/下的文档自动打上“API 文档”标签。反过来说这里有一个陷阱不要把头部信息写成和正文一样的普通段落那样和正文混在一起难以编辑。我通常用一条分割线加一个小标题把头部和正文隔开效果清爽很多。5. 常见问题排查与避坑经验实录批量上传过程中会遇到的问题很多都不是“代码写错了”而是“环境、接口、格式”三者之间的灰色地带。这一章我按问题现象来组织方便你对照排查。5.1 常见问题速查表现象可能原因排查方向解决方式图片全部裂图相对路径未解析成绝对路径检查图片解析阶段的基准目录以 Markdown 文件所在目录为基准解析部分图片裂图路径大小写不一致 / 文件缺失在 Linux 环境跑一遍统一路径大小写缺失文件告警表格排版错乱单元格内含复杂行内结构打印渲染后的块结构降级单元格内行内格式代码块无高亮lang 未传或值不被支持检查代码块节点属性建别名映射表归一语言标识中途大量失败触发接口限流看错误响应类型降并发 指数退避 补传重复文档重跑时未判重检查任务列表状态字段用成功状态跳过已处理项目录全平铺未还原本地目录结构检查 folder_token 逻辑按目录树创建并缓存 folder_token公式变成符号数学公式未特殊处理检查 $ 与 $$ 段落公式转成图片或代码块降级5.2 限流问题的完整应对思路限流这个问题值得单独说因为它是批量任务失败的头号原因也是新手最容易低估的。很多人第一次写批量脚本默认串行、每篇间隔不加控制跑几十篇没问题一上量就被浇冷水。我的完整应对思路是四点。一是把并发压到 2 到 3除非你对接口配额非常有把握否则不要贪快。二是给每个请求加轻量间隔比如每篇文档处理完后等待 200 到 500 毫秒成本不高但能显著降低触发限流的概率。三是识别限流响应并退避不要把所有错误都当限流只有明确的限流响应才走退避其他错误该快速失败就快速失败。四是保留补传能力把失败任务单独跑一轮通常补传的失败率会远低于首轮因为首轮往往有环境预热、缓存未命中等因素。提示判断限流不要只看 HTTP 状态码很多接口会在正常状态码里返回业务错误码务必读响应体的错误字段。5.3 状态管理与幂等性这是批量项目的生命线我见过太多批量脚本第一版功能都能跑通但只要中断一次就废了因为它没有状态管理重跑要么全量重传、要么手工挑。状态管理是这个项目里回报率最高的投入。具体做法是维护一个任务状态文件字段至少包含源路径、目标文档 ID、状态、失败原因、处理时间。每处理完一篇就更新一次。这样带来三个能力一是断点续跑重启后跳过已成功的二是幂等重跑不会产生重复文档三是审计迁移结束后能导出一份完整清单给团队。幂等性的另一个体现是判重策略。最简单的判重是基于本地状态文件但更稳妥的是“状态文件 云端校验”双层判重状态文件告诉你不该重传云端校验告诉你文档确实存在。我在一次迁移里就遇到过状态文件显示成功、但文档被误删的情况如果只信状态文件这篇就永远不会有第二次机会。云端校验的做法是定期抽样比对文档数量或者对关键文档做存在性检查。5.4 内容一致性校验的小技巧批量跑完之后别急着宣布结束。我总是会做一轮抽样校验随机抽 5% 到 10% 的文档打开看重点看三样图片是否显示、代码块语言是否正确、表格是否可读。这三样覆盖了绝大多数转换缺陷。如果团队对一致性要求高还可以做程序化校验把飞书文档的内容拉回来和本地 Markdown 做一次结构和关键字的比对。完全一致不现实格式肯定有差异但可以校验“标题数量、图片数量、代码块数量、链接数量”这些计量指标偏差超过阈值就告警。我用这个方法抓到过一次批量性问题某类目录下的文档全部丢了表格原因是那条目录里的表格用了非标准写法解析器直接忽略了。另外一个容易被忽略的点是特殊字符和转义。中文文档里常见的尖括号、反引号、竖线在某些解析路径下会被误判成语法。批量跑之前先拿几篇“最脏”的文档做样本测试把脏文档跑通了干净的批量文档基本不会出问题。6. 一些工程化之外的取舍和体会写到这里技术环节基本覆盖了。最后想聊几个偏“判断”的东西这些没有标准答案但往往决定项目是顺利交付还是烂尾。第一个判断是要不要追求 100% 无损转换。我的答案是不要。Markdown 和飞书块结构不是一一对应的硬追求无损会让你陷入无尽的边界处理。更务实的目标是“95% 的文档 95% 的内容正确”剩下的交给人工微调。批量迁移的价值是把你从重复劳动里解放出来而不是彻底消灭人工。接受这个现实你的项目会好做很多。第二个判断是迁移是一次性还是长期化。如果你只是迁一次脚本写得糙一点没关系能跑完就行。但如果团队打算长期用“本地 Markdown 写、飞书发布”这个流程那你的脚本就要变成工具需要考虑配置化哪些目录要传、哪些要忽略、命令行参数、日志规范。我后来把这个脚本做成了一个小 CLI团队同学可以自己指定目录和并发度比一个人维护强得多。第三个判断是图片和附件到底放哪。有的团队希望图片也存进飞书云空间便于权限统一管理有的团队有独立的对象存储希望链接指向自己那边。这没有对错取决于你的权限模型。如果选飞书云空间注意盘一下配额如果指向外部存储注意链接的稳定性和访问权限。第四个判断是要不要做成定时同步。我见过一些团队把“Markdown 仓库 → 飞书文档”做成定时任务每天同步一次变更。这个想法很好但前提是你要能识别“哪些文件变了”也就是基于文件修改时间或 Git diff 来做增量。直接全量跑定时任务会浪费大量配额也可能把人工在飞书端做的修改覆盖掉。我的建议是第一版先做手动触发的全量迁移跑稳了再考虑增量同步且增量必须建立在“知道哪些文件动过”的机制上。再说个很小的实操细节但实测能省不少事处理日志不要只打console.log。批量任务的日志会非常长全打在终端里你会滚屏滚到失去耐心。我一般把详细日志写进文件终端只输出进度条和失败汇总比如“已处理 120/400成功 116失败 4”。失败明细单独一个文件补传时直接读那个文件清爽高效。还有一点关于编码的提醒你的 Markdown 文件可能有 BOM 头也可能混着 CRLF 和 LF 换行符这些在批量场景下会偶发地导致某几篇文档开头多出奇怪字符或者段落换行异常。我的做法是在读取阶段统一做一次清洗去掉 BOM换行统一成 LF。这一步几乎零成本但能消除一类很隐蔽的问题。最后分享一个我踩过最深的坑也是最能体现“批量”和“单个”区别的地方。我第一版脚本单篇测试全部通过格式完美于是我信心满满地跑了全量。结果一半文档失败。排查了半天发现根因是脚本里有一个全局的图片 token 缓存对象我在写的时候没有考虑并发多个文件同时读到未命中的图片、同时发起上传、同时写缓存最后缓存里的状态是乱的导致后续引用错乱。这个 bug 在单篇场景下永远复现不了。从那之后我给自己立了个规矩任何批量脚本缓存和共享状态必须考虑并发安全能用不可变 Map 就别用可变对象随意改。这个项目到今天我已经用它迁过好几批不同结构的文档集从几百篇到两千多篇都有。回头看真正难的不是某一个 API 怎么调而是把“解析、转换、上传、容错、审计”这几件事组织成一个能重复运行、能中断恢复、能定位问题的整体。只要你把任务状态管住、把限流退避做足、把图片路径处理对这个项目八成就能跑通而且跑得比你想的稳。
返回列表