科技船长 · blog

Python+Flask的计算器

4,252 words 13 min read

一、项目概述

1.1 项目目标

实现一个在线计算器,包含两种使用方式:

使用方式文件说明
纯 API 调用app.py只提供 JSON 接口,用 curl 或命令行工具调用
浏览器页面版app2.py + templates/simple.html有输入框和按钮的网页版
命令行客户端client.py终端交互式计算工具

1.2 技术栈

技术
后端Python + Flask(Web 框架)
前端HTML + JavaScript(浏览器原生,无框架)
数据格式JSON(前后端通信)
环境隔离Python venv(虚拟环境)

1.3 文件结构

python_web/
├── venv/ # 虚拟环境(Python 解释器 + Flask 依赖)
├── app.py # 纯 API 版后端
├── app2.py # API + 前端页面版后端
├── client.py # 命令行客户端
├── requirements.txt # 依赖声明(flask)
└── templates/
└── simple.html # 前端页面(JS 调用 API)

二、阶段一:最基础的 API —— 从零开始的 app.py

2.1 什么是 API?

API(Application Programming Interface,应用程序接口)可以理解为:一个通过网络调用的函数

  • 普通函数:result = calc("1+1") —— 在同一个程序里调用
  • API 函数:通过 HTTP 请求调用,返回 JSON 数据
客户端(curl / 浏览器)
POST /api/calc ← HTTP 请求
{"expr": "1+1"}
Flask 服务器(app.py)
{"expr": "1+1", ← HTTP 响应
"result": "2"}

2.2 完整代码(25 行)

from flask import Flask, request, jsonify
app = Flask(__name__)
def safe_eval(expr: str) -> str:
allowed = set("0123456789+-*/(). ")
if not all(c in allowed for c in expr):
raise ValueError("只允许数字和 + - * / ( ) 运算符")
return str(eval(expr))
@app.route("/api/calc", methods=["POST"])
def calc():
data = request.get_json(force=True, silent=True) or {}
expr = str(data.get("expr", "")).strip()
if not expr:
return jsonify({"error": "表达式为空"}), 400
try:
result = safe_eval(expr)
return jsonify({"expr": expr, "result": result})
except Exception as e:
return jsonify({"expr": expr, "error": str(e)}), 400
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)

2.3 代码逐行详解

第 1 行:导入 Flask

from flask import Flask, request, jsonify
导入项作用
Flask创建 Web 应用的核心
request全局对象,代表当前收到的 HTTP 请求
jsonify工具函数,把 Python 字典转换成 JSON HTTP 响应

第 3 行:创建应用实例

app = Flask(__name__)
  • 调用 Flask() 构造函数,传入当前模块名 __name__
  • Flask 用这个信息知道项目根目录在哪里
  • app 就是我们的 Web 应用对象,后面所有路由都挂在它上面

第 6-10 行:安全计算函数

def safe_eval(expr: str) -> str:
allowed = set("0123456789+-*/(). ")
if not all(c in allowed for c in expr):
raise ValueError("只允许数字和 + - * / ( ) 运算符")
return str(eval(expr))

为什么要做安全检查?

Python 内置的 eval() 非常强大——强大到可以执行任意代码

eval("__import__('os').system('rm -rf /')") # ❌ 危险!

如果有人提交这样的表达式,服务器就被破坏了。所以必须在调用 eval() 之前做白名单检查

步骤代码说明
① 白名单allowed = set("0123456789+-*/(). ")只允许这些字符
② 检查all(c in allowed for c in expr)每个字符都必须在白名单里
③ 拒绝raise ValueError(...)有非法字符就抛出异常中断
④ 计算str(eval(expr))通过检查,安全执行

💡 all(c in allowed for c in expr) 是 Python 的生成器表达式,相当于:

for c in expr:
if c not in allowed:
return False
return True

遍历表达式的每个字符,检查是否全部合法。

第 13 行:路由装饰器

@app.route("/api/calc", methods=["POST"])

@app.route(...) 是一个装饰器(decorator),作用是:

“当收到访问 /api/calc 路径、且使用 POST 方法的请求时,执行下面的 calc() 函数。“

参数含义
"/api/calc"URL 路径(必须以 / 开头)
methods=["POST"]只接受 POST 请求

GET vs POST 的区别:

方法用途数据位置示例
GET获取数据URL 里打开网页、搜索
POST提交数据请求体里登录、提交表单、提交计算

我们的计算需要用户提交表达式,所以用 POST。

第 14-25 行:请求处理函数

def calc():
data = request.get_json(force=True, silent=True) or {}
expr = str(data.get("expr", "")).strip()
if not expr:
return jsonify({"error": "表达式为空"}), 400
try:
result = safe_eval(expr)
return jsonify({"expr": expr, "result": result})
except Exception as e:
return jsonify({"expr": expr, "error": str(e)}), 400
代码说明
15request.get_json(force=True, silent=True) or {}从请求体读取 JSON。
force=True — 即使请求头没声明 JSON 也强行解析
silent=True — 解析失败返回 None,不抛异常
or {} — 解析失败就用空字典兜底
16str(data.get("expr", "")).strip()从 JSON 中读 "expr" 字段:
data.get("expr", "") — 取不到就用 ""
str(...) — 强制转字符串
.strip() — 去两端空格
18-19if not expr: return jsonify({...}), 400表达式为空 → 返回错误。, 400 设置 HTTP 状态码 400(Bad Request)
21-25try/except 结构异常处理:
• 成功 → 返回结果(状态码 200)
• 失败 → 返回错误信息(状态码 400)
Exception as e — 把异常对象存到 e

第 28-29 行:启动服务器

if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)
参数含义
host="0.0.0.0"监听所有网络接口,局域网内其他设备也能访问
port=8000监听 8000 端口
debug=True调试模式:①修改代码自动重启;②错误时显示详细信息

if __name__ == "__main__": 的含义:只有直接运行这个文件时才执行这段代码。如果这个文件被其他文件 import,则不执行。

2.4 运行与测试

启动服务器:

Terminal window
cd /Users/alan/code/python_web
venv/bin/python app.py

看到以下输出说明启动成功:

* Running on http://127.0.0.1:8000

用 curl 测试(新开一个终端):

Terminal window
# 正常计算
curl -X POST http://localhost:8000/api/calc \
-H "Content-Type: application/json" \
-d '{"expr": "1+1"}'
# → {"expr": "1+1", "result": "2"}
# 运算符优先级
curl -X POST http://localhost:8000/api/calc \
-H "Content-Type: application/json" \
-d '{"expr": "(2+3)*4"}'
# → {"expr": "(2+3)*4", "result": "20"}
# 非法字符
curl -X POST http://localhost:8000/api/calc \
-H "Content-Type: application/json" \
-d '{"expr": "hello"}'
# → {"expr": "hello", "error": "只允许数字和 + - * / ( ) 运算符"}
# 空表达式
curl -X POST http://localhost:8000/api/calc \
-H "Content-Type: application/json" \
-d '{}'
# → {"error": "表达式为空"}

用 Python 命令行客户端:

Terminal window
venv/bin/python client.py "5+5"
# ✅ 5+5 = 10
# 交互式模式
venv/bin/python client.py
# > 10/2
# ✅ 10/2 = 5.0
# > quit

三、阶段二:添加前端页面 —— app2.py + simple.html

API 有了,现在给它加一个用户友好的网页界面,让用户用浏览器就能用。

3.1 架构思想:前后端分离

┌─────────────────┐ HTTP 请求 ┌─────────────────┐
│ 浏览器 │ ─────────────▶│ Flask 服务器 │
│ (前端) │ JSON 数据 │ (后端) │
│ │ ◀──────────── │ │
│ HTML + CSS │ │ app2.py │
│ + JavaScript │ │ │
└─────────────────┘ └─────────────────┘
  • 后端app2.py):提供数据和计算能力,只关心”算什么”和”返回什么”
  • 前端simple.html):关心用户交互和界面展示

前后端通过 JSON 格式 通信。

3.2 后端代码:app2.py(30 行)

from flask import Flask, request, jsonify, render_template
app = Flask(__name__)
@app.route("/")
def page():
return render_template("simple.html")
@app.route("/api/calc", methods=["POST"])
def calc():
data = request.get_json(force=True, silent=True) or {}
expr = str(data.get("expr", "")).strip()
if not expr:
return jsonify({"error": "请输入表达式"}), 400
allowed = set("0123456789+-*/(). ")
if not all(c in allowed for c in expr):
return jsonify({"error": "只能输入数字和 + - * / ( )"}), 400
try:
result = str(eval(expr))
return jsonify({"expr": expr, "result": result})
except Exception as e:
return jsonify({"error": str(e)}), 400
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)

3.3 与 app.py 的差异

相比 app.pyapp2.py 有三处不同:

差异app.pyapp2.py
① 导入from flask import Flask, request, jsonifyfrom flask import Flask, request, jsonify, render_template
② 页面路由@app.route("/") def page(): return render_template("simple.html")
③ 安全函数独立函数 safe_eval()检查逻辑嵌入 calc()

关键新增:render_template("simple.html")

@app.route("/")
def page():
return render_template("simple.html")
  • 当用户访问网站根路径 /(即 http://localhost:8000/)时,执行这个函数
  • render_template() 会自动去 templates/ 目录下找 simple.html 文件
  • 读取文件内容,返回给浏览器

⚠️ 注意render_template() 查找文件的路径是固定的:必须放在名为 templates/ 的文件夹中。这是 Flask 的约定。

3.4 前端代码:templates/simple.html(35 行)

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>计算器</title>
</head>
<body>
<h1>Python 计算器</h1>
<input type="text" id="expr" placeholder="例如 1+1">
<button onclick="calc()">计算</button>
<p id="result"></p>
<script>
async function calc() {
const expr = document.getElementById('expr').value.trim();
if (!expr) return;
const resp = await fetch('/api/calc', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ expr: expr })
});
const data = await resp.json();
const el = document.getElementById('result');
if (data.error) {
el.textContent = '错误:' + data.error;
} else {
el.textContent = data.expr + ' = ' + data.result;
}
}
</script>
</body>
</html>

3.5 HTML 部分详解

HTML(HyperText Markup Language,超文本标记语言)描述页面的结构

<h1>Python 计算器</h1>
<!-- 标题 -->
<input type="text" id="expr" placeholder="例如 1+1">
<!-- 文本输入框
type="text" → 这是文本输入框
id="expr" → 唯一标识,JavaScript 通过它找到这个元素
placeholder="例如 1+1" → 输入框为空时显示的灰色提示文字
-->
<button onclick="calc()">计算</button>
<!-- 按钮
onclick="calc()" → 点击时执行名为 calc() 的 JavaScript 函数
-->
<p id="result"></p>
<!-- 段落标签,用来显示结果
id="result" → JS 通过这个 id 找到它
当前为空,内容由 JavaScript 动态写入
-->

💡 id 就像给元素贴的门牌号码,JavaScript 通过门牌号码精确定位到某个元素。

3.6 JavaScript 部分详解

JavaScript 负责页面的交互逻辑

async function calc() {
关键字含义
function定义函数(类似 Python 的 def
calc函数名,对应 onclick="calc()"
async表示函数内将有异步操作(如网络请求),只有 async 函数内才能用 await

const expr = document.getElementById('expr').value.trim();

从右到左拆解:

部分作用
document.getElementById('expr')找到 id="expr" 的输入框
.value读取输入框当前的值
.trim()去掉两端空格(= Python 的 .strip()
const expr = ...保存到常量 expr

if (!expr) return;
部分含义
!expr逻辑非。空字符串 ""true;有内容 → false
return;立即退出函数,不再执行后面的代码

完整含义:用户没输入任何东西(或只输入了空格)时,不发送请求。


const resp = await fetch('/api/calc', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ expr: expr })
});

核心:向服务器发送 HTTP 请求

部分作用Python 类比
fetch(url, options)浏览器发送 HTTP 请求的标准方式requests.post(url, json=...)
'/api/calc'请求的 URL'http://localhost:8000/api/calc'
{ method: 'POST', ... }配置对象(类似 Python 字典)
method: 'POST'使用 POST 方法method='POST'
headers: { 'Content-Type': 'application/json' }告诉服务器:我发的数据是 JSON 格式headers={'Content-Type': 'application/json'}
body: JSON.stringify({ expr: expr })请求体,把 JS 对象转成 JSON 字符串json.dumps({"expr": expr})
await等待服务器响应回来再继续执行await

💡 为什么需要 await

发送网络请求就像寄快递:你把东西寄出去,不可能立刻收到回复。在等待期间,浏览器可以继续响应用户的其他操作。await 就是告诉 JavaScript:“在这里暂停一下,等对方回复后再继续执行后面的代码。”

没有 await 的话,代码会立即执行下一行,但此时响应还在路上,会拿到空值。


const data = await resp.json();

把响应体(JSON 字符串)解析成 JavaScript 对象。例如:

输入: '{"expr":"1+1","result":"2"}' (字符串)
输出: { expr: "1+1", result: "2" } (JS 对象)

之后就可以用 data.exprdata.resultdata.error 来取值了(类似 Python 的 data["expr"])。


const el = document.getElementById('result');

找到 id="result"<p> 元素,存到变量 el 里(el 是 element 的缩写,“元素”的意思)。


if (data.error) {
el.textContent = '错误:' + data.error;
} else {
el.textContent = data.expr + ' = ' + data.result;
}

根据后端返回结果显示内容:

情况后端返回页面显示
✅ 成功{"expr":"1+1","result":"2"}1+1 = 2
❌ 错误{"error":"只能输入数字和..."}错误:只能输入数字和...
代码作用
if (data.error)如果返回数据中有 error 字段
el.textContent = ...修改元素里显示的文字
+字符串拼接(和 Python 的 + 一样)

🔒 安全要点:用 textContent 而不是 innerHTML

  • textContent:把内容当作纯文本处理,浏览器不会解析其中的 HTML 标签
  • innerHTML:把内容当作 HTML 代码处理,其中的 <script> 等可能被执行

如果用户输入 <script>alert('hello')</script> 这样的恶意内容,用 textContent 会原样显示文字,用 innerHTML 会执行其中的脚本。所以显示用户输入的内容时,永远用 textContent

3.7 完整的前后端交互流程

用户在浏览器打开 http://localhost:8000
↓ 第 1 次请求(GET /)
Flask 执行 page() 函数 → 返回 simple.html
浏览器收到 HTML,渲染出页面(标题、输入框、按钮)
用户输入 "5+5",点击"计算"
浏览器触发 onclick="calc()",开始执行 JS 函数
document.getElementById('expr').value → "5+5"
第 2 次请求(POST /api/calc)
fetch('/api/calc', {
method: 'POST',
body: JSON.stringify({ expr: "5+5" })
})
Flask 收到请求,执行 calc() 函数
→ safe_eval("5+5") → "10"
返回 JSON: {"expr": "5+5", "result": "10"}
JavaScript 解析 JSON,更新页面
→ el.textContent = "5+5 = 10"
用户看到结果 ✅

3.8 运行测试

启动带前端的服务器:

Terminal window
cd /Users/alan/code/python_web
venv/bin/python app2.py

在浏览器中访问:

http://localhost:8000

看到输入框 → 输入 1+1 → 点击”计算” → 显示 1+1 = 2


四、JavaScript ↔ Python 速查表

学习前端时,把 JS 语法和已知的 Python 做对照,可以快速建立直觉:

JavaScriptPython含义
const x = ...x = ...定义常量
let x = ...x = ...定义可变变量
var x = ...旧版变量定义(不推荐)
document.getElementById('x')通过 id 找 HTML 元素
.value读取输入框的值
.trim().strip()去两端空格
if (!x)if not x:如果 x 为假
fetch(url, {...})requests.post(url, json=...)发送 HTTP 请求
awaitawait等待异步操作完成
JSON.stringify(obj)json.dumps(dict)对象转 JSON 字符串
resp.json()resp.json()响应体解析为对象
obj.keydict["key"]取对象属性 / 字典值
obj['key']dict["key"]另一种取值方式
+ 拼接字符串+ 拼接字符串字符串拼接
el.textContent = ...设置元素显示的文字
el.innerHTML = ...设置元素的 HTML 内容(⚠️ 有安全风险)
async functionasync def异步函数定义
// 注释# 注释单行注释
/* 多行 */"""多行"""多行注释
{ key: value }{ "key": value }对象 / 字典字面量
[1, 2, 3][1, 2, 3]数组 / 列表字面量

五、核心注意事项

5.1 安全方面

❌ 永远不要相信用户输入

风险原因防范措施
eval() 执行任意代码eval("__import__('os').system('rm -rf /')")字符白名单检查
XSS 跨站脚本攻击用户输入 <script>...</script>前端用 textContent,后端 HTML 转义
SQL 注入拼接 SQL 字符串使用参数化查询(本项目不涉及数据库)

本项目的安全做法:

  1. 后端字符白名单:只允许 0-9 + - * / ( ) . 空格
  2. 前端 textContent:不把用户输入当 HTML 解析

5.2 Flask 约定

事项约定
模板文件位置必须放在 templates/ 文件夹中
静态文件位置必须放在 static/ 文件夹中(CSS、图片等)
render_template()会自动去 templates/ 下找文件
debug=True仅开发环境使用!生产环境必须关闭

5.3 HTTP 基础

概念含义
GET获取资源,参数在 URL 中
POST提交数据,参数在请求体中
状态码 200成功
状态码 400客户端错误(请求数据不对)
状态码 404资源不存在(路径不对)
状态码 500服务器内部错误(代码崩溃)
Content-Type: application/json请求/响应体是 JSON 格式

5.4 JSON 格式

前后端通信的数据格式,非常简单:

{
"expr": "1+1",
"result": "2"
}
JSON 类型Python 对应示例
对象dict{"key": "value"}
数组list[1, 2, 3]
字符串str"hello"
数字int/float42, 3.14
布尔booltrue, false
nullNonenull

5.5 异步与 await

async function calc() {
const resp = await fetch('/api/calc', {...}); // 这里在"等"
const data = await resp.json(); // 这里也在"等"
}

规则

  • async 函数内才能使用 await
  • await 会暂停当前函数,等异步操作完成后继续执行
  • 等待期间,浏览器不会卡死,可以继续响应其他操作

六、扩展思路(可自行尝试)

掌握基础后,可以尝试以下改进,练习 Flask 和 JS:

  1. 添加样式:在 HTML 中加入 CSS,美化页面
  2. 回车键触发计算:用户按下回车就计算,不需要点击按钮
  3. 历史记录:每次计算结果都显示在页面上,形成历史列表
  4. 多个按钮:预设一些常用按钮(清除、1+1、π 等)
  5. 支持变量:让用户定义变量(如 x=5,然后 x*3
  6. 添加更多接口:例如 /api/sum/api/power,扩展 API

七、启动命令速查

Terminal window
cd /Users/alan/code/python_web
# 创建虚拟环境(首次)
python3 -m venv venv
# 安装 Flask(首次)
venv/bin/pip install flask
# 启动纯 API 版
venv/bin/python app.py
# 启动带前端页面版
venv/bin/python app2.py
# 命令行客户端
venv/bin/python client.py
# curl 测试
curl -X POST http://localhost:8000/api/calc \
-H "Content-Type: application/json" \
-d '{"expr": "1+1"}'

八、学习路径总结

第 1 步 app.py 理解 Flask 路由、请求、JSON 响应
第 2 步 client.py 理解如何用 Python 调用 API
第 3 步 simple.html 理解 HTML 结构和 JS 事件
第 4 步 app2.py + simple.html 前后端协作,完整流程
第 5 步 自己动手改代码 在练习中巩固理解

建议学习方法:先让代码跑起来 → 改一行、看效果 → 再改、再看 → 逐步理解每一行的作用。

Comments

Quiet notes for this article.