
很多人在写一篇几万字的个人经历长文时第一反应是打开 Word 或者在线文档从头写到尾。写到一半发现结构乱了想大改只能靠“另存为 最终版 2 修订版”想投给几个不同的平台每个平台对标题层级、字数、配图位置、正文格式的要求都不一样只能人工复制粘贴反复调整更麻烦的是文章里写到的真实姓名、电话、住址、单位、时间线稍不注意就在公开发布的版本里留了下来。这些问题跟文笔无关本质上是写作过程缺少工程管理。这篇文章要讲的就是一套用技术手段管理长文创作与投稿的完整方案用 Markdown 作为唯一的内容来源用 Git 管理版本和备份用脚本做发布前的隐私自检和格式导出用静态站点托管可控的发布渠道用自动化流水线把重复劳动交给机器。它的目标读者不是“想学写作技巧的人”而是已经写了一篇长文正卡在整理、修改、投稿、保密这几个环节上的人。先给一个明确判断对于超过一万字、需要反复修改、需要投递多个渠道、并且包含大量真实个人信息的长文用“文档软件 手工复制”的方式管理出错概率远高于收益。你省下的那点学习成本会在一次隐私泄露或者一次版本覆盖里全部还回去。而下面这套方案的学习成本熟悉命令行的人一个下午就能跑通不熟悉的人照着做也能在两天内成型。全文按“为什么需要—概念与选型—环境准备—工作流拆解—两个核心脚本—发布与验证—问题排查—长期实践”的顺序展开每一节都能单独拿去用。代码和配置都可以直接复制运行重点不是炫技而是让你在写完之后能安全、完整、低损耗地把稿子交出去。1. 为什么“写长文”这件事值得用工程方法解决先说一个很常见的场景。一篇两万字的个人经历长文写到第三稿的时候作者通常已经分不清哪一版是最新的。改一个段落要来回翻找前后文删掉一段回忆又担心以后想加回来。这种状态下的写作效率是极低的因为每改一次都要重新建立上下文而人脑并不擅长在几万字里做精确的文本定位。再看投稿环节。不同渠道对稿件的格式要求差别很大有的要求纯文本、段落之间空一行有的要求 Markdown、标题用##有的要求 Word 文档、正文小四号字还有的要求先提交一个大纲和字数统计。如果每一份都要手工整理一份稿子投五个渠道就是五次重复劳动而且每次都可能漏掉某一段或者把不同版本的句子混进去。这种损耗看起来不起眼实际上是把创作精力花在了搬运上。最容易出事的其实是隐私。个人经历类文章天然包含大量可识别信息真实姓名、亲属称谓、具体地址、工作单位、时间节点、电话号码、社交账号甚至聊天截图里的头像和昵称。这些东西在写作时是必要的细节在公开发布时却是风险。靠人眼通读一遍去检查几乎一定会漏。而且很多平台一旦发布即使后续删除也可能已经被转载或缓存。所以这套方案要解决的是三个具体问题版本不混乱、格式不重复劳动、隐私不漏发。这三件事都有成熟的技术手段可以覆盖而且不需要你成为程序员。你只需要接受一个前提把文章当成一份需要被管理的文件而不是一个只能存在于文档软件里的东西。2. 核心概念与技术选型把文章当作代码来管理这套方案由四个部分组成每个部分解决一个问题彼此之间通过“纯文本文件”这个共同格式串起来。下面逐个解释它们是什么、为什么选它、以及它在整个流程里处于什么位置。2.1 Markdown唯一的内容来源Markdown 是一种用纯文本表示排版的标记语法比如## 标题表示二级标题**加粗**表示加粗。它的关键优势不是“好看”而是纯文本任何编辑器都能打开任何版本管理工具都能比较差异任何转换工具都能读它。这意味着你的稿子不会被某个软件绑架十年后依然能打开。在长文写作中Markdown 带来的最实际的好处是结构可见。当你用## 1.## 2.标记章节时文章骨架一目了然调整章节顺序只需要剪切粘贴几行。相比之下富文本编辑器里的样式是隐式的你看到的是“看起来像标题”但机器不知道它是标题导出时就容易乱。需要说明的是Markdown 不是排版工具它只描述结构。最终呈现成什么样由渲染它的地方决定。这正是我们要的效果一份内容多种输出。2.2 Git版本管理 异地备份Git 是分布式版本控制系统最初为代码协作设计。用在写作上有三个直接收益第一每次提交都是一个可回溯的存档点改错了可以精确回退到某一个版本第二git diff能逐行显示你这次改了什么比肉眼对比两版文档可靠得多第三推送到远程仓库等于多了一份异地备份。很多写作者对 Git 有心理负担觉得是程序员专属。实际上写作场景只需要五条命令init、add、commit、log、diff。学会这五条就足够管理一篇文章的完整生命周期。分支、合并、变基这些可以先完全不管。这里有一个重要提醒如果文章包含真实个人信息不要推到公开仓库。Git 的历史是永久的即使后来删除文件历史提交里依然存在。这一点在第 7 节会详细展开。2.3 静态站点可控的发布渠道静态站点是指提前生成好 HTML 文件、不需要数据库和后端程序的网站常见工具有 Hugo、Jekyll、Hexo 等。对写作者来说它的价值在于文章以 Markdown 存放构建后变成网页部署成本极低并且你能完全控制哪些内容公开、哪些内容只在本地。对于个人经历长文静态站点有两个典型用法。一是作为“正式版”的归档地投稿被采用后自己保留一份带日期的版本二是作为草稿预览环境在投出去之前先看看排版效果。它不替代投稿渠道而是给你一个不依赖第三方的存档位置。2.4 导出层把一份内容变成多种格式导出层是几个小脚本和模板负责把 Markdown 转换成投稿需要的格式纯文本、带 front matter 的 Markdown、字数统计报告、分段文件等。这部分不需要复杂框架Python 标准库加上一两个转换工具就够用。下表把四个组成部分的定位、解决的问题和上手难度放在一起对比方便判断优先级组成部分主要解决的问题典型工具上手难度建议优先级Markdown内容与格式解耦、结构清晰任意文本编辑器低必选Git版本回溯、异地备份git 命令行中强烈建议静态站点可控发布、草稿预览Hugo / Jekyll中可选导出脚本多平台格式转换、字数统计Python中投稿多时必选选型上有一个原则值得强调优先选纯文本和本地工具避免把唯一副本放在某个在线服务里。在线文档协作方便但它的导出格式、权限模型、服务可用性都不由你控制。长文这种“写一次、存很多年”的东西本地纯文本 版本管理是最稳的底座。3. 环境准备与前置条件这一节说明跑通整套流程需要什么。版本号只给出建议区间具体以你机器上实际安装的为准本文重点演示通用思路不绑定某个特定版本。3.1 操作系统与基础工具Windows、macOS、Linux 都可以。Windows 用户建议使用 Git Bash 或 WSL因为后面的命令都是 Unix 风格。需要准备的东西不多一个文本编辑器。VS Code、Typora、Obsidian 均可。VS Code 的优势是内置 Git 面板和 Markdown 预览对新手最友好。Git。用于版本管理。Python 3.10 及以上。用于运行导出和自检脚本。标准库足够不需要额外装包。可选Hugo用于本地预览静态站点。如果你的文章里有大量图片还需要准备一个图片目录并在 Markdown 里用相对路径引用。这一点在第 4 节的目录结构里会体现。3.2 安装与版本确认不同系统的安装方式不同下面给出一组通用命令按需选择。安装完成后必须确认版本避免后面脚本因为版本过低报错。# 文件路径终端直接执行 # 1. 确认 Git 是否已安装 git --version # 2. 确认 Python 版本脚本要求 3.10 python3 --version # 3. 可选确认 Hugo 是否已安装 hugo version # macOS 使用 Homebrew 安装缺失项 brew install git python hugo # Ubuntu / Debian sudo apt update sudo apt install -y git python3 python3-pip # Windows 建议先安装 Git for Windows然后在 Git Bash 中执行上面的确认命令版本确认这一步经常被跳过结果是脚本跑起来才发现语法不兼容。比如 Python 3.9 之前不支持某些类型标注写法如果脚本里用了list[str]之外的更激进语法就会直接报错。先花三十秒确认版本比事后排查十分钟划算。3.3 创建写作仓库仓库就是一个被 Git 管理的文件夹。建议单独建一个只放这一篇长文及其素材不要和别的项目混在一起。# 文件路径终端直接执行 # 创建一个新的写作目录 mkdir -p ~/writing/long-story cd ~/writing/long-story # 初始化 Git 仓库 git init # 配置提交身份只需一次全局配置 git config --global user.name Your Name git config --global user.email youexample.com # 创建基础目录结构 mkdir -p drafts chapters assets/images exports scripts # 添加一个 .gitignore避免把中间产物提交进去 cat .gitignore EOF exports/*.txt exports/*.docx .DS_Store *.tmp EOF # 首次提交 git add . git commit -m chore: 初始化写作仓库这里有一个细节需要注意.gitignore排除的是exports目录下的中间产物而不是源文件。源文件Markdown必须全部纳入版本管理否则就失去了回溯的意义。中间产物随时可以重新生成没必要占用仓库空间。4. 目录结构与写作工作流拆解结构决定效率。一篇长文如果没有清晰的目录约定写到后期一定会出现“这段到底属于哪一章”“这个素材放哪了”的问题。下面这套结构经过实际使用验证能覆盖从草稿到投稿的完整流程。4.1 推荐的目录结构long-story/ ├── drafts/ # 草稿区允许混乱 │ ├── 2024-01-notes.md # 素材、片段、灵感 │ └── outline.md # 大纲随时调整 ├── chapters/ # 正式章节按顺序命名 │ ├── 01-opening.md │ ├── 02-background.md │ ├── 03-turning-point.md │ └── 99-ending.md ├── assets/ │ └── images/ # 文中引用的图片 ├── scripts/ │ ├── privacy_check.py # 隐私自检 │ └── export.py # 多格式导出 ├── exports/ # 导出产物不纳入版本管理 └── README.md # 写作说明与投稿记录这个结构的关键是把“草稿”和“正式章节”分开。草稿区允许写得很乱想到什么写什么正式章节区按编号排列最终合并时按文件名排序即可。这样做的直接好处是你永远知道该往哪个文件里写也永远不会因为一次误操作丢掉全部内容。README.md容易被忽略但在投稿场景里很有用。可以在里面记录每个渠道的投稿时间、要求、状态、反馈。这比在备忘录里零散记录可靠得多。4.2 一次完整的写作循环从“改一段话”到“投出去”一次完整的循环包含六步。理解这个循环比记住任何命令都重要。第一步是开工前同步。打开终端进入目录执行git status看看有没有上次忘记提交的改动。如果有先提交或丢弃保证工作区干净。第二步是写作与拆分。在chapters/下修改对应章节。如果某一段暂时没想好就写一个TODO:标记不要停下来硬憋。长文写作的节奏比句子质量更重要。第三步是阶段提交。写完一个完整意思比如解决了某一节的逻辑问题就提交一次。提交信息写清楚改了什么比如feat: 补充第三章的转折细节。这一步的意义是建立存档点不是为了好看。第四步是合并与自检。把所有章节合并成一个完整文件然后运行隐私自检脚本。这一步不能省原因在第 5 节会详细说。第五步是导出投稿版本。根据目标渠道的要求生成对应格式同时生成字数统计方便填写投稿表单。第六步是记录与归档。把这次投稿的信息写进README.md给当前版本打一个 tag比如git tag v1-submit-2024xx。这样以后对方说“投的是哪一版”你能立刻找出来。这六步看起来繁琐实际跑熟之后每一步都是几秒钟的事。它带来的最大变化是你不需要再依赖记忆去管理稿子。记忆是最不可靠的存储介质而版本库不是。5. 完整示例发布前的隐私自检脚本这一节给出第一个核心脚本。它的作用是把所有章节合并后扫描文本中可能泄露个人身份的信息输出一份报告让你在发布前逐条确认。它不是万能的但能覆盖绝大多数低级失误。5.1 为什么要用脚本检查人眼通读检查隐私有两个天然缺陷。一是注意力衰减读到第五千字时注意力已经大幅下降对电话号码这类模式化信息的敏感度会明显降低。二是上下文干扰一段文字在叙事上很自然你读的时候关注的是情节不会主动去想“这个地名加上这个单位名称是不是能定位到具体的人”。脚本的优势恰好在于它没有注意力问题也不会被叙事带走。它只做一件事按规则匹配命中就报告。至于命中项是否真的需要修改仍然由你判断。5.2 脚本实现脚本放在scripts/privacy_check.py只用标准库不依赖第三方包。# 文件路径scripts/privacy_check.py 发布前隐私自检脚本扫描合并后的长文报告可能泄露个人身份的信息。 from __future__ import annotations import argparse import re from dataclasses import dataclass from pathlib import Path dataclass class Rule: 一条检测规则。 name: str pattern: re.Pattern[str] suggestion: str RULES: list[Rule] [ Rule( name手机号, patternre.compile(r(?!\d)1[3-9]\d{9}(?!\d)), suggestion替换为“某号码”或做部分掩码处理, ), Rule( name身份证号, patternre.compile(r(?!\d)\d{17}[\dXx](?!\d)), suggestion整段删除或改为“身份证号已隐去”, ), Rule( name邮箱地址, patternre.compile(r[A-Za-z0-9._%-][A-Za-z0-9.-]\.[A-Za-z]{2,}), suggestion改为不可用的示例地址或直接删除, ), Rule( name银行卡号, patternre.compile(r(?!\d)\d{16,19}(?!\d)), suggestion确认是否为卡号是则整段删除, ), Rule( name详细门牌地址, patternre.compile(r[\u4e00-\u9fa5]{2,10}(路|街|巷|弄|号院|小区)\d{1,5}(号|栋|楼|室)), suggestion降级为“某小区”级别的模糊表述, ), Rule( name疑似 URL, patternre.compile(rhttps?://[^\s)]), suggestion确认链接是否指向包含个人信息的页面, ), ] def scan_text(text: str, source: str) - list[tuple[Rule, int, str]]: 扫描单段文本返回 (规则, 行号, 命中片段) 列表。 hits: list[tuple[Rule, int, str]] [] for line_no, line in enumerate(text.splitlines(), start1): for rule in RULES: for match in rule.pattern.finditer(line): hits.append((rule, line_no, match.group(0))) return hits def build_report(target: Path) - str: 合并目录下所有 Markdown 文件并生成报告。 files sorted(target.rglob(*.md)) if target.is_dir() else [target] if not files: return f未找到任何 Markdown 文件{target} lines: list[str] [] total_hits 0 for file_path in files: text file_path.read_text(encodingutf-8) hits scan_text(text, str(file_path)) if not hits: continue lines.append(f\n文件{file_path}) for rule, line_no, snippet in hits: total_hits 1 masked snippet[:3] * * max(len(snippet) - 3, 0) lines.append( f 第 {line_no} 行 | 规则{rule.name} | 命中{masked} | 建议{rule.suggestion} ) header [ * 60, 隐私自检报告, * 60, f扫描文件数{len(files)}, f命中总数{total_hits}, ] if total_hits 0: header.append(结果未发现规则内命中的敏感信息仍建议人工通读一遍。) else: header.append(结果存在命中项请逐条确认后再发布。) return \n.join(header lines) def main() - None: parser argparse.ArgumentParser(description发布前隐私自检) parser.add_argument( target, nargs?, defaultchapters, help要扫描的文件或目录默认 chapters, ) parser.add_argument( --output, defaultexports/privacy_report.txt, help报告输出路径, ) args parser.parse_args() report build_report(Path(args.target)) output_path Path(args.output) output_path.parent.mkdir(parentsTrue, exist_okTrue) output_path.write_text(report, encodingutf-8) print(report) print(f\n报告已写入{output_path}) if __name__ __main__: main()5.3 关键逻辑说明脚本的设计有三个考虑。第一规则集中定义。所有正则放在RULES列表里想加新规则只改这一处不用动扫描逻辑。实际使用中你可能会根据自己文章的内容补充规则比如“疑似学校全称”“疑似车牌号”直接往列表里加一条即可。第二命中片段做掩码。报告里不直接打印完整号码只保留前三位加星号。这样做是为了防止报告文件本身变成新的泄露源——很多人会把报告截图发出去问别人如果报告里是完整号码等于又泄露了一次。第三没有命中不等于安全。脚本只能匹配有固定模式的字符串无法识别“某个特定小区名字 某年某月 某职业”这种组合式定位。所以报告结尾始终保留人工通读的建议这个提示不能删。5.4 运行与验证在仓库根目录执行下面命令观察输出是否符合预期。# 文件路径仓库根目录执行 # 扫描 chapters 目录 python3 scripts/privacy_check.py chapters # 也可以只扫描单个合并后的文件 python3 scripts/privacy_check.py merged.md --output exports/privacy_report.txt如果报告显示“命中总数0”说明规则内没有匹配项。你可以先在drafts/里临时写一行测试内容验证脚本确实在工作例如包含一个虚构的 11 位号码跑完之后确认脚本能命中再把这行删掉。这个“先验证工具本身有效”的习惯很重要否则你永远不知道零命中是真的干净还是脚本根本没跑起来。6. 完整示例多平台投稿格式导出第二个脚本解决重复劳动问题把合并后的长文一键导出成多种投稿格式并同时生成字数统计和分段文件。这是整套流程里节省时间最明显的一环。6.1 导出脚本实现脚本放在scripts/export.py同样只用标准库。# 文件路径scripts/export.py 把 chapters 目录下的章节合并导出为多种投稿格式。 from __future__ import annotations import argparse import re from datetime import date from pathlib import Path def merge_chapters(chapter_dir: Path) - tuple[str, list[Path]]: 按文件名排序合并所有 Markdown 章节。 files sorted(chapter_dir.glob(*.md)) if not files: raise FileNotFoundError(f目录中没有 Markdown 文件{chapter_dir}) parts [f.read_text(encodingutf-8).strip() for f in files] return \n\n.join(parts) \n, files def count_chars(text: str) - dict[str, int]: 统计字数总字符、去空白字符、中文字符、段落数。 no_space re.sub(r\s, , text) chinese re.findall(r[\u4e00-\u9fa5], text) paragraphs [ p for p in re.split(r\n\s*\n, text.strip()) if p.strip() ] return { 总字符数: len(text), 去空白字符数: len(no_space), 中文字符数: len(chinese), 段落数: len(paragraphs), } def to_plain_text(md: str) - str: 把 Markdown 降级为纯文本去掉常见标记。 text re.sub(r^#{1,6}\s*, , md, flagsre.MULTILINE) # 标题符号 text re.sub(r\*\*(.?)\*\*, r\1, text) # 加粗 text re.sub(r\*(.?)\*, r\1, text) # 斜体 text re.sub(r([^]), r\1, text) # 行内代码 text re.sub(r!\[.*?\]\(.*?\), , text) # 图片引用 text re.sub(r\[(.?)\]\(.*?\), r\1, text) # 链接 text re.sub(r^\s?, , text, flagsre.MULTILINE) # 引用 text re.sub(r\n{3,}, \n\n, text) # 多余空行 return text.strip() \n def to_front_matter_doc(md: str, title: str) - str: 生成带 front matter 的 Markdown适合静态站点或支持元数据的平台。 head ( ---\n ftitle: \{title}\\n fdate: {date.today().isoformat()}\n draft: true\n ---\n\n ) return head md def split_by_heading(md: str) - dict[str, str]: 按二级标题切分成多个文件便于分段投稿。 sections: dict[str, str] {} current preamble buffer: list[str] [] for line in md.splitlines(): if line.startswith(## ): if buffer: sections[current] \n.join(buffer).strip() current line[3:].strip() or fsection-{len(sections) 1} buffer [line] else: buffer.append(line) if buffer: sections[current] \n.join(buffer).strip() return sections def safe_name(name: str, index: int) - str: 生成安全的文件名。 cleaned re.sub(r[^\w\u4e00-\u9fa5-], _, name).strip(_) return f{index:02d}_{cleaned[:40] or section}.md def main() - None: parser argparse.ArgumentParser(description长文多格式导出) parser.add_argument(--title, default未命名长文, help文章标题) parser.add_argument(--chapter-dir, defaultchapters, help章节目录) parser.add_argument(--out-dir, defaultexports, help输出目录) args parser.parse_args() chapter_dir Path(args.chapter_dir) out_dir Path(args.out_dir) out_dir.mkdir(parentsTrue, exist_okTrue) merged, files merge_chapters(chapter_dir) (out_dir / merged.md).write_text(merged, encodingutf-8) (out_dir / merged_plain.txt).write_text( to_plain_text(merged), encodingutf-8 ) (out_dir / merged_with_meta.md).write_text( to_front_matter_doc(merged, args.title), encodingutf-8 ) stats count_chars(merged) stat_lines [f合并章节数{len(files)}] stat_lines [f{k}{v} for k, v in stats.items()] (out_dir / stats.txt).write_text(\n.join(stat_lines) \n, encodingutf-8) sections_dir out_dir / sections sections_dir.mkdir(exist_okTrue) for index, (heading, content) in enumerate(split_by_heading(merged).items(), 1): (sections_dir / safe_name(heading, index)).write_text( content \n, encodingutf-8 ) print(\n.join(stat_lines)) print(f\n导出完成输出目录{out_dir.resolve()}) if __name__ __main__: main()6.2 关键逻辑说明merge_chapters依赖文件名的字典序所以章节文件必须以两位数字开头命名否则10-xxx.md会排在2-xxx.md前面。这是最容易踩的坑第 9 节会再提一次。to_plain_text只处理常见的行内标记不追求完整的 Markdown 解析。这是有意为之投稿平台要的纯文本通常也不需要复杂格式过度处理反而可能破坏正文内容。如果你的文章里有大量表格、代码块建议在导出后人工检查一遍。count_chars同时给出四个口径。不同渠道对“字数”的定义不一样有的算空格有的不算有的只算中文。四个数字都留着填表时按需要选择比事后重新统计省事。split_by_heading按二级标题切分主要用于两种情况一是某些平台要求分章节提交二是你想单独检查某一章的内容是否完整。切分后的文件名做了安全处理避免标题里的标点导致文件系统报错。6.3 运行与验证# 文件路径仓库根目录执行 python3 scripts/export.py --title 一段个人经历的完整记录 # 查看导出结果 ls -lh exports/ cat exports/stats.txt head -n 20 exports/merged_plain.txt预期输出类似下面这样数字取决于你的实际内容合并章节数8 总字符数18742 去空白字符数17103 中文字符数16240 段落数213 导出完成输出目录/home/user/writing/long-story/exports验证成功的标准有三条exports/merged.md的章节顺序与预期一致stats.txt里的中文字符数与你的直觉大致相符差太多说明有章节没被读到merged_plain.txt里没有残留的##或**。如果merged.md顺序不对先检查章节文件名前缀。7. 静态站点可控的草稿预览与归档如果你需要看排版效果或者想保留一个不带平台广告的正式版归档静态站点是最省事的方案。这里以 Hugo 为例其他工具思路类似。7.1 初始化站点与配置# 文件路径仓库根目录执行 # 初始化站点到 site 目录 hugo new site site --force # 创建一个内容文件 hugo new content story/index.md # 本地预览默认地址 http://localhost:1313/ hugo server --buildDrafts配置文件site/hugo.toml里需要关注的几项# 文件路径site/hugo.toml baseURL https://example.com/ languageCode zh-cn title 写作归档 disableKinds [taxonomy, term, RSS, sitemap] [build] writeStats true [markup.goldmark.renderer] unsafe falsedisableKinds关闭了分类页和 RSS个人归档场景通常用不到关掉能减少无关页面。unsafe false表示不允许 Markdown 中直接嵌入原始 HTML这是一条安全默认值如果你的稿子里有从网页复制的内容可能夹带脚本标签关闭它可以把这类风险挡在渲染层之外。7.2 草稿与公开内容的边界Hugo 的 front matter 里有draft: true字段标记为草稿的内容默认不会出现在构建结果里只有加--buildDrafts才能在本地看到。这个机制非常适合长文场景未完成的章节保持草稿状态确认可以公开的才改为 false。需要提醒的是draft只是构建时的过滤它不能替代访问控制。如果你的站点部署在公开网络上任何被构建出来的页面都是可访问的。因此包含真实个人信息的版本不要构建、不要部署只保留在本地仓库里。8. 自动化把自检和导出串成一条命令手动执行两个脚本容易忘。可以用一个简单的 shell 脚本把“合并、自检、导出”串起来再配合 Git 的钩子在提交时自动跑一次检查。# 文件路径scripts/build.sh #!/usr/bin/env bash set -euo pipefail echo [1/3] 隐私自检... python3 scripts/privacy_check.py chapters --output exports/privacy_report.txt echo [2/3] 检查报告是否有关键命中... if grep -q 命中总数0 exports/privacy_report.txt; then echo 未发现规则内命中项。 else echo 存在命中项请打开 exports/privacy_report.txt 逐条确认。 fi echo [3/3] 导出投稿格式... python3 scripts/export.py --title ${1:-未命名长文} echo 完成。输出位于 exports/赋予执行权限后运行# 文件路径仓库根目录执行 chmod x scripts/build.sh ./scripts/build.sh 一段个人经历的完整记录这个脚本刻意没有在发现命中时直接中断。原因是隐私判断需要人来做某些命中可能是误报比如文章中引用的公开客服电话、示例邮箱。让脚本报告、让人决策比让脚本一刀切更实用。如果你的团队或合作编辑要求强制拦截把else分支里的exit 1加上即可。9. 常见问题与排查思路下面这些问题在实际使用中出现的频率最高按“现象—原因—排查—解决”整理成表遇到时可以直接对照。问题现象可能原因排查方式解决方案合并后章节顺序错乱章节文件名前缀位数不一致执行ls chapters/看排序统一改为两位数前缀如01、02脚本报FileNotFoundError章节目录为空或路径写错确认chapters/下有.md文件检查--chapter-dir参数或先创建章节字数统计明显偏少部分章节未匹配到*.md后缀查看stats.txt里的“合并章节数”统一文件后缀避免.markdown混用导出的纯文本仍带##标题符号前有空格或用了全角符号用grep -n ^ *#定位统一使用半角##且顶格书写隐私报告零命中但确有号码号码被空格或连字符分隔手工搜索前几位数字补充规则或在写作时避免拆分号码Hugo 构建后看不到文章front matter 里draft: true查看文章头部元数据确认可公开后改为falseGit 提交后发现包含隐私检查发生在提交之后git log -p查看历史在提交前跑build.sh必要时重写历史中文文件名在 Windows 下乱码终端编码与文件编码不一致用编辑器确认文件为 UTF-8统一保存为 UTF-8终端设置对应编码表格之外还有两个需要单独说明的点。第一个是隐私检查的时机。很多人习惯在“准备发布”时才检查这时候文章已经定稿改起来心理成本很高容易抱着侥幸心理放过某些命中项。更稳妥的做法是在每个章节写完后就跑一次针对该章节的检查问题在上下文还热的时候处理改动阻力最小。第二个是Git 历史里的隐私无法靠删除文件解决。如果已经提交了包含真实信息的版本仅仅删掉文件再提交一次是不够的历史提交里依然完整保留。处理方式有两种如果仓库从未推送到远程可以重建仓库如果已经推送需要用git filter-repo之类的工具重写历史并且要意识到任何已经拉取过的人本地仍有副本。这也是为什么检查必须前置到提交之前。10. 最佳实践与长期维护建议工具跑通只是起点真正决定这套方案能不能长期用的是习惯。下面几条建议来自实际使用中的反复踩坑按重要性排序。第一把“提交”当成存档而不是任务。不要为了凑提交次数去提交无意义的改动也不要在写完一大段之后忘记提交。判断标准很简单如果现在电脑突然坏掉你损失的内容是否超过十分钟的工作量。超过就提交。第二备份遵守三份原则。本地仓库一份远程私有仓库一份离线介质移动硬盘或加密压缩包一份。三份里至少有一份不在同一台设备上。个人经历类长文往往写了很多年丢失的代价远高于任何技术成本。第三隐私处理要“写到就模糊”而不是“发之前再改”。写作时直接用“某年冬天”“南方一个中等城市”“一位亲戚”这类表述比写完再回头逐条替换省力得多也不容易漏。真实的细节留在你自己的记忆和未公开的草稿里公开版本不需要精确到可以定位。第四投稿记录要结构化。在README.md里维护一张表渠道名称、投稿日期、要求格式、当前状态、反馈要点。这看起来是小事但当你在三个月后收到回复时能立刻知道当时投的是哪一版、承诺了什么避免答非所问。第五给重要节点打 tag。每次正式投出去执行git tag v1-submit-日期。tag 是不可变的指针比提交信息更容易检索。对方问“你投的是哪一版”一条命令就能导出当时的完整内容。第六不要为了工具而工具。如果你只写一篇三千字的短文、只投一个渠道用 Markdown 加一个云盘备份就够了不必上静态站点和自动化脚本。这套方案的价值随文章长度、修改次数和投稿渠道数量增长规模不够时反而增加负担。判断标准是当你开始因为“找不到最新版”或“又要重新排版一次”而感到烦躁时就是引入它的时机。第七注意投稿渠道的合规要求。不同平台对原创声明、首发要求、独家授权的规定不同投稿前应阅读对应规则避免一稿多投引发的纠纷。这部分与技术无关但和技术方案直接相关——正因为你的稿子在本地有完整版本记录才能在需要证明“这是我什么时候写的”时拿得出证据。第八定期做一次仓库体检。每隔一段时间执行git status和du -sh .git确认没有误提交的大文件比如几十兆的原始照片。Git 对大二进制文件不友好图片建议压缩后再入库或者用单独的资源目录管理。仓库体积一旦膨胀后续操作都会变慢。写到这一步整套流程其实已经闭环了Markdown 负责内容Git 负责版本与备份脚本负责自检与导出静态站点负责预览与归档。它不改变你写作的方式只改变你管理稿子的方式。对一篇需要反复打磨、谨慎发布的长文来说这种“管理方式”的稳定性往往比某一段写得好不好更影响最终结果。如果你现在正准备动手建议的顺序是先建仓库、把已有内容按章节拆进去、提交一次然后加上隐私检查脚本跑一遍看看命中什么最后再考虑导出和站点。不要一上来就把所有工具装齐那样容易在配置环节消耗掉写作的热情。工具是为内容服务的顺序反过来就本末倒置了。