n8n 工作流 + Python 自动批量生成 Markdown
概览
通过 Python 脚本 + n8n 工作流,可将零散草稿自动生成带 FrontMatter 的 Markdown 文件。
核心流程包括:批量触发 → 工作流处理 → 输出生成。
系统流程
- 批量触发:Python 扫描草稿目录,发送文件名 JSON 至 n8n Webhook。
- 工作流处理:Webhook 接收 → 读取正文 + FrontMatter 模板 → 标记来源 → 合并 → 生成 Markdown。
- 输出生成:编码 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`, // 定义最终保存的文件名
},
};
数据流转说明:
-
输入:通过 Webhook 发送一个 JSON,例如
{"filename": "my-draft.md"}。 -
提取:脚本会读取
my-draft.md的内容。如果内容第一行是2024-05-20-n8n-guide,它会识别出日期是 2024-05-20,slug 是 n8n-guide。 - 转换:将这些信息填入 Jekyll/Hugo 风格的 Frontmatter 模板中。
-
输出:在
/data/posts/目录下生成一个新的文件,例如2024-05-20-n8n-guide.md。
总结
Python + n8n 实现了从草稿到 Markdown 的全自动化流程:
- 批量触发 → 读取模板与正文 → 合并生成 Markdown → 输出文件
- 自动解析日期、标题、slug
- 适合博客、知识库、文档批量管理和内容生成场景