Руководство по 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 вам не нужно иметь дело напрямую ни с тем, ни с другим, так как предоставляются объекты запроса и ответа для работы с ними.
Данные запроса берут объект environ и позволяют вам получить доступ к данным из этого environ удобным способом. Объект ответа — это само по себе WSGI-приложение и предоставляет гораздо более удобный способ создания ответов.
Вот как вы бы написали это приложение с объектами ответа:
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.middleware.shared_data 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-адресом до целевого 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 количество нажатий на ссылку и устанавливаем значение по умолчанию 0, если такого ключа ещё нет:
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/1.0.x/tutorial/