概览


通过 Python 脚本 + n8n 工作流,可将零散草稿自动生成带 FrontMatter 的 Markdown 文件。
核心流程包括:批量触发 → 工作流处理 → 输出生成


系统流程


  1. 批量触发:Python 扫描草稿目录,发送文件名 JSON 至 n8n Webhook。
  2. 工作流处理:Webhook 接收 → 读取正文 + FrontMatter 模板 → 标记来源 → 合并 → 生成 Markdown。
  3. 输出生成:编码 Base64 → 写入 /data/posts/ → 保留原文格式与空行。

Python 批量触发脚本


(n8n/workflow/txt-md.py)

import requests
# 报错 requests 模块不存在?请安装依赖:pip3 install requests
import os

# n8n Webhook 地址,请替换成你的实际地址和路径
N8N_WEBHOOK_URL = 'http://localhost:5678/webhook/workflow'

# 草稿目录路径(相对于脚本目录,上一级 n8n 下的 drafts)
DRAFTS_DIR = '../drafts'

def batch_trigger_n8n():
    for filename in os.listdir(DRAFTS_DIR):
        if filename.endswith('.md') or filename.endswith('.txt'):
            payload = {'filename': filename}
            try:
                response = requests.post(N8N_WEBHOOK_URL, json=payload)
                response.raise_for_status()
                print(f'[SUCCESS] Triggered for file: {filename}')
            except requests.RequestException as e:
                print(f'[ERROR] Failed for file: {filename}, error: {e}')

if __name__ == '__main__':
    batch_trigger_n8n()


# ▶️ 运行方式,在该文件目录下运行:
# cd ~/n8n/workflow
# python3 "txt-md.py"


FrontMatter 模板


(n8n/templates/frontmatter.txt)

---
layout: post
date: { DATE } # 自动替换日期
title: { 中文标题 } # 自动解析标题
permalink: /blog/{slug}/ # 自动生成 slug
description: .
categories: 随笔
tags:
---

---

{正文内容} # 保留原文,包括空行

---

工作流 json 详解


工作流流程概览


节点名称 节点类型 核心功能注释
workflow Webhook 入口点:接收 POST 请求,触发流程。请求体需包含 filename
Read Chinese Content Read Binary File 读取草稿:根据传入的文件名,从 /data/drafts/ 目录读取正文。
Read ZH Front Matter Template Read Binary File 读取模板:读取预设的 Frontmatter 配置文件。
Set Chinese Source / Template Source Set 标记来源:给数据打标签,方便后续 Merge 节点区分哪个是正文,哪个是模板。
Merge Inputs Merge 合流:将正文数据和模板数据合并到同一个流中。
Generate Markdown Function (JS) 核心逻辑:解析文本、提取元数据、组装最终 Markdown 字符串。
Convert to Binary Function (JS) 格式转换:将处理好的字符串转回二进制 Buffer,以便写入文件。
Save Markdown Write Binary File 持久化:将生成的 Markdown 文件保存到 /data/posts/ 目录。

具体代码


(n8n/workflow/txt-md.json)

{
  "nodes": [
    {
      "parameters": {
        "filePath": "={{`/data/drafts/${$json.body.filename}`}}"
      },
      "name": "Read Chinese Content",
      "type": "n8n-nodes-base.readBinaryFile",
      "typeVersion": 1,
      "position": [-384, -64],
      "id": "d717aff7-e011-42aa-bab8-2fd2b7a27bd4"
    },
    {
      "parameters": {
        "filePath": "/data/templates/frontmatter.txt"
      },
      "name": "Read ZH Front Matter Template",
      "type": "n8n-nodes-base.readBinaryFile",
      "typeVersion": 1,
      "position": [-384, 128],
      "id": "54aed545-c3f9-404e-82e6-918ad1116ca6"
    },
    {
      "parameters": {
        "values": {
          "string": [
            {
              "name": "source",
              "value": "chinese"
            },
            {
              "name": "originalFilename",
              "value": "={{$json.body.filename}}"
            }
          ]
        },
        "options": {}
      },
      "name": "Set Chinese Source",
      "type": "n8n-nodes-base.set",
      "typeVersion": 1,
      "position": [-160, -64],
      "id": "088ee33a-32a0-4a46-95b1-dc82f1f33cb6"
    },
    {
      "parameters": {
        "values": {
          "string": [
            {
              "name": "source",
              "value": "template"
            }
          ]
        },
        "options": {}
      },
      "name": "Set Template Source",
      "type": "n8n-nodes-base.set",
      "typeVersion": 1,
      "position": [-160, 128],
      "id": "55ec1c7e-39a4-4277-98ab-19686d812264"
    },
    {
      "parameters": {},
      "name": "Merge Inputs",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [64, 32],
      "id": "a496f105-a92d-4c38-ae75-3cb7b0e43ecf"
    },
    {
      "parameters": {
        "functionCode": "const inputs = $input.all();\nconst chinese = inputs.find(i => i.json.source === 'chinese');\n\nif (!chinese?.binary?.data) throw new Error('正文未找到');\n\n// 完整读取内容\nconst content = Buffer.from(chinese.binary.data.data, 'base64').toString('utf8');\nconst lines = content.split(/\\r?\\n/);\n\n// 初始化\nlet date = '';\nlet slug = '';\nlet title = '';\nlet dateLineIndex = -1;\n\n// 扫描前 3 行寻找日期+slug\nfor (let i = 0; i < Math.min(3, lines.length); i++) {\n  const line = lines[i].trim();\n  const match = line.match(/^(\\d{4}-\\d{2}-\\d{2})-(.+)$/);\n  if (match) {\n    date = match[1];\n    slug = match[2].toLowerCase();\n    dateLineIndex = i;\n    break;\n  }\n}\n\n// 扫描前 3 行寻找标题(跳过日期+slug行)\nfor (let i = 0; i < Math.min(3, lines.length); i++) {\n  if (i === dateLineIndex) continue;\n  if (lines[i].trim() !== '') {\n    title = lines[i].trim();\n    break;\n  }\n}\n\n// 默认值处理\nif (!date) date = new Date().toISOString().slice(0,10);\nif (!slug) slug = 'untitled';\nif (!title) title = '标题未命名';\n\n// 正文保持原文完整\nconst bodyContent = content;\n\n// 生成 frontmatter\nconst frontmatter = `---\nlayout: post\ndate: ${date}\ntitle: ${title}\npermalink: /blog/${slug}/\ndescription: .\ncategories: 随笔\ntags: \n---`;\n\n// 合并 frontmatter 和正文,正文前后 --- 各空两行\nconst rendered = `${frontmatter}\\n\\n---\\n\\n${bodyContent}\\n\\n---`;\n\nreturn {\n  json: {\n    content,\n    frontMatter: frontmatter,\n    fullContent: rendered,\n    filename: `${date}-${slug}.md`\n  }\n};"
      },
      "name": "Generate Markdown",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [288, 32],
      "id": "77118e4f-71a2-418f-bafa-1eee33d8c7df"
    },
    {
      "parameters": {
        "functionCode": "return [{\n  json: $json,\n  binary: {\n    data: {\n      data: Buffer.from($json.fullContent, 'utf8').toString('base64'),\n      mimeType: 'text/plain',\n      fileName: $json.filename\n    }\n  }\n}];"
      },
      "name": "Convert to Binary",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [512, 32],
      "id": "0b934aa7-b177-4e98-b423-16bbfc5a2634"
    },
    {
      "parameters": {
        "fileName": "=/data/posts/{{$json.filename}}",
        "options": {}
      },
      "name": "Save Markdown",
      "type": "n8n-nodes-base.writeBinaryFile",
      "typeVersion": 1,
      "position": [736, 32],
      "id": "64546372-ab09-43b2-9c31-3e8038bffcdf"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "workflow",
        "options": {}
      },
      "name": "workflow",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [-608, 32],
      "id": "54a715a2-8a68-4a92-b619-c205d001944c",
      "webhookId": "af9e5d5b-8629-4210-9032-bc25c6f71447"
    }
  ],
  "connections": {
    "Read Chinese Content": {
      "main": [
        [
          {
            "node": "Set Chinese Source",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read ZH Front Matter Template": {
      "main": [
        [
          {
            "node": "Set Template Source",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Chinese Source": {
      "main": [
        [
          {
            "node": "Merge Inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Template Source": {
      "main": [
        [
          {
            "node": "Merge Inputs",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Inputs": {
      "main": [
        [
          {
            "node": "Generate Markdown",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Markdown": {
      "main": [
        [
          {
            "node": "Convert to Binary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert to Binary": {
      "main": [
        [
          {
            "node": "Save Markdown",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "workflow": {
      "main": [
        [
          {
            "node": "Read Chinese Content",
            "type": "main",
            "index": 0
          },
          {
            "node": "Read ZH Front Matter Template",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "ab1b5967b82f71f6034503cc7421f592e183c0451c8c39539355e953bbd4b573"
  }
}

核心逻辑代码详细注释


这是该工作流中最关键的 JavaScript 处理逻辑:

// 1. 获取输入数据并定位正文
const inputs = $input.all();
const chinese = inputs.find((i) => i.json.source === "chinese");

if (!chinese?.binary?.data) throw new Error("正文未找到");

// 2. 将二进制数据从 Base64 转换为 UTF-8 文本字符串
const content = Buffer.from(chinese.binary.data.data, "base64").toString("utf8");
const lines = content.split(/\r?\n/);

// 3. 初始化变量
let date = "";
let slug = "";
let title = "";
let dateLineIndex = -1;

// 4. 解析逻辑:扫描前 3 行寻找日期和 Slug (格式需为: YYYY-MM-DD-filename)
for (let i = 0; i < Math.min(3, lines.length); i++) {
  const line = lines[i].trim();
  const match = line.match(/^(\d{4}-\d{2}-\d{2})-(.+)$/);
  if (match) {
    date = match[1]; // 提取日期 (如 2023-10-27)
    slug = match[2].toLowerCase(); // 提取缩略名并转小写
    dateLineIndex = i;
    break;
  }
}

// 5. 提取标题:在非日期行中寻找第一个非空行作为标题
for (let i = 0; i < Math.min(3, lines.length); i++) {
  if (i === dateLineIndex) continue;
  if (lines[i].trim() !== "") {
    title = lines[i].trim();
    break;
  }
}

// 6. 容错处理:若没找到则设置默认值
if (!date) date = new Date().toISOString().slice(0, 10);
if (!slug) slug = "untitled";
if (!title) title = "标题未命名";

// 7. 生成 YAML Frontmatter
const frontmatter = `---
layout: post
date: ${date}
title: ${title}
permalink: /blog/${slug}/
description: .
categories: 随笔
tags: 
---`;

// 8. 组装最终内容:Frontmatter + 分隔符 + 正文
const rendered = `${frontmatter}\n\n---\n\n${content}\n\n---`;

return {
  json: {
    content,
    frontMatter: frontmatter,
    fullContent: rendered,
    filename: `${date}-${slug}.md`, // 定义最终保存的文件名
  },
};

数据流转说明:

  1. 输入:通过 Webhook 发送一个 JSON,例如 {"filename": "my-draft.md"}
  2. 提取:脚本会读取 my-draft.md 的内容。如果内容第一行是 2024-05-20-n8n-guide,它会识别出日期是 2024-05-20,slug 是 n8n-guide。
  3. 转换:将这些信息填入 Jekyll/Hugo 风格的 Frontmatter 模板中。
  4. 输出:在 /data/posts/ 目录下生成一个新的文件,例如 2024-05-20-n8n-guide.md

总结


Python + n8n 实现了从草稿到 Markdown 的全自动化流程:

  • 批量触发 → 读取模板与正文 → 合并生成 Markdown → 输出文件
  • 自动解析日期、标题、slug
  • 适合博客、知识库、文档批量管理和内容生成场景