简介:这是一份基于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比普通语言麻烦在缩进敏感。它内部维护一个缩进栈,看到冒号就把新的缩进级别压栈,遇到elif、else再按规则弹栈。这也是为什么缩进里混入全角空格或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 5 | ACE | Monaco |
|---|---|---|---|
| 引入方式 | 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校验。stdout、stderr都是字符串,前端直接拼进<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 返回体字段表和输入框的边界
返回体设计成四个字段,前端处理逻辑就固定了:
| 字段 | 类型 | 说明 |
|---|---|---|
| stdout | string | 标准输出内容,已按UTF-8解码,超长截断 |
| stderr | string | 标准错误,包含语法错误和traceback |
| exit_code | int | 0正常退出,-1超时,其他为非零退出 |
| elapsed_ms | int | 实际运行耗时毫秒数,答辩时展示性能用 |
这里有一个课程作业经常忽略的边界:input()函数。子进程没有继承终端tty,用户代码里写了input()会直接抛出EOFError。如果大作业要求支持交互输入,常见做法是加一个输入文本框,通过subprocess.run的input参数把字符串传给stdin;不做输入支持就把这点在文档里写明,否则演示现场很尴尬。
4. 前端集成CodeMirror实现代码高亮并接通运行按钮
4.1 模板页面与CodeMirror依赖的引入顺序
index.html的骨架要保持简单,一个编辑区、一个输出区、一个运行按钮。引入CodeMirror的顺序非常重要:必须先引lib,再引mode和addon,最后引你自己的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,否则只有默认高亮;indentUnit和tabSize都设成4,这是Python社区最主流的缩进习惯;matchBrackets和autoCloseBrackets依赖之前引入的两个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(),这个强制重绘方法能解决九成布局相关的显示问题。
本文还有配套的精品资源,点击获取