18  将一个 md 文档切割成多个 md 文档

18.1 任务描述

在过去的三年中,除了重要节假日以外,连享会 基本能确保每天发布一篇新推文。这些推文的选题基本上都是我确定的。

我每周会花一天的时间,阅读大量的文献和各种网站,收集和整理选题。这些选题会被我统一记录在一个 Markdown 文档中,形如 Topics-2025-07.md。经过修改后,这些选题会发布在 备选推文主题 页面,由助教们认领。

我每周大概公布 10 个选题。其中最让我头疼的工作就是将这个 Markdown 文档切割成多个小文档 (由 图 1 转成 图 2):我需要 新建一个 .md 文档重命名贴入内容。这个工作没什么难度,但很耗神,也容易出错。

图 1:Topics-2025-07.md 文档中的选题笔记

图 2:github 仓库中选题发布页面

18.2 处理思路

我此前尝试过用 Stata 写代码来处理这个任务,但效果不佳,可能是因为我对文档读写不太熟悉。

前段时间我尝试用 AI 编写 Python 代码来完成这个任务。AI 给出的代码基本上没有 bugs。我运行代码后,便有了新的需求,比如:

  • 自动覆盖旧文件;
  • 可以指定从 .md 文档中选择一部分文本进行切割;
  • 指定存放输出结果的文件夹
  • 确定屏幕打印结果

需要声明的是:在此过程中,我只能大概读懂 AI 编写的 Python 代码,我基本上无法自行编写 Python 代码。我的主要工作就是优化提示词 (本质上是弄清楚我自己到底想要什么样的结果)。因此,我从不修改 AI 编写的 Python 代码,我只修改提示词。

下面是我经过 5-6 轮修改后形成的提示词。这些提示词的编排格式是我自己设计的,虽然花了一些时间,但我发现这个习惯在应对较为复杂的任务时至关重要。否则,如果你的提示词是大段的文字,逻辑不清晰,AI 帮你编写的代码也会很容易出错。

提示词
### input-output

- Folder:当前路径
- filename-input:包含关键词 'Topics-' 且后缀为 '.md' 的文件
- range-转换范围: 
  - begin: # B860
  -   end: # B851
- Folder-output:'.\topics'

### 任务:

- Input: 读取 {filename-input} 文件。
- Actions:
  - 在 {filename-input} 文档中,以 `^# B\d{3,.}:title` 开头的行为分割线,将该行及以下到下一个 `^# B\d{3,.}:title` 之间的文本写入一个新的 **.md** 文件中
  - 仅读取 {begin} 到 {end} 范围内的选题
- Output:
  - 存入:{Folder-output} 文件夹
    - 预先清空该文件夹 
  - 文件名为 `B\d{3,.}:title` 
    - 首字母为 'B'
    - 文件后缀为 '.md',而不是 '.md.md'
  - 屏幕打印:
    - 文件总数
    - 文件名称列表

下面是 AI 编写的 Python 代码:

import os
import re
import shutil

# ================= 用户参数 =================
input_folder = r"."
output_folder = r".\topics"
begin_tag = "# B860"
end_tag = "# B851"

# ================= 辅助函数 =================
def get_input_filename(folder):
    for fname in os.listdir(folder):
        if "Topics-" in fname and fname.endswith('.md'):
            return os.path.join(folder, fname)
    raise FileNotFoundError("未找到包含 'Topics-' 且后缀为 '.md' 的文件")

def read_file(filepath):
    with open(filepath, encoding="utf-8") as f:
        return f.read()

def split_by_title(text):
    # 正则匹配 '# Bxxx:title'
    blocks = list(re.finditer(r'^# B\d{3,}:.*$', text, flags=re.MULTILINE))
    result = []
    for idx, match in enumerate(blocks):
        start = match.start()
        end = blocks[idx + 1].start() if idx + 1 < len(blocks) else len(text)
        title_line = text[match.start():match.end()]
        block_text = text[start:end].strip()
        result.append((title_line, block_text))
    return result

def find_range(blocks, begin_tag, end_tag):
    begin_idx, end_idx = None, None
    for i, (title, _) in enumerate(blocks):
        if title.strip().startswith(begin_tag):
            begin_idx = i
        if title.strip().startswith(end_tag):
            end_idx = i
    if begin_idx is not None and end_idx is not None and begin_idx <= end_idx:
        return blocks[begin_idx:end_idx+1]
    else:
        raise ValueError("未能定位 begin 或 end 位置,或顺序错误")

def clean_folder(folder):
    if os.path.exists(folder):
        shutil.rmtree(folder)
    os.makedirs(folder)

def save_blocks_to_folder(blocks, folder):
    filenames = []
    for title, content in blocks:
        # 文件名采用 '# Bxxx:title' 的形式
        file_base = re.match(r'^# (B\d{3,}:.*)$', title.strip())
        if not file_base:
            continue
        filename = file_base.group(1) + ".md"
        # 替换非法字符
        filename = re.sub(r'[\\/:*?"<>|]', '_', filename)
        filepath = os.path.join(folder, filename)
        with open(filepath, "w", encoding="utf-8") as f:
            f.write(content.strip() + "\n")
        filenames.append(filename)
    return filenames

# ================= 主程序 =================
if __name__ == "__main__":
    # 1. 获取输入文件
    input_path = get_input_filename(input_folder)
    # 2. 读取内容
    text = read_file(input_path)
    # 3. 按分割线分割
    blocks = split_by_title(text)
    # 4. 提取 begin 到 end 范围
    blocks_in_range = find_range(blocks, begin_tag, end_tag)
    # 5. 预清空输出文件夹
    clean_folder(output_folder)
    # 6. 写入输出
    filelist = save_blocks_to_folder(blocks_in_range, output_folder)
    # 7. 打印信息
    print(f"文件总数:{len(filelist)}")
    print("文件名称列表:")
    for fn in filelist:
        print(fn)
文件总数:9
文件名称列表:
B860:Stata 可视化:heatplot-热力图.md
B859:cdist-如何估计反事实的分布特征?.md
B858:数据量大时,啥啥都显著.md
B857:介绍 Local-DID 模型.md
B856:Stata 可视化:robbox-强大的的箱型图命令 - 快速呈现离群值.md
B855:介绍 I4R 机构和网站.md
B854:知乎热议:为什么国内博士毕业的 paper 普遍比国外博士多?.md
B852:mmqreg_ quantile regressions via Method of Moments.md
B851:翻译:异方差稳健性标准误:实操建议.md

有了这段代码,我每次只需 1 秒即可完成任务。

你可以在 B860:Stata 可视化:heatplot-热力图 查看选题 B860 的具体要求。再过 1 月左右,你就可以在 lianxh.cn 上看到这篇推文了。