news 2026/9/14 13:36:38

Flask+CodeMirror+subprocess网页版Python编辑器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flask+CodeMirror+subprocess网页版Python编辑器

简介:这是一份基于Flask与CodeMirror构建的网页版Python编辑器项目源码,源自程序设计课程大作业,适合需要完成在线代码编辑、远程实验或课程设计展示的开发者参考。后端由Python Flask提供路由、登录认证与文件管理,前端通过HTML、CSS及JavaScript实现界面交互,借助CodeMirror实现代码高亮和自动联想补全,并能根据用户编写的Python代码生成对应流程图,便于初学者直观理解程序执行逻辑。资源压缩包共收录630个文件,压缩后体积仅1.75MB,整体十分轻量,便于下载部署与阅读分析。其中310个JS文件对应CodeMirror编辑器及各类主题组件,190个HTML与82个CSS文件负责页面布局、样式和配色,12个Python文件承载Flask应用入口、用户注册登录、代码保存与运行等核心逻辑,另有少量SVG图标、数据库及配置文件,目录层次清楚,方便按需查找与二次开发。内置测试账号可直接体验登录、编辑、新建、删除、保存及生成流程图等完整流程,运行与生成流程图须在保存后执行,输出结果会同步显示在编辑区下方;当前已有235人学习下载,是完成课程设计、快速理解Flask项目组织方式或作为在线编辑器二次开发起点的轻量级参考。

1. 网页版Python编辑器大作业的真实价值:把环境问题从课程中剥离

“程序设计思维”这门课面向的通常是没接触过程序设计的初学者,第一周的内容往往是“第一节:什么是Python”。但真正上课时,助教答疑时间大多被“为什么我双击.py文件没反应”“我把环境变量改坏了”这类问题占据。既然课程重点是语言和算法,环境差异就不该成为损耗点。用Flask加CodeMirror做一个网页版Python编辑器,本质就是把开发环境收敛成一个URL:浏览器打开即进入可编辑、可运行、带高亮的Python页面。后端用Flask接收代码,调subprocess跑解释器,把输出回传页面;前端用CodeMirror负责Python语法高亮。这套方案对大作业来说难度适中、工作量可控,答辩阶段也有实际技术点可讲。下面的内容按架构、后端执行、前端集成、排错打磨四段展开。

2. Flask项目架构与CodeMirror高亮原理:先想清楚再动手

2.1 建一个把边界划清楚的项目目录

课程大作业最忌讳的是把所有代码堆进一个app.py。不是说一个文件跑不起来,而是后面排错和答辩的时候,你自己都会找不到逻辑在哪。我这里采用一个非常常规的Flask静态文件结构,前端素材全部放进static目录,页面模板放进templates目录,后端逻辑留在根目录的app.py

web-python-editor/ ├── app.py ├── requirements.txt ├── static/ │ ├── codemirror/ │ │ ├── lib/ │ │ │ ├── codemirror.min.js │ │ │ └── codemirror.css │ │ ├── mode/python/ │ │ │ └── python.min.js │ │ ├── addon/edit/ │ │ │ ├── matchbrackets.min.js │ │ │ └── closebrackets.min.js │ │ └── theme/ │ │ └── dracula.min.css │ ├── css/style.css │ └── js/editor.js └── templates/ └── index.html

为什么不把CodeMirror直接用CDN?我建议课程项目把依赖文件下载到本地。演示答辩的教室网络不一定允许访问外部CDN,甚至有离线环境要求。CodeMirror的全部静态文件加起来不大,放到static/codemirror下最省心。Flask默认就会挂载/static路径,不需要额外配置。

app.py的最小骨架只有两个路由:GET /渲染编辑器页面,POST /run接收并执行代码。如果后面打算做自动保存,再加一个POST /save,仅此而已。保持职责单一,前端也不会因为路由过多而混乱。

# app.py 最小骨架 from flask import Flask, request, render_template, jsonify app = Flask(__name__) @app.route("/") def index(): return render_template("index.html") @app.route("/run", methods=["POST"]) def run_code(): code = request.get_json()["code"] # 后续换成真正的执行逻辑 return jsonify({"stdout": "", "stderr": "not implemented", "exit_code": 1}) if __name__ == "__main__": app.run(debug=True)

这里request.get_json()返回整个JSON对象,代码字符串从code字段取。后面接执行逻辑时,只需要把中间那段return换掉,接口形状保持不变。

2.2 CodeMirror的高亮逻辑:Token流、缩进栈与模式注册

CodeMirror的高亮不是一个正则替换成HTML就完事的。它的核心是一个由mode驱动的tokenizer,逐行逐token解析代码,给每个token打上带有语义的CSS class,再用CSS渲染颜色。以Python的def foo():为例,CodeMirror的Python mode会依次识别出def是keyword、foo是def、()是punctuation、:是operator,页面样式就来自这些class的组合。

Python mode比普通语言麻烦在缩进敏感。它内部维护一个缩进栈,看到冒号就把新的缩进级别压栈,遇到elifelse再按规则弹栈。这也是为什么缩进里混入全角空格或Tab时,高亮会整体失常——不是CodeMirror坏了,而是tokenizer对缩进的判断彻底乱了。

CodeMirror 5引入python模式只需要在加载lib之后引入一行mode/python/python.min.js,它内部会完成模式注册。如果你直接引入CodeMirror 6,则是另一套ESM体系,需要@codemirror/lang-python配合打包器使用。课程作业用CodeMirror 5是最稳妥的,标签引完就能用,零构建成本。把ACE、Monaco和CodeMirror 5放在一起比较,差异更直观:

对比维度CodeMirror 5ACEMonaco
引入方式script标签直引script标签直引模块化加载,配置较重
核心体积约300KB约600KB更大,依赖VS Code生态
Python模式缩进栈完善支持但边界处理较粗机制重,适合大型应用
课程作业友好度最合适有一定学习成本杀鸡用牛刀

Monaco本身是VS Code的编辑器内核,能力强,但为了做一个网页版编辑器去引入整个monaco-editor的模块体系,得不偿失。CodeMirror 5在“直接能用”和“功能够用”之间平衡得最好。

2.3 前后端接口契约:用一页JSON对齐

这个项目的前后端交互其实只有三个接口要点,定死协议后两边并行开发都不容易出错:

  • POST /run,请求体{"code": "print('hi')"},返回{"stdout": "...", "stderr": "...", "exit_code": 0, "elapsed_ms": 12}
  • code必须是字符串。后端只做基础类型判断,不做JSON Schema校验。
  • stdoutstderr都是字符串,前端直接拼进<pre>显示,不返回对象数组。

前端把请求封装成一个简单的异步函数,后面所有按钮、快捷键都复用它:

async function runCode(code) { const resp = await fetch('/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }) }); return await resp.json(); }

提交时用fetch加JSON而不是form表单,因为form提交会刷新页面,导致编辑器内容丢失。这个选择直接决定了交互质量。

3. Flask后端subprocess执行Python代码:接管输入与输出

3.1 为什么是用subprocess而不是在Flask进程里exec

浏览器里没有Python解释器,代码必须回到服务端执行。直接用Python的exec(code)看起来很简洁,但代码里出现import os; os.system('rm')这种操作会直接作用在Flask所在进程里,一个无限循环就能让整个编辑器卡死。用subprocess起一个新进程执行,相当于在操作系统层面把用户代码隔离出主服务,至少不会拖垮页面本身。

这也让timeout控制变得自然:子进程超过5秒直接杀掉,Flask主进程不受影响。如果换用exec,要自己写信号处理、线程控制,复杂度完全不一样。

3.2 subprocess.run的完整实现与参数逐项说明

# app.py 完整执行逻辑 import os import sys import time import subprocess import tempfile from flask import Flask, jsonify, render_template, request app = Flask(__name__) EXEC_TIMEOUT = 5 MAX_OUTPUT = 1024 * 1024 @app.route("/") def index(): return render_template("index.html") @app.route("/run", methods=["POST"]) def run_code(): payload = request.get_json(silent=True) if not payload or not isinstance(payload.get("code"), str): return jsonify({"stdout": "", "stderr": "请求格式错误", "exit_code": 1}), 400 code = payload["code"] with tempfile.NamedTemporaryFile( mode="w", suffix=".py", encoding="utf-8", delete=False ) as f: f.write(code) tmp_path = f.name env = os.environ.copy() env["PYTHONIOENCODING"] = "utf-8" started = time.time() try: result = subprocess.run( [sys.executable, tmp_path], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=EXEC_TIMEOUT, env=env, ) stdout = result.stdout stderr = result.stderr if len(stdout) > MAX_OUTPUT: stdout = stdout[:MAX_OUTPUT] + "\n[输出已截断]" return jsonify({ "stdout": stdout, "stderr": stderr, "exit_code": result.returncode, "elapsed_ms": int((time.time() - started) * 1000), }) except subprocess.TimeoutExpired: return jsonify({ "stdout": "", "stderr": f"执行超时,超过 {EXEC_TIMEOUT} 秒", "exit_code": -1, "elapsed_ms": EXEC_TIMEOUT * 1000, }) finally: try: os.unlink(tmp_path) except OSError: pass if __name__ == "__main__": app.run(debug=True)

逐个参数讲清楚为什么这么写:

  • tempfile.NamedTemporaryFile(...):把代码写进临时文件再执行,而不是用python -c code直接传参。Windows下-c传参经过Unicode转换,代码含中文或特殊字符时容易出问题,临时文件方案最稳。
  • delete=False:Windows上如果文件句柄没释放就删除会报权限错误,所以这里先不自动删,等子进程跑完在finally里手动os.unlink
  • env["PYTHONIOENCODING"] = "utf-8":强制子进程按UTF-8输出。没有这一行,Windows中文系统上print("你好")会按GBK编码输出,前端再按UTF-8解码,必然乱码。
  • text=True配合encoding="utf-8":让stdout、stderr直接以字符串返回,而不是bytes。只写text=True不写encoding会使用系统locale默认编码,还是绕回乱码问题。
  • errors="replace":遇到非法编码字节时用?替换而不是抛异常,保证接口不会因为单个字符挂掉。
  • timeout=5:子进程超过5秒由Python主动杀掉。

3.3 返回体字段表和输入框的边界

返回体设计成四个字段,前端处理逻辑就固定了:

字段类型说明
stdoutstring标准输出内容,已按UTF-8解码,超长截断
stderrstring标准错误,包含语法错误和traceback
exit_codeint0正常退出,-1超时,其他为非零退出
elapsed_msint实际运行耗时毫秒数,答辩时展示性能用

这里有一个课程作业经常忽略的边界:input()函数。子进程没有继承终端tty,用户代码里写了input()会直接抛出EOFError。如果大作业要求支持交互输入,常见做法是加一个输入文本框,通过subprocess.runinput参数把字符串传给stdin;不做输入支持就把这点在文档里写明,否则演示现场很尴尬。

4. 前端集成CodeMirror实现代码高亮并接通运行按钮

4.1 模板页面与CodeMirror依赖的引入顺序

index.html的骨架要保持简单,一个编辑区、一个输出区、一个运行按钮。引入CodeMirror的顺序非常重要:必须先引lib,再引modeaddon,最后引你自己的editor.js。如果顺序颠倒,浏览器会报CodeMirror is not defined

<!-- templates/index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>网页版Python编辑器</title> <link rel="stylesheet" href="{{ url_for('static', filename='codemirror/lib/codemirror.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='codemirror/theme/dracula.min.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> </head> <body> <header> <button id="run-btn" type="button">运行</button> </header> <main> <textarea id="code">print("Hello, 编程思维!") # 在这里编写你的Python代码</textarea> <pre id="output"></pre> </main> <script src="{{ url_for('static', filename='codemirror/lib/codemirror.min.js') }}"></script> <script src="{{ url_for('static', filename='codemirror/mode/python/python.min.js') }}"></script> <script src="{{ url_for('static', filename='codemirror/addon/edit/matchbrackets.min.js') }}"></script> <script src="{{ url_for('static', filename='codemirror/addon/edit/closebrackets.min.js') }}"></script> <script src="{{ url_for('static', filename='js/editor.js') }}"></script> </body> </html>

为什么把元素的初始内容直接写在textarea里?因为CodeMirror.fromTextArea会把textarea原有的文本作为编辑器初始内容。这是最自然的初值传递方式,比通过JavaScript再赋值一次更省事。

4.2 初始化编辑器实例和Python模式参数

editor.js里做三件事:初始化编辑器、绑定运行按钮、处理输出显示。

// static/js/editor.js const editor = CodeMirror.fromTextArea( document.getElementById('code'), { mode: 'python', theme: 'dracula', lineNumbers: true, indentUnit: 4, tabSize: 4, matchBrackets: true, autoCloseBrackets: true, styleActiveLine: true, viewportMargin: Infinity } );

参数说明:mode指定语法模式,CodeMirror 5的python模式会在加载python.min.js后自动注册;theme要写成字符串,同时页面里必须引入对应的主题CSS,否则只有默认高亮;indentUnittabSize都设成4,这是Python社区最主流的缩进习惯;matchBracketsautoCloseBrackets依赖之前引入的两个addon文件,它们让括号自动匹配和自动闭合,属于编辑体验上的加分项;viewportMargin: Infinity让编辑器自动撑满内容高度,否则内容超过一屏会出现内部滚动条,视觉上很别扭。

初始化的副作用是原textarea会被隐藏,页面布局要围绕.CodeMirror这个容器来写,不要再用#code作为样式选择器。

4.3 绑定运行按钮和结果回显的完整流程

运行按钮的逻辑分成请求、异常处理、输出展示三段:

const runBtn = document.getElementById('run-btn'); const output = document.getElementById('output'); async function execute() { const code = editor.getValue(); output.textContent = '运行中...'; let data; try { const resp = await fetch('/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }) }); data = await resp.json(); } catch (err) { output.textContent = '网络请求失败: ' + err.message; return; } let out = ''; if (data.stdout) { out += '--- stdout ---\n' + data.stdout + '\n'; } if (data.stderr) { out += '--- stderr ---\n' + data.stderr + '\n'; } output.textContent = out || '(无输出)'; } runBtn.addEventListener('click', execute);

这段代码把stdout和stderr分开显示,而不是简单拼接,因为调试程序时区分“正常输出”和“错误信息”是基础习惯。注意按钮上的type="button"必不可少——如果漏掉,浏览器默认把它当成submit按钮,在表单环境里点击后会触发页面刷新,编辑器里的内容全部丢失。这是一个非常隐蔽的坑。

5. 网页版Python编辑器验收排错清单与编码细节打磨

5.1 三个必踩的坑和对应修复

课程作业演进中,我见过最多的是下面三个问题,按出现频率排序:

现象原因修复
页面只有textarea没有代码高亮python.min.js未引入或引入顺序错lib之后引mode,并保证mode: 'python'
点击运行后页面刷新按钮默认submit行为<button type="button">
输出中文乱码子进程GBK编码输出设置PYTHONIOENCODING=utf-8

另外检查一个隐蔽点:如果初始代码放在Jinja2模板的textarea里,且内容包含{{{%,模板引擎会尝试解析它。解决办法是用{% raw %}包裹内容,或者干脆通过editor.setValue()从JavaScript端初值化。

5.2 为答辩准备一个30秒验证脚本

演示前跑一遍这个脚本,能一次性暴露中文编码、循环执行、异常回显三个核心问题:

print("中文编码:正常") for i in range(3): print("i =", i) raise ValueError("traceback 展示")

预期结果是:stdout区域出现四行正常输出,stderr区域出现带traceback的异常信息,编辑器内容不发生刷新,运行耗时字段有数值。如果输出顺序乱掉,检查是不是浏览器console里有其他JavaScript报错。

页面关闭后重新打开,初始内容回到模板预设值的状态。想保留现场代码,加两行就够了:

editor.on('change', function() { localStorage.setItem('py-editor-code', editor.getValue()); });

页面加载时从localStorage读取并editor.setValue()恢复。这个“自动保存草稿”功能实现成本极低,但答辩演示时万一误关页面,不会直接翻车。编辑器在初始隐藏容器里渲染出空白时,记得调用editor.refresh(),这个强制重绘方法能解决九成布局相关的显示问题。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 13:35:10

OpenCore Legacy Patcher完整指南:让老Mac装上新版macOS

OpenCore Legacy Patcher完整指南&#xff1a;让老Mac装上新版macOS 【免费下载链接】OpenCore-Legacy-Patcher Experience macOS just like before 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher 翻开苹果的系统支持列表&#xff0c;很多…

作者头像 李华
网站建设 2026/9/14 13:33:10

MongoDB一对多关系设计:数组嵌入与独立集合性能对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 13:30:38

新能源汽车BMS MATLAB仿真模型:SOC估算与热管理实战

简介&#xff1a;本资源是面向新能源汽车动力系统建模与控制开发的MATLAB/Simulink工程实践包&#xff0c;适用于车辆工程、能源系统及自动化方向的研究者与工程师&#xff0c;聚焦电动机建模、电池特性仿真、能量管理策略设计与整车动力学分析等核心问题。压缩包共86个文件&am…

作者头像 李华