批量导出 Markdown:清洗文件名之后,还要处理第二次碰撞

举报
yd_212881929 发表于 2026/10/23 10:23:12 2026/10/23
【摘要】 以八组实际运行结果检查 Markdown 批量导出的文件名碰撞:清洗、大小写、Unicode 规范化、后缀预占和跨批次限制。

本文基于“笔记铸造台”的实际源码与本地运行结果整理,由 AI 辅助撰写和核验。讨论的是一批 JSON 记录映射为 Markdown 下载文件名的规则,不涉及读取或修改用户本地文件。

把 JSON 数组转换成多篇 Markdown,看起来只需选择一个 id 字段,再拼上 .md。真正容易遗漏的地方是:输入中的两个 id 不相同,输出文件名仍可能相同。更隐蔽的是,自动生成的重名后缀,还会撞上后续记录本来就有的名称。

这次我用已有工具的核心函数跑了八组输入,重点检查清洗、大小写、Unicode 规范化和后缀分配。源码是公开仓库 Auxior/auxior-note-forge 中的单文件 index.html,核心逻辑位于 script#core。

1. 清洗是多对一映射

工具把路径分隔符、Windows 不允许的部分字符及 ASCII 控制字符替换为连字符,并去掉两端的空格和句点。

例如:

A/B  → A-B
A:B  → A-B

所以不能只对原始 id 做唯一性检查。检查必须发生在清洗之后,并使用与后续输出一致的比较规则。

工具先做 NFC 规范化,再用 toLowerCase() 产生比较键,因此 Note 与 note 会被视为同一组冲突。预组合的 Café 和使用组合重音符的 Cafe\u0301 也会进入同一组。

这里的规则是应用自己选择的保守策略,不等于所有操作系统的文件名等价规则。JavaScript 的小写转换也不是完整的 Unicode case folding,不能据此声称覆盖了全部跨平台情况。

2. 一个 used 集合为什么不够

考虑这一批原始 id:

["A/B", "A:B", "A-B (1)", "A-B (2)", "A-B"]

如果分配器只记住“已经输出的名字”,处理第二条时会自然地选择 A-B (1).md。这会占用第三条记录原本的目标名,迫使第三条再改名。

这样的算法未必会覆盖数据,但会让自动后缀抢占后续原名,映射关系变得难解释。工具因此使用两个集合:

  • originals:整批输入清洗后的全部目标名。
  • used:本次转换已经分配出去的目标名。

第一条原名直接保留。只有出现重复需要自动加后缀时,才要求候选名同时避开这两个集合。核心步骤可以概括为:

let n = 1;
do {
  target = name + ' (' + n++ + ')';
} while (used.has(key(target)) || originals.has(key(target)));

实际运行得到:

A-B.md
A-B (3).md
A-B (1).md
A-B (2).md
A-B (4).md

第二条跳过了整批输入已经预占的 (1)、(2);第五条又避开已经生成的 (3)。原本带后缀的第三、第四条保留自己的名称。

这套结果依赖输入顺序,并不是内容哈希命名。若导出需求要求重排后仍得到同一映射,需要额外定义稳定排序或稳定唯一键,不能把当前策略当成永久标识方案。

3. 八组运行结果

本次通过 Node.js 从 HTML 提取核心脚本,在独立上下文中调用公开导出的 convert 函数;没有改动核心算法。以下均为实际结果:

输入 策略 结果
A/B、A:B 报错 第二条提示清洗后重名,整批阻止
上述五条后缀案例 保留两份 输出 (3)、原始 (1)、原始 (2)、(4),五个名称唯一
Note、note 保留两份 Note.md、note (1).md
Café、Cafe\u0301 保留两份 Café.md、Café (1).md
.. 保留两份 清洗为空,阻止输出
CON.txt.more 保留两份 识别为 Windows 保留名称,阻止输出
A/B、A:B 跳过后项 仅保留 A-B.md,返回跳过提示
../../folder:note 保留两份 -..-folder-note.md

最后一行保留了内部的两个句点,但所有路径分隔符已经替换掉,输出作为一个 basename 使用。在本工具中,这个名字仅交给浏览器下载属性;浏览器仍可能调整下载文件名。

如果以后改成服务端写入目录,还必须校验解析后的目标路径位于允许目录内,并处理已有文件、符号链接和并发写入,不能只沿用这段清洗函数就认为路径安全已经解决。

4. 冲突策略应让用户看见

工具提供三种策略。“报错阻止”适合要求一条记录对应一个明确文件名的场景;“保留两份”保留全部记录并展示改名通知;“跳过后项”会减少输出条数,所以必须明确提示被跳过的记录。

清洗后的空名、保留名及过长名称都在生成前检查。原始名称加 .md 的 UTF-8 长度上限为 220 字节,添加后缀后的上限为 240 字节。这是工具自行设置的保守阈值,不是所有文件系统的通用上限。

这次验证只覆盖同一批次内的命名分配。工具没有扫描下载目录,也不知道昨天是否已经下载过同名文件,更没有跨批次覆盖保护。浏览器最终落盘行为不能由这套内存集合保证。

5. 把验证目标写清楚

批量导出的验收不能停在“按钮能下载”。至少应检查:清洗后发生碰撞时是否可见;自动后缀是否避开后续原名;大小写与组合字符是否按约定处理;跳过策略是否汇报数量变化;修改输入或策略后,旧结果是否失效。

当产品明确区分“输入记录”“清洗后目标名”“最终分配名”这三个阶段,命名规则就更容易验证,用户也更容易理解每个下载文件来自哪条记录。

源码:https://gitcode.com/Auxior/auxior-note-forge

作品:https://gallery.developer.huaweicloud.com/gallery/0069b28e96c00000

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。