
Man can do what he wants but he cannot want what he wants - 亚瑟·叔本华 "人虽然能够做他所想做的,但不能要他所想要的"
Python,Flask, Bootstrap5 基于网络api构建一个笔迹清除应用
主要目地是方便普通用户使用: - 功能单一 - 流程清晰 - 使用无门槛
擦除试卷手写笔迹的web api
我选择 https://www.textin.com/document/text_auto_removal
根据官网的示例代码,我选择python版本:
import requests
import json
def get_file_content(filePath):
with open(filePath, 'rb') as fp:
return fp.read()
class CommonOcr(object):
def __init__(self, img_path):
# 请登录后前往 “工作台-账号设置-开发者信息” 查看 x-ti-app-id
# 示例代码中 x-ti-app-id 非真实数据
self._app_id = 'c81f*************************e9ff'
# 请登录后前往 “工作台-账号设置-开发者信息” 查看 x-ti-secret-code
# 示例代码中 x-ti-secret-code 非真实数据
self._secret_code = '5508***********************1c17'
self._img_path = img_path
def recognize(self):
# 自动擦除手写文字
url = 'https://api.textin.com/ai/service/v1/handwritten_erase'
head = {}
try:
image = get_file_content(self._img_path)
head['x-ti-app-id'] = self._app_id
head['x-ti-secret-code'] = self._secret_code
result = requests.post(url, data=image, headers=head)
return result.text
except Exception as e:
return e
if __name__ == "__main__":
response = CommonOcr(r'example.jpg')
print(response.recognize())
安装python环境,建立一个虚拟环境venv
python3 -m venv myvenv
激活虚拟环境
source myvenv/bin/activate or myvenv\script\activate.bat
## 安装必要的package
python -m pip install requests
python -m pip install flask
以下是一个使用 Python、Flask 和 Bootstrap 5 构建的简单 Web 应用的参考实现,能够让用户上传试卷文件并通过 API 擦除笔迹。这个实现主要展示了文件上传和处理的流程,API 调用部分你可以根据实际的擦除笔迹 API 来集成。
exam_marks_eraser/
│
├── app.py # 主应用文件
├── static/ # 静态文件(如 CSS、图片等)
│ └── styles.css
└── templates/
├── index.html # 主页面
└── result.html # 结果页面
app.py代码处理文件上传,api处理,以及显示已经上传和处理的文件列表
import os
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, flash, send_file
import requests
from dotenv import load_dotenv
import base64
import json
from celery import Celery
import time
app = Flask(__name__)
# 配置 Celery
app.config['CELERY_BROKER_URL'] = 'redis://localhost:6379/0' # Redis作为消息代理
app.config['CELERY_RESULT_BACKEND'] = 'redis://localhost:6379/0'
celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'])
celery.conf.update(app.config)
app.config['UPLOAD_FOLDER'] = 'uploads/'
# PROCESSED_FOLDER = 'processed/' # 处理后的文件存储目录
app.config['PROCESSED_FOLDER'] = 'processed/'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 限制文件大小为16MB
app.secret_key = 'supersecretkey'
def get_file_content(filePath):
with open(filePath, 'rb') as fp:
return fp.read()
class CommonOcr(object):
def __init__(self, img_path):
# 请登录后前往 “工作台-账号设置-开发者信息” 查看 x-ti-app-id
load_dotenv() # 加载 .env 文件
self._app_id = os.getenv('APP_ID')
self._secret_code = os.getenv('SECRET_CODE')
# 示例代码中 x-ti-app-id 非真实数据
# self._app_id = 'd19ecaa770409041a6ae2d392bcf566c'
# 请登录后前往 “工作台-账号设置-开发者信息” 查看 x-ti-secret-code
# 示例代码中 x-ti-secret-code 非真实数据
# self._secret_code = '629f242b0f05e685c95f8994558c7a1e'
self._img_path = img_path
def recognize(self):
# 自动擦除手写文字
url = 'https://api.textin.com/ai/service/v1/handwritten_erase'
head = {}
try:
image = get_file_content(self._img_path)
head['x-ti-app-id'] = self._app_id
head['x-ti-secret-code'] = self._secret_code
result = requests.post(url, data=image, headers=head)
return result.text
except Exception as e:
return e
def process_image(input_file):
# 假设CommonOcr是一个定义好的函数或类,负责图像识别
response = CommonOcr(input_file)
result = response.recognize()
# print(result)
json_response = json.loads(result)
image_data_base64 = json_response['result']['image']
image_data = base64.b64decode(image_data_base64)
# 生成输出文件名,添加后缀xxxx_output
base, ext = os.path.splitext(input_file)
output_file = f"{base}_output{ext}"
with open(output_file, 'wb') as file:
file.write(image_data)
print(f"Output saved to {output_file}")
# 创建上传文件夹
if not os.path.exists(app.config['UPLOAD_FOLDER']):
os.makedirs(app.config['UPLOAD_FOLDER'])
if not os.path.exists(app.config['PROCESSED_FOLDER']):
os.makedirs(app.config['PROCESSED_FOLDER'])
def erase_marks(file_path):
"""
通过 API 擦除文件中的笔迹。
这里只是一个伪函数,你需要用真实 API 来替换。
"""
response = CommonOcr(file_path)
result = response.recognize()
json_response = json.loads(result)
if json_response['code'] == 200:
image_data_base64 = json_response['result']['image']
image_data = base64.b64decode(image_data_base64)
output_file_path = os.path.join(app.config['PROCESSED_FOLDER'], 'cleaned_' + os.path.basename(file_path))
with open(output_file_path, 'wb') as f:
f.write(image_data)
return output_file_path
else:
flash('处理失败,请稍后重试。')
return None
@app.route('/', methods=['GET', 'POST'])
def index():
# 获取已上传的文件
uploaded_files = os.listdir(app.config['UPLOAD_FOLDER'])
processed_files = os.listdir(app.config['PROCESSED_FOLDER'])
if request.method == 'POST':
# 检查是否上传了文件
if 'file' not in request.files:
flash('没有文件上传')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
flash('没有选择文件', 'warning')
return redirect(request.url)
if file:
file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
file.save(file_path)
# 通过 API 处理文件
processed_file_path = erase_marks(file_path)
if processed_file_path:
return redirect(url_for('result', filename=os.path.basename(processed_file_path)))
return render_template('index.html', uploaded_files=uploaded_files, processed_files=processed_files)
@app.route('/result/<filename>')
def result(filename):
return render_template('result.html', filename=filename)
@app.route('/uploads/<filename>')
def uploaded_file(filename):
#return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if os.path.exists(file_path):
# 返回带有附件标志的文件
return send_file(file_path, as_attachment=True)
else:
flash('文件未找到!')
return redirect(url_for('index'))
# 已处理文件的下载路由
@app.route('/processed/<filename>')
def download_processed_file(filename):
file_path = os.path.join(app.config['PROCESSED_FOLDER'], filename)
if os.path.exists(file_path):
# 返回带有附件标志的文件
return send_file(file_path, as_attachment=True)
else:
flash('文件未找到!')
return redirect(url_for('index'))
# return send_from_directory(app.config['PROCESSED_FOLDER'], filename)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
页面风格
为了让页面拥有现代、简约并带有科技未来感的风格,你可以使用较深的背景色调(如深蓝或黑色),搭配流畅的线条和渐变效果,使用光滑的按钮、阴影效果以及酷炫的字体样式。以下是 static/styles.css 的一个参考样式:
- static/styles.css
/* 全局样式 */
body {
background-color: #0e1d36; /* 深蓝色背景,科技感 */
color: #ffffff; /* 文字颜色为白色,突出与背景的对比 */
font-family: 'Roboto', sans-serif; /* 使用科技感较强的字体 */
margin: 0;
padding: 0;
}
h1 {
font-weight: 300;
text-transform: uppercase;
letter-spacing: 2px;
color: #00d4ff; /* 明亮的青色,带来未来感 */
text-shadow: 0 0 5px rgba(0, 212, 255, 0.6);
}
.container {
max-width: 600px;
margin: 100px auto;
padding: 20px;
background: #16243b; /* 深色背景容器,区分主体与背景 */
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
/* 表单样式 */
input[type="file"] {
background: transparent;
color: #00d4ff;
border: 2px dashed #00d4ff; /* 虚线边框,科技感十足 */
padding: 10px;
width: 100%;
border-radius: 10px;
transition: border 0.3s ease;
}
input[type="file"]:hover {
border: 2px dashed #00f2ff; /* hover 时的边框颜色变亮 */
}
button {
background: linear-gradient(90deg, #00d4ff, #0072ff); /* 渐变按钮 */
color: #ffffff;
border: none;
padding: 12px;
width: 100%;
border-radius: 50px; /* 圆滑按钮,现代感 */
font-size: 16px;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 8px 20px rgba(0, 212, 255, 0.4); /* 按钮光影效果 */
transition: background 0.3s ease;
}
button:hover {
background: linear-gradient(90deg, #0072ff, #00d4ff); /* 鼠标悬停时改变渐变方向 */
box-shadow: 0 12px 25px rgba(0, 212, 255, 0.7);
}
/* 链接按钮 */
a.btn-success {
background: linear-gradient(90deg, #00ff99, #00d4ff); /* 下载按钮也有渐变 */
border-radius: 50px;
text-transform: uppercase;
letter-spacing: 1px;
padding: 12px 20px;
color: white;
text-decoration: none;
display: inline-block;
box-shadow: 0 8px 20px rgba(0, 255, 153, 0.4);
transition: background 0.3s ease;
}
a.btn-success:hover {
background: linear-gradient(90deg, #00d4ff, #00ff99);
box-shadow: 0 12px 25px rgba(0, 255, 153, 0.7);
}
/* 提示信息样式 */
.alert-danger {
background-color: #ff3b3b;
border: none;
border-radius: 5px;
color: white;
padding: 10px;
text-align: center;
box-shadow: 0 4px 15px rgba(255, 59, 59, 0.5);
}
/* 其他样式保持不变 */
/* 为 btn-secondary 按钮添加自定义样式 */
.btn-secondary {
background: linear-gradient(90deg, #6c757d, #5a6268); /* 渐变背景色 */
color: #ffffff; /* 文字颜色为白色 */
border: none; /* 去掉默认边框 */
padding: 12px 20px; /* 添加内边距 */
border-radius: 50px; /* 圆滑按钮边角 */
font-size: 16px; /* 字体大小 */
text-transform: uppercase; /* 全部大写 */
letter-spacing: 1px; /* 字符间距 */
box-shadow: 0 8px 20px rgba(108, 117, 125, 0.4); /* 添加阴影效果 */
transition: background 0.3s ease, box-shadow 0.3s ease; /* 平滑过渡效果 */
}
.btn-secondary:hover {
background: linear-gradient(90deg, #5a6268, #6c757d); /* 鼠标悬停时的渐变背景色变化 */
box-shadow: 0 12px 25px rgba(108, 117, 125, 0.7); /* 鼠标悬停时的阴影效果变化 */
}
.btn-secondary:focus, .btn-secondary:active {
outline: none; /* 去掉焦点轮廓 */
box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5); /* 修改焦点时的阴影效果 */
}
/* 自定义底部样式 */
.footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #16243b;
color: white;
text-align: center; /* 居中对齐文本 */
padding: 10px;
font-size: 14px;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.2); /* 可选:添加阴影 */
}
.footer-content {
display: flex;
flex-direction: column;
align-items: center;
}
.footer a {
color: #ffffff;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
/* 自定义背景色 */
.list-group-item {
background-color: #343a40; /* 深色背景 */
color: #f8f9fa; /* 浅色字体 */
border: 1px solid #495057; /* 深色边框 */
}
/* 自定义悬停效果 */
.list-group-item:hover {
background-color: #495057; /* 悬停时的深色背景 */
color: #f8f9fa; /* 悬停时的浅色字体 */
}
/* 自定义文件上传控件样式 */
.custom-file-input {
background-color: #343a40; /* 深色背景 */
color: #f8f9fa; /* 浅色文字 */
border: 1px solid #495057; /* 深色边框 */
border-radius: .375rem; /* 圆角 */
}
/* 自定义文件选择按钮的样式 */
.custom-file-input::-webkit-file-upload-button {
background-color: #495057; /* 深色按钮背景 */
color: #f8f9fa; /* 按钮文字颜色 */
border: none; /* 去掉边框 */
padding: .375rem .75rem; /* 内边距 */
border-radius: .375rem; /* 圆角 */
}
/* 在 Firefox 和其他浏览器中应用自定义样式 */
.custom-file-input::file-selector-button {
background-color: #495057;
color: #f8f9fa;
border: none;
padding: .375rem .75rem;
border-radius: .375rem;
}
.custom-file-input:hover {
background-color: #495057; /* 鼠标悬停时背景色 */
border-color: #6c757d; /* 鼠标悬停时边框色 */
}
/* 自定义 Flash 消息样式 */
.alert {
border-radius: .375rem; /* 圆角边框 */
padding: .75rem 1.25rem; /* 内边距 */
margin-bottom: 1rem; /* 下边距 */
}
.alert-success {
background-color: #28a745; /* 成功消息背景色 */
color: #f8f9fa; /* 成功消息文字颜色 */
border-color: #1e7e34; /* 成功消息边框颜色 */
}
.alert-danger {
background-color: #dc3545; /* 错误消息背景色 */
color: #f8f9fa; /* 错误消息文字颜色 */
border-color: #c82333; /* 错误消息边框颜色 */
}
.alert-info {
background-color: #17a2b8; /* 信息消息背景色 */
color: #f8f9fa; /* 信息消息文字颜色 */
border-color: #117a8b; /* 信息消息边框颜色 */
}
.alert-warning {
background-color: #ffc107; /* 警告消息背景色 */
color: #212529; /* 警告消息文字颜色 */
border-color: #e0a800; /* 警告消息边框颜色 */
}
.alert-dismissible .btn-close {
filter: invert(1); /* 使关闭按钮的颜色与背景颜色协调 */
}
页面模板
- index.html
- result.html
index.html首页页面,有个文件选择控件,列出已经上传的文件,以及处理好的文件列表链接,可以直接点击下载
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>试卷笔迹擦除工具</title>
<!-- 引入 Bootstrap 样式 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- 引入自定义的 CSS 文件 -->
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
<div class="container mt-5">
<h1 class="text-center">上传试卷文件</h1>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="alert-container">
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- 文件上传表单 -->
<form action="/" method="post" enctype="multipart/form-data">
<div class="mb-3">
<!-- <label for="file-input" class="form-label">Choose file</label> -->
<input class="custom-file-input" type="file" name="file" required>
</div>
<button type="submit" class="btn btn-primary w-100">上传并处理</button>
</form>
<!-- 已上传文件列表 -->
<h2 class="mt-5">已上传文件列表</h2>
{% if uploaded_files %}
<ul class="list-group">
{% for file in uploaded_files %}
<li class="list-group-item">
{{ file }}
</li>
{% endfor %}
</ul>
{% else %}
<p>尚未上传文件</p>
{% endif %}
<!-- 已处理文件列表 -->
<h2 class="mt-5">已处理文件列表</h2>
{% if processed_files %}
<ul class="list-group">
{% for file in processed_files %}
<li class="list-group-item">
<a href="{{ url_for('download_processed_file', filename=file) }}">{{ file }}</a>
</li>
{% endfor %}
</ul>
{% else %}
<p>尚未处理文件</p>
{% endif %}
</div>
<footer class="footer">
<div class="footer-content">
<p>© 2024 你的公司名. 版权所有.</p>
<p>联系方式: <a href="mailto:contact@example.com">contact@example.com</a></p>
</div>
</footer>
</body>
</html>
result.html,结果页面,处理完成后,重定向到结果页面,用户可以下载擦除笔迹后的文件;也可以点击直接返回主页,继续上传新文件或者点击下载已经处理好的文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>处理结果</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
<div class="container mt-5">
<h1 class="text-center">处理完成</h1>
<div class="text-center">
<a href="{{ url_for('download_processed_file', filename=filename) }}" class="btn btn-success mt-3">下载处理后的文件</a>
<a href="{{ url_for('index') }}" class="btn btn-secondary mt-3">返回上传页面</a>
</div>
</div>
<footer class="footer">
<div class="footer-content">
<p>© 2024 你的公司名. 版权所有.</p>
<p>联系方式: <a href="mailto:contact@example.com">contact@example.com</a></p>
</div>
</footer>
</body>
</html>
运行效果
- 运行flask app
简单做法: python app.py
或者
export FLASK_APP=app.py
python -m flask run --host 0.0.0.0
注意事项 需要定义一个.env文件,将自己申请的web api的key 和id放在里面,调用web api的时候需要用到
在生产环境部署,使用gunicorn
在生产环境中部署 Flask 应用需要使用一个稳定、安全的配置,而不仅仅依赖于开发环境中的 flask run。生产环境通常包括一个实际的 Web 服务器和一个 WSGI 服务器,以确保应用能够处理多个并发请求,并提供适当的安全性和性能。
部署 Flask 应用的步骤:
使用 WSGI 服务器(如 Gunicorn 或 uWSGI) Flask 内置的开发服务器不适合生产环境。你需要使用一个 WSGI 兼容的服务器,如 Gunicorn 或 uWSGI。Gunicorn 是一种轻量级的、多线程的 WSGI HTTP 服务器,适合生产环境。
选择 Web 服务器(如 Nginx 或 Apache) Nginx 或 Apache 是常用的 Web 服务器,用于反向代理并提供静态文件的服务。Nginx 更常用于现代的 Flask 部署中。
配置和部署步骤
- Step 1: 安装依赖 在你的生产服务器上,你需要首先确保 Python 及相关依赖已安装。
sudo apt update
sudo apt install python3-pip python3-venv nginx
- Step 2: 创建 Python 虚拟环境并安装依赖 在生产服务器上,为 Flask 应用创建虚拟环境,并安装所需的 Python 包。
cd /path/to/your/flask/app
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
- Step 3: 安装并配置 Gunicorn 安装 Gunicorn 来处理 WSGI 请求:
pip install gunicorn
使用以下命令测试 Gunicorn 运行 Flask 应用:
gunicorn --bind 0.0.0.0:8000 wsgi:app
这里假设你的 Flask 应用文件叫 wsgi.py,其中定义了 app 作为 Flask 实例。
- Step 4: 配置 Nginx 作为反向代理 在生产环境中,你通常会使用 Nginx 来处理客户端请求并将它们代理到 Gunicorn。
安装 Nginx:
sudo apt install nginx
配置 Nginx:
创建一个新的 Nginx 配置文件:
sudo nano /etc/nginx/sites-available/myflaskapp
文件配置内容:
server {
listen 80;
server_name your_domain_or_IP;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /static/ {
alias /path/to/your/flask/app/static/;
}
}
确保将 /path/to/your/flask/app/static/ 替换为你的 Flask 项目中静态文件的路径。 启用 Nginx 配置:
创建一个指向新配置的符号链接,并重新加载 Nginx:
sudo ln -s /etc/nginx/sites-available/myflaskapp /etc/nginx/sites-enabled
sudo systemctl restart nginx
- Step 5: 配置 Gunicorn 作为服务
为了让 Gunicorn 在系统启动时自动运行,可以将其配置为一个 systemd 服务。
创建一个新的服务文件:
sudo nano /etc/systemd/system/myflaskapp.service
在该文件中,添加以下内容:
[Unit]
Description=Gunicorn instance to serve myflaskapp
After=network.target
[Service]
User=youruser
Group=www-data
WorkingDirectory=/path/to/your/flask/app
ExecStart=/path/to/your/flask/app/venv/bin/gunicorn --workers 3 --bind 127.0.0.1 wsgi:app
[Install]
WantedBy=multi-user.target
假定应用是wsgi.py,里面定义了app 启动并启用服务:
sudo systemctl start myflaskapp
sudo systemctl enable myflaskapp
使用socket通信没有搞定,暂时用ip端口绑定通信

评论