Руководство по Werkzeug
Добро пожаловать в руководство по Werkzeug, в котором мы создадим клон сервиса TinyURL, сохраняющий URL-адреса в экземпляре redis. Библиотеки, которые мы будем использовать для этого приложения, — это Jinja 2 для шаблонов, redis для уровня базы данных и, конечно же, Werkzeug для уровня WSGI.
Вы можете использовать pip для установки необходимых библиотек:
pip install Jinja2 redis Werkzeug
Также убедитесь, что на вашей локальной машине запущен сервер redis. Если вы работаете в OS X, вы можете использовать brew для его установки:
brew install redis
Если вы работаете в Ubuntu или Debian, вы можете использовать apt-get:
sudo apt-get install redis-server
Redis был разработан для систем UNIX и никогда не был предназначен для работы в Windows. Однако неофициальные порты хорошо работают в целях разработки. Вы можете получить их на github.
Представление сервиса Shortly
В этом руководстве мы вместе создадим простой сервис сокращения URL-адресов с помощью Werkzeug. Имейте в виду, что Werkzeug не является фреймворком, это библиотека с утилитами для создания собственного фреймворка или приложения, и поэтому она очень гибкая. Применяемый здесь подход — лишь один из многих возможных.
В качестве хранилища данных мы будем использовать redis вместо реляционной базы данных, чтобы упростить задачу, и потому, что redis отлично подходит для подобных задач.
Конечный результат будет примерно таким:
Шаг 0: Введение в WSGI
Werkzeug — это утилитарная библиотека для WSGI. Сам WSGI — это протокол или соглашение, гарантирующее, что ваше веб-приложение может взаимодействовать с веб-сервером, и что веб-приложения работают вместе без проблем.
Базовое приложение «Hello World» в WSGI без помощи Werkzeug выглядит так:
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
return ['Hello World!']
Веб-приложение WSGI — это то, что вы можете вызвать и передать ему словарь environ и start_response вызываемый объект. environ содержит всю входящую информацию, а функция start_response используется для обозначения начала ответа. С помощью Werkzeug вам не нужно взаимодействовать непосредственно с этими объектами, так как предоставляются объекты request и response для работы с ними.
Данные запроса принимают объект environ и позволяют вам получить доступ к данным из environ удобным образом. Объект response — это само по себе приложение WSGI и предоставляет более удобный способ создания ответов.
Вот как вы бы написали это приложение с использованием объектов response:
from werkzeug.wrappers import Response
def application(environ, start_response):
response = Response('Hello World!', mimetype='text/plain')
return response(environ, start_response)
И вот расширенная версия, которая анализирует строку запроса в URL (а главное, параметр name в URL для замены «World» на другое слово):
from werkzeug.wrappers import Request, Response
def application(environ, start_response):
request = Request(environ)
text = 'Hello %s!' % request.args.get('name', 'World')
response = Response(text, mimetype='text/plain')
return response(environ, start_response)
И это всё, что вам нужно знать о WSGI.
Шаг 1: Создание папок
Прежде чем начать, давайте создадим папки, необходимые для этого приложения:
/shortly
/static
/templates
Папка shortly не является пакетом Python, а просто местом, куда мы помещаем наши файлы. Непосредственно в эту папку мы затем поместим наш главный модуль на последующих шагах. Файлы внутри папки static доступны пользователям приложения через HTTP. Это место, куда помещаются файлы CSS и JavaScript. Внутри папки templates будет находиться место, где Jinja2 будет искать шаблоны. Созданные вами шаблоны позже в руководстве будут помещены в этот каталог.
Шаг 2: Базовая структура
Теперь давайте приступим к созданию модуля для нашего приложения. Давайте создадим файл под названием shortly.py в папке shortly. Сначала нам понадобятся импорты. Я приведу все импорты сюда, даже если они не будут сразу использованы, чтобы не создавать путаницы:
import os import redis import urlparse from werkzeug.wrappers import Request, Response from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, NotFound from werkzeug.wsgi import SharedDataMiddleware from werkzeug.utils import redirect from jinja2 import Environment, FileSystemLoader
Затем мы можем создать базовую структуру нашего приложения и функцию для создания нового экземпляра, по желанию с фрагментом WSGI-средства, экспортирующего все файлы в папке static в веб:
class Shortly(object):
def __init__(self, config):
self.redis = redis.Redis(config['redis_host'], config['redis_port'])
def dispatch_request(self, request):
return Response('Hello World!')
def wsgi_app(self, environ, start_response):
request = Request(environ)
response = self.dispatch_request(request)
return response(environ, start_response)
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
def create_app(redis_host='localhost', redis_port=6379, with_static=True):
app = Shortly({
'redis_host': redis_host,
'redis_port': redis_port
})
if with_static:
app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
'/static': os.path.join(os.path.dirname(__file__), 'static')
})
return app
Наконец, мы можем добавить код, который запустит локальный сервер разработки с автоматической перезагрузкой кода и отладчиком:
if __name__ == '__main__':
from werkzeug.serving import run_simple
app = create_app()
run_simple('127.0.0.1', 5000, app, use_debugger=True, use_reloader=True)
Основная идея состоит в том, что наш класс Shortly является фактическим приложением WSGI. Метод __call__ непосредственно перенаправляет вызов на wsgi_app. Это сделано для того, чтобы мы могли обернуть wsgi_app для применения средних уровней, как мы делаем в функции create_app. Фактический метод wsgi_app создает объект Request и вызывает метод dispatch_request, который должен вернуть объект Response, который затем оценивается как приложение WSGI снова. Как вы можете видеть: черепахи до самого низа. Как создаваемый нами класс Shortly, так и любой объект запроса в Werkzeug реализуют интерфейс WSGI. В результате вы даже можете вернуть другое приложение WSGI из метода dispatch_request.
Функция-фабрика create_app может быть использована для создания нового экземпляра нашего приложения. Она не только передаст некоторые параметры в качестве конфигурации приложению, но также по желанию добавит WSGI-средство, которое экспортирует статические файлы. Таким образом, мы можем получить доступ к файлам из папки static, даже если мы не настраиваем наш сервер для их предоставления, что очень полезно при разработке.
Интермедия: Запуск приложения
Теперь вы должны быть в состоянии выполнить файл с помощью python и увидеть сервер на вашем локальном компьютере:
$ python shortly.py * Running on http://127.0.0.1:5000/ * Restarting with reloader: stat() polling
Он также сообщает вам, что релоадер активен. Он будет использовать различные методы для определения того, изменились ли какие-либо файлы на диске, и затем автоматически перезапустится.
Просто перейдите по URL-адресу, и вы увидите «Hello World!».
Шаг 3: Среда
Теперь, когда у нас есть базовый класс приложения, мы можем заставить конструктор выполнять полезные действия и предоставить несколько помощников, которые могут пригодиться. Нам нужно будет иметь возможность отображать шаблоны и подключаться к redis, поэтому давайте немного расширим класс:
def __init__(self, config):
self.redis = redis.Redis(config['redis_host'], config['redis_port'])
template_path = os.path.join(os.path.dirname(__file__), 'templates')
self.jinja_env = Environment(loader=FileSystemLoader(template_path),
autoescape=True)
def render_template(self, template_name, **context):
t = self.jinja_env.get_template(template_name)
return Response(t.render(context), mimetype='text/html')
Шаг 4: Маршрутизация
Следующим шагом является маршрутизация. Маршрутизация — это процесс сопоставления и анализа URL-адреса с тем, что мы можем использовать. Werkzeug предоставляет гибкую интегрированную систему маршрутизации, которую мы можем использовать для этого. Она работает так, что вы создаёте экземпляр Map и добавляете несколько объектов Rule. Каждый правило имеет шаблон, который он будет пытаться сопоставить с URL-адресом, и «точку входа». Точка входа обычно представляет собой строку и может использоваться для уникальной идентификации URL-адреса. Мы также можем использовать её для автоматического обратного преобразования URL-адреса, но в этом руководстве мы этого не будем делать.
Просто добавьте это в конструктор:
self.url_map = Map([
Rule('/', endpoint='new_url'),
Rule('/<short_id>', endpoint='follow_short_link'),
Rule('/<short_id>+', endpoint='short_link_details')
])
Здесь мы создаём карту URL с тремя правилами. / для корня пространства URL, где мы просто перенаправим вызов на функцию, которая реализует логику создания нового URL-адреса. Затем одно правило, которое следует за короткой ссылкой до целевого URL-адреса, и ещё одно с тем же правилом, но с плюсом (+) в конце для отображения подробностей ссылки.
Итак, как мы можем перейти от точки входа к функции? Это зависит от вас. В этом руководстве мы будем делать это, вызывая метод on_ + endpoint в самом классе. Вот как это работает:
def dispatch_request(self, request):
adapter = self.url_map.bind_to_environ(request.environ)
try:
endpoint, values = adapter.match()
return getattr(self, 'on_' + endpoint)(request, **values)
except HTTPException, e:
return e
Мы привязываем карту URL к текущей среде и получаем URLAdapter. Адаптер может использоваться для сопоставления запроса, а также для обратного преобразования URL-адресов. Метод match вернёт точку входа и словарь значений в URL. Например, для правила follow_short_link есть переменная часть под названием short_id. Когда мы перейдём по адресу http://localhost:5000/foo, мы получим следующие значения:
endpoint = 'follow_short_link'
values = {'short_id': u'foo'}
Если ничего не будет найдено, будет поднято исключение NotFound, которое является HTTPException. Все исключения HTTP также являются приложениями WSGI сами по себе, которые отображают страницу ошибки по умолчанию. Поэтому мы просто перехватываем все из них и возвращаем ошибку саму по себе.
Если всё пройдёт хорошо, мы вызываем функцию on_ + endpoint и передаём в неё запрос в качестве аргумента, а также все аргументы URL в качестве ключевых аргументов, и возвращаем объект ответа, который возвращает этот метод.
Шаг 5: Первый вид
Давайте начнём с первого вида: создания новых URL-адресов:
def on_new_url(self, request):
error = None
url = ''
if request.method == 'POST':
url = request.form['url']
if not is_valid_url(url):
error = 'Please enter a valid URL'
else:
short_id = self.insert_url(url)
return redirect('/%s+' % short_id)
return self.render_template('new_url.html', error=error, url=url)
Эта логика должна быть понятна. В основном мы проверяем, является ли метод запроса POST, в этом случае мы валидируем URL-адрес и добавляем новую запись в базу данных, а затем перенаправляем на страницу деталей. Это означает, что нам нужно написать функцию и вспомогательный метод. Для проверки URL-адреса этого достаточно:
def is_valid_url(url):
parts = urlparse.urlparse(url)
return parts.scheme in ('http', 'https')
Для вставки URL-адреса нам нужен только этот небольшой метод в нашем классе:
def insert_url(self, url):
short_id = self.redis.get('reverse-url:' + url)
if short_id is not None:
return short_id
url_num = self.redis.incr('last-url-id')
short_id = base36_encode(url_num)
self.redis.set('url-target:' + short_id, url)
self.redis.set('reverse-url:' + url, short_id)
return short_id
reverse-url: + URL будет хранить короткий идентификатор. Если URL уже был отправлен, это не будет None, и мы можем просто вернуть это значение, которое будет коротким идентификатором. В противном случае мы увеличиваем ключ last-url-id и преобразуем его в базу 36. Затем мы сохраняем ссылку и обратную запись в redis. И вот функция для преобразования в базу 36:
def base36_encode(number):
assert number >= 0, 'positive integer required'
if number == 0:
return '0'
base36 = []
while number != 0:
number, i = divmod(number, 36)
base36.append('0123456789abcdefghijklmnopqrstuvwxyz'[i])
return ''.join(reversed(base36))
Таким образом, для работы этого вида нам не хватает шаблона. Мы создадим его позже, давайте сначала напишем другие виды, а затем сделаем шаблоны за один раз.
Шаг 6: Вид перенаправления
Вид перенаправления прост. Всё, что ему нужно сделать, — это найти ссылку в redis и перенаправить на неё. Кроме того, мы также увеличим счётчик, чтобы знать, сколько раз на ссылку нажимали:
def on_follow_short_link(self, request, short_id):
link_target = self.redis.get('url-target:' + short_id)
if link_target is None:
raise NotFound()
self.redis.incr('click-count:' + short_id)
return redirect(link_target)
В этом случае мы будем поднимать исключение NotFound вручную, если URL-адрес не существует, что вызовет работу функции dispatch_request и преобразует его в стандартный ответ 404.
Шаг 7: Вид подробностей
Вид подробностей ссылки очень похож, мы просто снова отображаем шаблон. В дополнение к поиску целевого адреса, мы также запрашиваем у redis количество нажатий на ссылку и устанавливаем значение по умолчанию в ноль, если такой ключ ещё не существует:
def on_short_link_details(self, request, short_id):
link_target = self.redis.get('url-target:' + short_id)
if link_target is None:
raise NotFound()
click_count = int(self.redis.get('click-count:' + short_id) or 0)
return self.render_template('short_link_details.html',
link_target=link_target,
short_id=short_id,
click_count=click_count
)
Пожалуйста, имейте в виду, что redis всегда работает со строками, поэтому вам нужно преобразовать количество нажатий в int вручную.
Шаг 8: Шаблоны
И вот все шаблоны. Просто поместите их в папку templates. Jinja2 поддерживает наследование шаблонов, поэтому в первую очередь мы создадим шаблон макета с блоками-заполнителями. Мы также настроили Jinja2 так, что он автоматически экранирует строки с правилами HTML, поэтому нам не нужно тратить время на это самим. Это предотвращает атаки XSS и ошибки рендеринга.
layout.html:
<!doctype html>
<title>{% block title %}{% endblock %} | shortly</title>
<link rel=stylesheet href=/static/style.css type=text/css>
<div class=box>
<h1><a href=/>shortly</a></h1>
<p class=tagline>Shortly is a URL shortener written with Werkzeug
{% block body %}{% endblock %}
</div>
new_url.html:
{% extends "layout.html" %}
{% block title %}Create New Short URL{% endblock %}
{% block body %}
<h2>Submit URL</h2>
<form action="" method=post>
{% if error %}
<p class=error><strong>Error:</strong> {{ error }}
{% endif %}
<p>URL:
<input type=text name=url value="{{ url }}" class=urlinput>
<input type=submit value="Shorten">
</form>
{% endblock %}
short_link_details.html:
{% extends "layout.html" %}
{% block title %}Details about /{{ short_id }}{% endblock %}
{% block body %}
<h2><a href="/{{ short_id }}">/{{ short_id }}</a></h2>
<dl>
<dt>Full link
<dd class=link><div>{{ link_target }}</div>
<dt>Click count:
<dd>{{ click_count }}
</dl>
{% endblock %}
Шаг 9: Стиль
Чтобы это выглядело лучше, чем уродливый черно-белый вариант, вот простой стильный файл:
static/style.css:
body { background: #E8EFF0; margin: 0; padding: 0; }
body, input { font-family: 'Helvetica Neue', Arial,
sans-serif; font-weight: 300; font-size: 18px; }
.box { width: 500px; margin: 60px auto; padding: 20px;
background: white; box-shadow: 0 1px 4px #BED1D4;
border-radius: 2px; }
a { color: #11557C; }
h1, h2 { margin: 0; color: #11557C; }
h1 a { text-decoration: none; }
h2 { font-weight: normal; font-size: 24px; }
.tagline { color: #888; font-style: italic; margin: 0 0 20px 0; }
.link div { overflow: auto; font-size: 0.8em; white-space: pre;
padding: 4px 10px; margin: 5px 0; background: #E5EAF1; }
dt { font-weight: normal; }
.error { background: #E8EFF0; padding: 3px 8px; color: #11557C;
font-size: 0.9em; border-radius: 2px; }
.urlinput { width: 300px; }
Бонус: Усовершенствования
Посмотрите на реализацию в примере словаря в репозитории Werkzeug, чтобы увидеть версию этого руководства с некоторыми небольшими усовершенствованиями, такими как пользовательская страница 404.
© 2007–2020 Pallets
Licensed under the BSD 3-clause License.
https://werkzeug.palletsprojects.com/en/0.16.x/tutorial/