
简介这是一份基于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.pyweb-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(debugTrue)这里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 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校验。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(silentTrue) if not payload or not isinstance(payload.get(code), str): return jsonify({stdout: , stderr: 请求格式错误, exit_code: 1}), 400 code payload[code] with tempfile.NamedTemporaryFile( modew, suffix.py, encodingutf-8, deleteFalse ) 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_outputTrue, textTrue, encodingutf-8, errorsreplace, timeoutEXEC_TIMEOUT, envenv, ) 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(debugTrue)逐个参数讲清楚为什么这么写tempfile.NamedTemporaryFile(...)把代码写进临时文件再执行而不是用python -c code直接传参。Windows下-c传参经过Unicode转换代码含中文或特殊字符时容易出问题临时文件方案最稳。deleteFalseWindows上如果文件句柄没释放就删除会报权限错误所以这里先不自动删等子进程跑完在finally里手动os.unlink。env[PYTHONIOENCODING] utf-8强制子进程按UTF-8输出。没有这一行Windows中文系统上print(你好)会按GBK编码输出前端再按UTF-8解码必然乱码。textTrue配合encodingutf-8让stdout、stderr直接以字符串返回而不是bytes。只写textTrue不写encoding会使用系统locale默认编码还是绕回乱码问题。errorsreplace遇到非法编码字节时用?替换而不是抛异常保证接口不会因为单个字符挂掉。timeout5子进程超过5秒由Python主动杀掉。3.3 返回体字段表和输入框的边界返回体设计成四个字段前端处理逻辑就固定了字段类型说明stdoutstring标准输出内容已按UTF-8解码超长截断stderrstring标准错误包含语法错误和tracebackexit_codeint0正常退出-1超时其他为非零退出elapsed_msint实际运行耗时毫秒数答辩时展示性能用这里有一个课程作业经常忽略的边界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 langzh-CN head meta charsetUTF-8 title网页版Python编辑器/title link relstylesheet href{{ url_for(static, filenamecodemirror/lib/codemirror.css) }} link relstylesheet href{{ url_for(static, filenamecodemirror/theme/dracula.min.css) }} link relstylesheet href{{ url_for(static, filenamecss/style.css) }} /head body header button idrun-btn typebutton运行/button /header main textarea idcodeprint(Hello, 编程思维!) # 在这里编写你的Python代码/textarea pre idoutput/pre /main script src{{ url_for(static, filenamecodemirror/lib/codemirror.min.js) }}/script script src{{ url_for(static, filenamecodemirror/mode/python/python.min.js) }}/script script src{{ url_for(static, filenamecodemirror/addon/edit/matchbrackets.min.js) }}/script script src{{ url_for(static, filenamecodemirror/addon/edit/closebrackets.min.js) }}/script script src{{ url_for(static, filenamejs/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分开显示而不是简单拼接因为调试程序时区分“正常输出”和“错误信息”是基础习惯。注意按钮上的typebutton必不可少——如果漏掉浏览器默认把它当成submit按钮在表单环境里点击后会触发页面刷新编辑器里的内容全部丢失。这是一个非常隐蔽的坑。5. 网页版Python编辑器验收排错清单与编码细节打磨5.1 三个必踩的坑和对应修复课程作业演进中我见过最多的是下面三个问题按出现频率排序现象原因修复页面只有textarea没有代码高亮python.min.js未引入或引入顺序错在lib之后引mode并保证mode: python点击运行后页面刷新按钮默认submit行为button typebutton输出中文乱码子进程GBK编码输出设置PYTHONIOENCODINGutf-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()这个强制重绘方法能解决九成布局相关的显示问题。本文还有配套的精品资源点击获取