# Python 实现的极简网页记事本
这是一个基于 Flask 和 Waitress 实现的轻量级在线记事本,灵感来自 [minimalist-web-notepad](https://github.com/pereorga/minimalist-web-notepad)。
## 特点
- 极简设计,无需登录
- 自动保存
- 支持开机自启
- 基于文件系统存储
- 自动清理过期笔记
## 技术栈
- Python 3.x
- Flask (Web框架)
- Waitress (WSGI服务器)
- JavaScript (前端自动保存)
## 核心代码
### 1. 主应用 (app.py)
```python
from flask import Flask, request, render_template, redirect, url_for, abort
import os
import string
import random
from datetime import datetime, timedelta
import logging
from logging.handlers import RotatingFileHandler
from config import Config
import re
app = Flask(__name__)
app.config.from_object(Config)
# 配置日志
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = RotatingFileHandler('logs/minitex.log', maxBytes=10240, backupCount=10)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Minitex startup')
def generate_id(length=5):
"""生成随机记事本ID"""
chars = string.ascii_lowercase + string.digits
return ''.join(random.choice(chars) for _ in range(length))
def get_note_path(note_id):
"""获取记事本文件路径"""
if not re.match(r'^[a-z0-9]+$', note_id):
abort(400)
return os.path.join(app.config['DATA_DIR'], note_id + '.txt')
def cleanup_old_notes():
"""清理过期笔记"""
try:
cutoff_date = datetime.now() - timedelta(days=app.config['NOTE_LIFETIME_DAYS'])
for filename in os.listdir(app.config['DATA_DIR']):
filepath = os.path.join(app.config['DATA_DIR'], filename)
if os.path.getmtime(filepath) < cutoff_date.timestamp():
os.remove(filepath)
app.logger.info(f'Deleted old note: {filename}')
except Exception as e:
app.logger.error(f'Error during cleanup: {str(e)}')
@app.route('/')
def index():
"""主页 - 创建新记事本"""
try:
note_id = generate_id(app.config['NOTE_ID_LENGTH'])
while os.path.exists(get_note_path(note_id)):
note_id = generate_id(app.config['NOTE_ID_LENGTH'])
os.makedirs(app.config['DATA_DIR'], exist_ok=True)
return redirect(url_for('note', note_id=note_id))
except Exception as e:
app.logger.error(f'Error creating new note: {str(e)}')
abort(500)
@app.route('/<note_id>')
def note(note_id):
"""显示记事本页面"""
try:
note_path = get_note_path(note_id)
content = ''
if os.path.exists(note_path):
with open(note_path, 'r', encoding='utf-8') as f:
content = f.read()
return render_template('note.html', content=content)
except Exception as e:
app.logger.error(f'Error accessing note {note_id}: {str(e)}')
abort(500)
@app.route('/<note_id>', methods=['POST'])
def save_note(note_id):
"""保存记事本内容"""
try:
content = request.form.get('content', '')
if len(content.encode('utf-8')) > app.config['MAX_NOTE_SIZE']:
abort(413)
note_path = get_note_path(note_id)
os.makedirs(app.config['DATA_DIR'], exist_ok=True)
with open(note_path, 'w', encoding='utf-8') as f:
f.write(content)
cleanup_old_notes()
return 'OK'
except Exception as e:
app.logger.error(f'Error saving note {note_id}: {str(e)}')
abort(500)
@app.errorhandler(404)
def not_found_error(error):
return render_template('error.html', error='页面未找到'), 404
@app.errorhandler(500)
def internal_error(error):
return render_template('error.html', error='服务器内部错误'), 500
@app.errorhandler(413)
def request_entity_too_large(error):
return render_template('error.html', error='内容超出大小限制'), 413
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8082)
2. 配置文件 (config.py)
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard-to-guess-string'
DATA_DIR = 'data'
MAX_CONTENT_LENGTH = 1 * 1024 * 1024
NOTE_ID_LENGTH = 5
MAX_NOTE_SIZE = 500 * 1024
NOTE_LIFETIME_DAYS = 30
3. 记事本模板 (templates/note.html)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Minimalist Web Notepad</title>
<style>
body {
margin: 0;
padding: 0;
height: 100vh;
}
textarea {
width: 100%;
height: 100vh;
border: none;
padding: 20px;
font-family: monospace;
font-size: 14px;
box-sizing: border-box;
resize: none;
outline: none;
}
</style>
</head>
<body>
<textarea id="content" autofocus>{{ content }}</textarea>
<script>
let textarea = document.getElementById('content');
let timeoutId;
textarea.addEventListener('input', function() {
clearTimeout(timeoutId);
timeoutId = setTimeout(saveContent, 1000);
});
function saveContent() {
fetch(window.location.pathname, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'content=' + encodeURIComponent(textarea.value)
});
}
</script>
</body>
</html>
4. 错误页面模板 (templates/error.html)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>错误 - Minimalist Web Notepad</title>
<style>
body {
margin: 40px auto;
max-width: 650px;
line-height: 1.6;
font-size: 18px;
color: #444;
padding: 0 10px;
font-family: sans-serif;
}
h1 {
line-height: 1.2;
}
</style>
</head>
<body>
<h1>出错了</h1>
<p>{{ error }}</p>
<p><a href="/">返回首页</a></p>
</body>
</html>
5. WSGI入口 (wsgi.py)
from app import app
if __name__ == "__main__":
app.run()
6. 启动脚本 (start.bat)
@echo off
set FLASK_ENV=production
python -m waitress --host=0.0.0.0 --port=8082 wsgi:app
7. 开机自启动脚本 (startup.bat)
@echo off
cd /d %~dp0
start /min cmd /c start.bat
部署步骤
- 安装依赖:
pip install flask waitress
- 创建必要的目录:
mkdir data logs
- 配置开机自启:
$WshShell = New-Object -comObject WScript.Shell
$Shortcut = $WshShell.CreateShortcut("$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\MinitexNotepad.lnk")
$Shortcut.TargetPath = "$(Get-Location)\startup.bat"
$Shortcut.WorkingDirectory = "$(Get-Location)"
$Shortcut.WindowStyle = 7
$Shortcut.Save()
- 启动服务:
.\start.bat
使用说明
- 访问 http://localhost:8082 自动创建新记事本
- 直接在页面上编辑内容,会自动保存
- 通过URL分享笔记
- 笔记在30天后自动清理
注意事项
- 确保端口 8082 未被占用
- 需要 Python 3.x 环境
- 建议在生产环境中修改配置文件中的密钥
- 可以根据需要调整笔记的过期时间
改进空间
- 添加密码保护功能
- 实现笔记加密存储
- 添加富文本编辑功能
- 支持多语言界面
- 添加笔记分享限制