承上一篇: nodejs:如何使用 express markdown-it 实现指定目录下 Md 文件的渲染
模仿 server2.js, 请用 python编程 : flask 3, mistune 2, python-multipart 实现同样功能的Web服务
以下是使用 Flask 3 + mistune 2 + python-multipart 实现与 server2.js 相同功能的 Python Web 服务,完整保留了原功能并适配 Python 生态:
python
# server.py
import os
import re
import mistune
from mistune.renderers import HTMLRenderer
from flask import Flask, request, jsonify, send_file, send_from_directory, make_response
from werkzeug.utils import secure_filename
from pathlib import Path
import mimetypes
# 初始化 Flask 应用
app = Flask(__name__, static_folder='static', static_url_path='/')
# ====================== 配置项 ======================
# 默认MD文件目录
DEFAULT_MD_DIR = Path(__file__).parent / 'md-files'
# 确保默认目录存在
DEFAULT_MD_DIR.mkdir(parents=True, exist_ok=True)
print(f"创建/使用默认MD文件目录: {DEFAULT_MD_DIR.absolute()}")
# 允许上传的文件类型
ALLOWED_EXTENSIONS = {'.md', '.markdown'}
# 文件大小限制 (5MB)
MAX_CONTENT_LENGTH = 5 * 1024 * 1024
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH
# ====================== 安全工具函数 ======================
def validate_and_normalize_dir(dir_path: str) -> Path:
"""验证并规范化目录路径"""
# 如果为空,返回默认目录
if not dir_path or dir_path.strip() == '':
return DEFAULT_MD_DIR.absolute()
# 解析为绝对路径
abs_path = Path(dir_path.strip()).absolute()
# 检查目录是否存在
if not abs_path.exists():
raise ValueError(f"目录不存在: {abs_path}")
# 检查是否是目录(不是文件)
if not abs_path.is_dir():
raise ValueError(f"不是有效的目录: {abs_path}")
# 检查目录是否可读
if not os.access(abs_path, os.R_OK):
raise ValueError(f"没有目录读取权限: {abs_path}")
return abs_path
def allowed_file(filename: str) -> bool:
"""检查文件扩展名是否合法"""
return Path(filename).suffix.lower() in ALLOWED_EXTENSIONS
# ====================== 配置 mistune 渲染器(支持 mermaid) ======================
class MermaidRenderer(HTMLRenderer):
"""自定义渲染器,支持 mermaid 语法"""
def block_code(self, code: str, info: str = '') -> str:
if info and info.strip() == 'mermaid':
return f'<div class="mermaid">{code}</div>'
# 调用父类默认实现
return super().block_code(code, info)
# 创建 mistune 实例
md_renderer = MermaidRenderer()
md_parser = mistune.create_markdown(renderer=md_renderer,
plugins=['strikethrough', 'table', 'url'],
escape=False # 关键配置:禁用字符转义
)
# ====================== 读取静态 HTML 模板(缓存) ======================
def get_index_html() -> str:
"""读取并缓存 index2.html 内容"""
index_path = Path(__file__).parent / 'static' / 'index2.html'
if not index_path.exists():
raise FileNotFoundError(f"index2.html 不存在: {index_path}")
with open(index_path, 'r', encoding='utf-8') as f:
return f.read()
index_html = get_index_html()
# ====================== 接口:获取MD文件列表(支持自定义目录) ======================
@app.route('/get-md-files', methods=['GET'])
def get_md_files():
try:
# 获取并验证目录路径
dir_path = validate_and_normalize_dir(request.args.get('dirPath', ''))
# 读取目录下所有.md文件(排除隐藏文件)
files = []
for file in dir_path.iterdir():
if file.is_file() and allowed_file(file.name) and not file.name.startswith('.'):
files.append(file.name)
# 按名称排序
files.sort()
return jsonify(files)
except ValueError as e:
app.logger.error(f"读取文件列表失败: {e}")
return jsonify({'error': str(e)}), 400
except Exception as e:
app.logger.error(f"读取文件列表异常: {e}")
return jsonify({'error': f'服务器内部错误: {str(e)}'}), 500
# ====================== 接口:加载指定MD文件并渲染(支持自定义目录) ======================
@app.route('/load-md-file', methods=['GET'])
def load_md_file():
try:
# 验证文件名参数
filename = request.args.get('filename', '')
if not filename or '..' in filename or not allowed_file(filename):
return jsonify({'error': '无效的文件名'}), 400
# 解码文件名(处理URL编码)
filename = request.args.get('filename', '').encode('utf-8').decode('unicode_escape')
# 获取并验证目录路径
dir_path = validate_and_normalize_dir(request.args.get('dirPath', ''))
file_path = dir_path / secure_filename(filename)
# 检查文件是否存在
if not file_path.exists() or not file_path.is_file():
return jsonify({'error': f'文件不存在: {filename}'}), 404
# 检查文件是否可读
if not os.access(file_path, os.R_OK):
return jsonify({'error': f'没有文件读取权限: {filename}'}), 403
# 读取文件内容并渲染为HTML
with open(file_path, 'r', encoding='utf-8') as f:
md_content = f.read()
rendered_html = md_parser(md_content)
return rendered_html
except ValueError as e:
app.logger.error(f"加载MD文件失败: {e}")
return jsonify({'error': str(e)}), 400
except Exception as e:
app.logger.error(f"加载MD文件异常: {e}")
return jsonify({'error': f'服务器内部错误: {str(e)}'}), 500
# ====================== 原有接口 ======================
# 1. 首页:返回上传页面
@app.route('/')
def index():
return send_file(Path(__file__).parent / 'static' / 'index2.html')
# 2. 处理 Markdown 文件上传 + 解析 + 渲染
@app.route('/upload-md', methods=['POST'])
def upload_md():
try:
# 检查是否有文件上传
if 'mdFile' not in request.files:
return '<h1>错误</h1><p>请选择要上传的 Markdown 文件!</p>', 400
file = request.files['mdFile']
# 检查文件名是否为空
if file.filename == '':
return '<h1>错误</h1><p>请选择要上传的 Markdown 文件!</p>', 400
# 检查文件类型
if file and allowed_file(file.filename):
# 读取文件内容
md_content = file.read().decode('utf-8')
# 渲染为HTML
rendered_html = md_parser(md_content)
# 替换模板中的占位符
final_html = index_html.replace('<div id="markdown-content"></div>', rendered_html)
response = make_response(final_html)
response.headers['Content-Type'] = 'text/html; charset=utf-8'
return response
# 文件类型不允许
return '<h1>错误</h1><p>仅支持上传 .md 或 .markdown 格式的文件!</p>', 400
except Exception as e:
app.logger.error(f"处理上传文件出错: {e}")
error_html = f"""
<h1>出错了</h1>
<p>处理上传的 Markdown 文件失败:{str(e)}</p>
<a href="/">返回上传页面</a>
"""
return error_html, 500
# 静态文件服务(兼容原逻辑)
@app.route('/<path:filename>')
def serve_static(filename):
return send_from_directory(app.static_folder, filename)
# ====================== 启动服务 ======================
if __name__ == '__main__':
# 注册 mimetypes(确保js文件正确解析)
mimetypes.add_type('application/javascript', '.js')
print(f"服务已启动:http://localhost:8000")
print(f"默认MD文件目录:{DEFAULT_MD_DIR.absolute()}")
print(f"访问 http://localhost:8000/index2.html 即可浏览和渲染MD文件")
# 启动Flask服务(调试模式,生产环境请关闭)
app.run(host='127.0.0.1', port=8000, debug=True)
配套依赖安装
创建 requirements.txt 文件:
txt
Flask==3.0.3
mistune==2.0.5
python-multipart==0.0.6
Werkzeug==3.0.3
安装依赖:
bash
pip install -r requirements.txt
关键说明(与原 Node.js 版本对齐)
-
功能完全对齐:
- 支持自定义目录加载 MD 文件列表
- 加载并渲染指定 MD 文件(支持 Mermaid 图表)
- 上传 MD 文件并实时渲染
- 路径安全校验(防止路径遍历、权限检查)
- 文件类型/大小限制
-
核心差异适配:
- Node.js 的
markdown-it→ Python 的mistune(自定义渲染器支持 Mermaid) - Node.js 的
multer→ Flask 原生 +python-multipart处理文件上传 - Node.js 的
fs/path→ Python 的pathlib/os处理文件系统 - 错误处理、响应格式完全对齐原接口
- Node.js 的
-
部署注意事项:
- 需要保持原有的
static目录结构(包含index2.html和mermaid.min.js) md-files目录会自动创建,用于存放默认 MD 文件- 生产环境请关闭
debug=True,并使用 Gunicorn 等 WSGI 服务器运行
- 需要保持原有的
目录结构要求
├── server.py # Python 服务文件
├── static/ # 静态文件目录
│ ├── index2.html # 前端页面(复用原文件)
│ └── mermaid.min.js # Mermaid 渲染脚本
├── md-files/ # 默认 MD 文件目录(自动创建)
└── requirements.txt # 依赖文件
运行方式
bash
python server.py
访问 http://localhost:8000 即可使用,功能与原 Node.js 版本完全一致。