
本文介绍如何使用pandoc自定义过滤器(pyscript)在markdown文档中嵌入并执行python代码,将函数返回值作为原始markdown内容插入,实现动态内容生成与静态文档渲染的无缝集成。
本文介绍如何使用pandoc自定义过滤器(pyscript)在markdown文档中嵌入并执行python代码,将函数返回值作为原始markdown内容插入,实现动态内容生成与静态文档渲染的无缝集成。
Pandoc本身不内置执行代码块的功能,但其强大的过滤器(filter)机制支持通过外部程序(如Python脚本)解析、修改AST(抽象语法树)。要实现“运行Python代码 → 捕获返回值 → 将结果视为Markdown解析”,需编写一个符合Pandoc JSON filter规范的Python过滤器。
以下是一个轻量、安全、生产可用的 pyscript 过滤器实现(保存为 pyscript.py):
#!/usr/bin/env python3
import sys
import json
import subprocess
import tempfile
import os
from pathlib import Path
def run_python_code(code: str) -> str:
"""安全执行Python代码片段,仅允许函数定义+单次调用,返回字符串结果"""
# 构建最小执行环境:强制要求以 `return` 结尾的表达式或显式函数调用
wrapper = f"""\
import sys
sys.path.insert(0, '.')
# 用户代码
{code}
# 执行逻辑(仅允许单一可调用表达式)
if '__main__' == '__name__':
try:
# 优先尝试直接求值(如 return "## Hello")
result = eval(compile({repr(code)}, '<string>', 'eval'))
except:
# 否则查找并调用名为 'main' 或 'run' 的函数
local_ns = {{}}
exec({repr(code)}, {{}}, local_ns)
if 'main' in local_ns:
result = local_ns['main']()
elif 'run' in local_ns:
result = local_ns['run']()
else:
result = str(local_ns)
print(str(result), end='')
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(wrapper)
tmp_path = f.name
try:
res = subprocess.run(
[sys.executable, tmp_path],
capture_output=True,
text=True,
timeout=10,
cwd=os.getcwd()
)
if res.returncode != 0:
return f"[ERROR: Python execution failed — {res.stderr.strip()}]"
return res.stdout.strip()
finally:
Path(tmp_path).unlink(missing_ok=True)
def main():
# 读取Pandoc JSON AST
doc = json.load(sys.stdin)
if 'blocks' not in doc:
return
for i, block in enumerate(doc['blocks']):
if block.get('t') == 'CodeBlock':
attrs = block.get('c', [None, {}, []])[1]
classes = attrs.get('classes', [])
if 'pyscript' in classes:
code = block['c'][1]
output_md = run_python_code(code)
# 替换为Para节点(含Inline Markdown内容)
doc['blocks'][i] = {
"t": "Para",
"c": [
{"t": "Str", "c": output_md} # ⚠️ 注意:此处仅为纯文本;若需解析Markdown,须用pandoc.read()转换
]
}
json.dump(doc, sys.stdout)
if __name__ == "__main__":
main()⚠️ 重要说明:上述实现默认将Python输出作为纯文本插入。若需让输出内容被Pandoc当作真正的Markdown(例如支持 **bold**、[link](...)、标题等),需进一步调用 pandoc --from=plain --to=json 对输出做二次解析,并合并AST。更健壮的做法是使用 panflute 库(专为Pandoc过滤器设计):
pip install panflute
对应简化版 pyscript.py(推荐):
立即学习“Python免费学习笔记(深入)”;
#!/usr/bin/env python3
import panflute as pf
import subprocess
import tempfile
import os
def action(elem, doc):
if isinstance(elem, pf.CodeBlock) and 'pyscript' in elem.classes:
code = elem.text
try:
result = subprocess.run(
[os.sys.executable, '-c', f'import sys; {code}; print(str(locals().get("result", locals().get("main", lambda: "")())))'],
capture_output=True, text=True, timeout=5
).stdout.strip()
# 将Markdown字符串解析为Panflute元素
return pf.convert_text(result, input_format='markdown')
except Exception as e:
return pf.Para(pf.Str(f'[pyscript error: {e}]'))
if __name__ == '__main__':
pf.run_filter(action)✅ 使用方式:
保存为
pyscript.py,赋予可执行权限(chmod +x pyscript.py);-
在Markdown中使用
.pyscript类标记代码块:# Report Generated On ```{.pyscript} from datetime import datetime result = f"## Generated at {datetime.now():%Y-%m-%d %H:%M}" ``` -
运行转换:
pandoc --filter ./pyscript.py input.md -o output.html
? 安全提醒:此过滤器在服务端或共享环境中使用时,务必限制执行权限(如禁用 os, subprocess, open 等危险模块),或改用沙箱(如 pysandbox)——生产环境强烈建议仅允许白名单函数。
总结:pyscript 过滤器填补了Pandoc“静态渲染”与“动态内容”之间的关键缺口。它不是简单地打印输出,而是将Python的表达能力注入文档工作流,使技术文档、报告、讲义真正具备可编程性与可复现性。


















