Python. Основы веб-сервера Flask
Время чтения: 8 минут
Представь, что ты написал скрипт, который может пригодиться кому-то кроме тебя – как им поделиться?
У тебя есть три варианта:
- Выложить исходный код в открытый доступ (например на GitHub) – для этого пользователь твоей программы должен уметь запускать Python скрипты.
- Подготовить исполняемый файл (например через PyInstaller) – скрипт будет готов к запуску, и пользователь сможет запустить его как обычное приложение.
- Развернуть веб-сервер, на котором будет исполняться твоя программа – любой пользователь сможет воспользоваться ей через браузер.
Первые два варианта будут запускаться на машине пользователя, а вот для третьего варианта тебе понадобится сервер.
Веб-сервер идеально подойдёт, если ты хочешь централизованно хранить данные пользователя и иметь над ними полный контроль.
Веб-сервер
Если ты слабо представляешь себе что такое сервер, то обратись к этой статье. В ней я рассказываю всё, что поможет тебе представить полную картину.
Что такое веб-сервер? Это специализированный сервер, который может возвращать пользователю статические или динамические HTML страницы.
- Статические – эти страницы никак не изменяются сервером, и просто отдаются пользователю (например страницы на википедии – они для всех одинаковые)
- Динамические – эти страницы изменяются перед отправкой пользователю (например твоя страница в соцсети – она изменяется специально под тебя)
Веб-сервер в Python
В Python есть 2 самых распространённых фреймворка:
- Django – предоставляет множество функций и подходит для больших проектов
- Flask – предоставляет минимум функций и подходит для небольших проектов
Фреймворк (framework, каркас) – готовая основа приложения, которую можно легко расширять под свои задачи
В этой статье мы рассмотрим фреймворк Flask, потому что на нём будет проще начать изучение веб-разработки.
Простейший веб-сервер с использованием Flask
Сперва необходимо установить этот фреймворк – выполни в консоли эту команду:
pip install Flask
pip – это инструмент установки сторонних пакетов, встроенный в Python.
После установки Flask, создай скрипт и скопируй туда этот код:
from flask import Flask
app = Flask('my_first_server')
@app.route('/')
def hello_world():
return 'Hello World'
app.run(port=1234)
Всего 6 строчек и веб-сервер готов – вот мощь фреймворков. Минусом же является то, что тебе придётся разбираться в том, как заставить этот фреймворк делать то, что тебе надо.
Сейчас мы разберём что тут написано, но сперва давай проверим что всё работает.
Запусти этот скрипт и открой в браузере адрес: 127.0.0.1:1234
Ты увидишь текст, который возвращается в функции hello_world
.
Теперь давай разбираться!
Почему 127.0.0.1? Это IP адрес интерфейса обраной петли (loopback interface) – если ты указываешь этот адрес, то твой компьютер заходит сам на себя. Обычно этот адрес используется при разработке приложения, которое принимает данные по сети (наш случай).
Порт 1234 – это порт, на котором твой скрипт принимает данные по сети (указан в последней строчке). Как ты мог узнать из статьи про сервер, порты от 1024 до 49152 могут использоваться различными серверами – ты сейчас как раз разрабатываешь сервер.
Теперь по коду:
# Подключение пакета flask, и импортирование из него класса Flask
# В классе Flask реализованы все функции веб-сервера
from flask import Flask
# Создаём объект класса Flask - наш веб-сервер
app = Flask('my_first_server')
# Создаём функцию hello_world(), и заворачиваем её в декоратор route с параметром "/"
# Этот декоратор регистрирует в "app" функции, которые будут обрабатывать клиентские
# запросы по указанному пути. В данном случае, если клиент ничего не указывает
# в запросе, то мы вызываем эту функцию.
@app.route('/')
def hello_world():
return 'Hello World'
# Запускаем наш веб-сервер на порте 1234. При вызове run() веб-сервер входит в бесконечный
# цикл, в котором он будет обрабатывать все входящие запросы, и вызвать функции, которые
# мы зарегистрировали через декоратор route.
app.run(port=1234)
Давай добавим обработку нового пути:
from flask import Flask
app = Flask('my_first_server')
@app.route('/')
def hello_world():
return 'Hello World'
@app.route('/test')
def this_is_test_handler_func():
return '<H1 style="color:red;">test</H1>'
app.run(port=1234)
Попробуй перезапустить скрипт и зайти на: 127.0.0.1:1234/test
Вот, теперь твой сервер поддерживает несколько разных запросов:
- Если клиент ничего не указывает после адреса, то мы вернём строку “Hello World”
- Если клиент указал путь “/test”, то мы верём красную строку “test”, написанную крупным шрифтом
Функция, которая вызывается для различных запросов, называется обработчиком запроса.
Возвращаем HTML страницу
Статическая HTML страница
В папке со своим скриптом создай папку “templates” и создай в ней файл “index.html”.
Скопируй этот текст в этот файл:
<html>
<body>
<h1>Current date and time: 2021-10-24 15:03:59</h1>
</body>
</html>
Этими действиями ты создал шаблон HTML файла, который можно теперь отдавать через веб-сервер.
Сначала добавь импорт новой функции render_template
из пакета Flask:
from flask import Flask, render_template
А затем добавь новый обработчик для нового запроса “/time”:
@app.route('/time')
def time_handler():
return render_template("index.html")
Функция render_template
возвращает текст файла, который ты укажешь. По умолчанию она ищет только среди файлов, находящихся в папке “templates”.
Попробуй перезапустить скрипт и зайти на: 127.0.0.1:1234/time
Ты увидишь страницу “index.html”, которую ты только что добавил.
Динамическая HTML страница
Похоже что на этой странице время остаётся одним и тем же – давай сделаем так, чтобы всегда выводилось текущее время.
Для этого надо зайти в наш шаблон “index.html”, и заменить статическое время на вот такое выражение:
<html>
<body>
<h1>Current date and time: {{cur_datetime}}</h1>
</body>
</html>
Что такое “{{cur_datetime}}”? Это значение шаблона, которое мы теперь можем подставлять из нашего Python скрипта.
Поменяй обработчик запроса “/time” таким образом:
# Добавь это в начало файла
from datetime import datetime
@app.route('/time')
def time_handler():
cur_datetime_str = datetime.now().strftime("%d-%m-%Y %H:%M:%S")
return render_template("index.html", cur_datetime=cur_datetime_str)
Что тут изменилось:
- Добавился пакет datetime, который позволяет получать текущее время и дату
- В параметры функции
render_template
добавился параметрcur_datetime
– такой же как тот, который мы прописали в шаблоне
Попробуй перезапустить скрипт и проверь что изменилось. Обнови страницу несколько раз, и посмотри в какие моменты обновляется время.
Добавляем взаимодействие с пользователем
Давай добавим немного интерактива! Я модифицировал скрипт и шаблон таким образом, чтобы мы могли получать от пользователя его ник и сообщение.
На главной странице мы выводим ник и сообщение от последнего пользователя – получилось что-то отдалённо похожее на стену ВКонтакте.
Вот текст скрипта (комментарии описывают все новые моменты):
from flask import Flask, render_template, request
from datetime import datetime
import sqlite3
app = Flask('my_first_server')
DB_NAME = 'messages.db'
def init_wall_data():
'''
Открываем соединение с базой. Если файла базы ещё нет, то он создастся.
Создаём таблицу с названием wall. Если она уже создана, то ничего не делаем.
В этой таблице есть:
- численный id, уникальный для каждой записи
- строка nick - тут будет ник пользователя
- строка message - тут будет сообщение, которое он оставил
'''
conn = sqlite3.connect(DB_NAME)
conn.execute('''create table if not exists wall (
id INTEGER PRIMARY KEY,
nick TEXT,
message TEXT)''')
# Эта строчка сохраняет внесённые изменения -- она нужна при
# создании таблиц и при добавлении/обновлении/удалении полей
conn.commit()
def set_wall_data(nick, message):
'''
Записываем новое сообщение в таблицу wall
'''
conn = sqlite3.connect(DB_NAME)
conn.execute('insert into wall(nick, message) values(?, ?)', (nick, message))
conn.commit()
def get_wall_data():
'''
Забираем последнюю запись из таблицы wall
'''
conn = sqlite3.connect(DB_NAME)
cursor = conn.execute('select nick, message from wall')
rows = cursor.fetchall()
return rows[-1] if rows else (None, None)
def render_main_page():
'''
Отдельная функция, которая подставляет в шаблон все необходимые значения
'''
cur_datetime_str = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
wall_data = get_wall_data()
return render_template("index.html", cur_datetime=cur_datetime_str, nick=wall_data[0], message=wall_data[1])
@app.route('/wall', methods=['POST'])
def response():
'''
Обработчик, который принимает данные от пользователя,
вставляет их в базу и возвращает обновлённую страничку
'''
nick = request.form.get("nick")
message = request.form.get("message")
set_wall_data(nick, message)
return render_main_page()
@app.route('/')
def handle_time():
return render_main_page()
init_wall_data()
app.run(port=1234)
Вот текст изменённого шаблона:
<html>
<body>
<h3>Current date and time: {{cur_datetime}}</h3>
<br>
<h3>Last message:</h3>
{% if nick %}
<b>{{nick}} says: </b>
{% endif %}
{% if message %}
<p>{{message}}</p>
{% else %}
<p>This wall is empty</p>
{% endif %}
<br>
<b>New wall message:</b>
<form method="POST" action="/wall">
Nick: <input type="text" name="nick" required><br>
Message: <input type="text" name="message" required><br>
<input type="submit" value="Send">
</form>
</body>
</html>
Из нового в шаблоне конструкции “if” – они позволяют не выводить какие-то блоки, если переданное значение шаблона пустое.
Обнови скрипт и шаблон, и проверь как оно работает – теперь надо заходить на: 127.0.0.1:1234 (я убрал “/time”)
Разберись в том, что происходит. Если что-то не понятно из комментариев, то напиши мне – я объясню и дополню объяснение.
Если ты не знаком с SQL, который был здесь использован, то можешь обратиться к этой статье.
Выводим сразу все сообщения пользователей
Выводить только последнее сообщение как-то скучно – давай сделаем полноценную стену, чтобы было видно все сообщения.
В скрипте я изменил две функции:
def get_wall_data():
'''
Забираем все записи из таблицы wall
'''
conn = sqlite3.connect(DB_NAME)
cursor = conn.execute('select nick, message from wall')
rows = cursor.fetchall()
return rows
def render_main_page():
'''
Отдельная функция, которая подставляет в шаблон все необходимые значения
'''
cur_datetime_str = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
wall_data = get_wall_data()
return render_template("index.html", cur_datetime=cur_datetime_str,
len=len(wall_data), wall_data=wall_data)
Теперь скрипт передаёт в шаблон список всех сообщений.
Так я изменил шаблон:
<html>
<body>
<h3>Current date and time: {{cur_datetime}}</h3>
<h3>Wall messages:</h3>
{% for i in range(0, len) %}
<b>{{wall_data[i][0]}} says: </b>
<p>{{wall_data[i][1]}}</p>
{% endfor %}
<br>
<b>New wall message:</b>
<form method="POST" action="/wall">
Nick: <input type="text" name="nick" required><br>
Message: <input type="text" name="message" required><br>
<input type="submit" value="Send">
</form>
</body>
</html>
В шаблон добавлен цикл, который проходясь по всему списку, наполняет страницу сообщениями.
Все вот эти {% if %}, {% for %} и {{значения}} используются фреймворком Flask для генерации веб-страниц – в обычном HTML так делать нельзя.
Зайди на страницу, посмотри что изменилось.
Задание на закрепление
В получившемся веб-сайте добавь две функции:
- Если пользователь пытается второй раз отправить ничем не отличающиеся данные – не вставляй их в базу
- Отображай время, когда сообщение было оставлено:
- Добавь в запрос создания таблицуы wall строковое поле “time” (удали файл базы данных, чтобы таблица пересоздалась)
- При вставке новой записи в таблицу wall, записывай в “time” строкой текущее время (бери его так же, как мы выводим текущее время)
- Модифицируй шаблон и функцию
get_wall_data()
, чтобы в сгенерированной странице для каждого сообщения выводилось время, когда оно было оставлено
Заключение
Итого, мы изучили:
- Веб-сервер (сервер, выдающий HTML страницы)
- Статические и динамические страницы (одинаковые для всех, создаются под каждого пользователя)
- Фреймворк (расширяемая основа приложения)
- Django и Flask (сложный и мощный, простой и лёгкий)
- Создание веб-сервера
- Функции-обработчики для разных запросов
- Шаблоны HTML страниц
- Подстановка значений в шаблон
Если что – пиши, я помогу и постараюсь объяснить лучше.