← Назад к курсу

Применение фреймворка Flask для создания веб-приложений на Python

Цель урока

Освоить основы создания веб-приложений с использованием микрофреймворка Flask и научиться создавать простые, но функциональные веб-решения.

Теоретическая часть

1. Что такое Flask?

Flask - это микрофреймворк для создания веб-приложений на Python. Он называется "микро", потому что его ядро простое, но расширяемое.

Преимущества Flask:

  • Легковесность и простота
  • Гибкость и расширяемость
  • Встроенный сервер для разработки
  • Поддержка модульного тестирования
  • Хорошая документация и активное сообщество

2. Установка Flask

pip install flask

3. Базовая структура Flask-приложения

from flask import Flask

# Создание экземпляра приложения
app = Flask(__name__)

# Определение маршрута
@app.route('/')
def home():
    return 'Привет, мир!'

# Запуск приложения
if __name__ == '__main__':
    app.run(debug=True)

Практическая часть

Проект 1: Простое веб-приложение "Блог"

Шаг 1: Базовая структура

# app.py
from flask import Flask, render_template

app = Flask(__name__)

# Моковые данные (в реальном приложении будет база данных)
posts = [
    {
        'id': 1,
        'title': 'Первая запись',
        'content': 'Это содержимое первой записи в блоге',
        'author': 'Иван Иванов',
        'date': '2024-01-15'
    },
    {
        'id': 2,
        'title': 'Вторая запись',
        'content': 'Продолжаем вести наш блог',
        'author': 'Петр Петров',
        'date': '2024-01-16'
    }
]

@app.route('/')
def index():
    return render_template('index.html', posts=posts)

@app.route('/post/<int:post_id>')
def show_post(post_id):
    post = next((p for p in posts if p['id'] == post_id), None)
    if post:
        return render_template('post.html', post=post)
    return 'Запись не найдена', 404

if __name__ == '__main__':
    app.run(debug=True)

Шаг 2: Создаем шаблоны

templates/base.html (базовый шаблон):

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Мой Блог{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <nav>
        <a href="{{ url_for('index') }}">Главная</a>
    </nav>
    
    <div class="container">
        {% block content %}{% endblock %}
    </div>
    
    <footer>
        <p>© 2024 Мой Блог</p>
    </footer>
</body>
</html>

templates/index.html:

{% extends "base.html" %}

{% block title %}Главная страница{% endblock %}

{% block content %}
    <h1>Последние записи</h1>
    
    {% for post in posts %}
        <article class="post">
            <h2>
                <a href="{{ url_for('show_post', post_id=post.id) }}">
                    {{ post.title }}
                </a>
            </h2>
            <p class="meta">
                Автор: {{ post.author }} | Дата: {{ post.date }}
            </p>
            <p>{{ post.content[:100] }}...</p>
        </article>
    {% endfor %}
{% endblock %}

templates/post.html:

{% extends "base.html" %}

{% block title %}{{ post.title }}{% endblock %}

{% block content %}
    <article class="post-detail">
        <h1>{{ post.title }}</h1>
        <p class="meta">
            Автор: {{ post.author }} | Дата: {{ post.date }}
        </p>
        <div class="content">
            {{ post.content }}
        </div>
        <a href="{{ url_for('index') }}">← Назад к списку</a>
    </article>
{% endblock %}

Шаг 3: Создаем статические файлы

static/style.css:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: Arial, sans-serif;
    line-height: 1.6;
    color: #333;
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
}

nav {
    background-color: #333;
    padding: 1rem;
    margin-bottom: 2rem;
}

nav a {
    color: white;
    text-decoration: none;
    padding: 0.5rem 1rem;
}

nav a:hover {
    background-color: #555;
}

.container {
    min-height: 70vh;
}

.post {
    border: 1px solid #ddd;
    padding: 1.5rem;
    margin-bottom: 1.5rem;
    border-radius: 5px;
}

.post h2 a {
    color: #333;
    text-decoration: none;
}

.post h2 a:hover {
    color: #007bff;
}

.meta {
    color: #666;
    font-size: 0.9rem;
    margin-bottom: 1rem;
}

.post-detail {
    background-color: #f9f9f9;
    padding: 2rem;
    border-radius: 5px;
}

footer {
    text-align: center;
    margin-top: 2rem;
    padding: 1rem;
    border-top: 1px solid #ddd;
}

Проект 2: REST API для управления задачами

# api.py
from flask import Flask, request, jsonify

app = Flask(__name__)

# Хранилище данных (в реальном приложении используйте БД)
tasks = []
next_id = 1

# Вспомогательная функция
def find_task(task_id):
    for task in tasks:
        if task['id'] == task_id:
            return task, tasks.index(task)
    return None, -1

# Получить все задачи
@app.route('/api/tasks', methods=['GET'])
def get_tasks():
    return jsonify({'tasks': tasks})

# Получить одну задачу
@app.route('/api/tasks/<int:task_id>', methods=['GET'])
def get_task(task_id):
    task, _ = find_task(task_id)
    if task:
        return jsonify(task)
    return jsonify({'error': 'Task not found'}), 404

# Создать новую задачу
@app.route('/api/tasks', methods=['POST'])
def create_task():
    global next_id
    
    if not request.json or not 'title' in request.json:
        return jsonify({'error': 'Title is required'}), 400
    
    task = {
        'id': next_id,
        'title': request.json['title'],
        'description': request.json.get('description', ''),
        'done': False
    }
    
    tasks.append(task)
    next_id += 1
    
    return jsonify(task), 201

# Обновить задачу
@app.route('/api/tasks/<int:task_id>', methods=['PUT'])
def update_task(task_id):
    task, index = find_task(task_id)
    
    if not task:
        return jsonify({'error': 'Task not found'}), 404
    
    updated_task = {
        'id': task_id,
        'title': request.json.get('title', task['title']),
        'description': request.json.get('description', task['description']),
        'done': request.json.get('done', task['done'])
    }
    
    tasks[index] = updated_task
    return jsonify(updated_task)

# Удалить задачу
@app.route('/api/tasks/<int:task_id>', methods=['DELETE'])
def delete_task(task_id):
    task, index = find_task(task_id)
    
    if not task:
        return jsonify({'error': 'Task not found'}), 404
    
    tasks.pop(index)
    return jsonify({'result': 'Task deleted'})

if __name__ == '__main__':
    app.run(debug=True, port=5001)

Проект 3: Форма с загрузкой файлов

# upload_app.py
import os
from flask import Flask, render_template, request, redirect, url_for
from werkzeug.utils import secure_filename

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB
app.config['ALLOWED_EXTENSIONS'] = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}

# Создаем папку для загрузок, если её нет
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']

@app.route('/')
def index():
    # Получаем список загруженных файлов
    files = os.listdir(app.config['UPLOAD_FOLDER'])
    return render_template('upload_form.html', files=files)

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return redirect(request.url)
    
    file = request.files['file']
    
    if file.filename == '':
        return redirect(request.url)
    
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        return redirect(url_for('index'))
    
    return 'Файл не разрешен'

if __name__ == '__main__':
    app.run(debug=True, port=5002)

templates/upload_form.html:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Загрузка файлов</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        .upload-form {
            background: #f5f5f5;
            padding: 20px;
            border-radius: 5px;
            margin-bottom: 30px;
        }
        .file-list {
            margin-top: 20px;
        }
        .file-item {
            padding: 10px;
            border-bottom: 1px solid #ddd;
        }
    </style>
</head>
<body>
    <h1>Загрузка файлов</h1>
    
    <div class="upload-form">
        <form method="POST" action="/upload" enctype="multipart/form-data">
            <input type="file" name="file" required>
            <button type="submit">Загрузить</button>
        </form>
    </div>
    
    <div class="file-list">
        <h2>Загруженные файлы:</h2>
        {% for file in files %}
            <div class="file-item">{{ file }}</div>
        {% else %}
            <p>Файлы не загружены</p>
        {% endfor %}
    </div>
</body>
</html>

Продвинутые концепции

1. Использование базы данных (SQLAlchemy)

from flask_sqlalchemy import SQLAlchemy

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    author = db.Column(db.String(100), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    def to_dict(self):
        return {
            'id': self.id,
            'title': self.title,
            'content': self.content,
            'author': self.author,
            'created_at': self.created_at.isoformat()
        }

2. Аутентификация пользователей

from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required

login_manager = LoginManager(app)
login_manager.login_view = 'login'

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True)
    password_hash = db.Column(db.String(128))

3. Обработка ошибок

@app.errorhandler(404)
def page_not_found(error):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_error(error):
    db.session.rollback()
    return render_template('500.html'), 500

Домашнее задание

Задание 1: Расширьте блог

Добавьте в блог следующие функции:

  1. Форму для создания новых записей
  2. Возможность редактирования и удаления записей
  3. Поиск по записям
  4. Пагинацию (по 5 записей на странице)

Задание 2: Создайте API для блога

Создайте REST API для блога со следующими endpoint'ами:

  • GET /api/posts - получить все записи
  • GET /api/posts/<id> - получить конкретную запись
  • POST /api/posts - создать новую запись
  • PUT /api/posts/<id> - обновить запись
  • DELETE /api/posts/<id> - удалить запись

Задание 3: Добавьте аутентификацию

Реализуйте систему регистрации и входа пользователей для API, используя JWT-токены.

Полезные ресурсы

  1. Официальная документация Flask: https://flask.palletsprojects.com/
  2. Flask Mega-Tutorial (Miguel Grinberg): https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world
  3. Flask на Habr: https://habr.com/ru/hub/flask/

Заключение

Flask - это мощный и гибкий инструмент для создания веб-приложений на Python. Он позволяет начать с простых проектов и постепенно добавлять сложность по мере необходимости. Ключевые преимущества Flask - его простота и расширяемость, что делает его отличным выбором как для начинающих, так и для опытных разработчиков.