Содержание
Создание сайтов с помощью python
Когда мы открываем в браузере страницу, например http://kodomo.fbb.msu.ru/wiki/Main/Python, сервер присылает нам в ответ некий текст на языке html. На самом деле, нам не важно, получен этот текст из html-файла, или сгенерирован какой-то программой с учётом параметров нашего запроса. Если полный текст страницы на самом деле лежит на сервере, то такую страницу называют статической, если нет, и она создаётся на лету, то такая страница называется динамической.
CGI
Понятие динамической страницы возникло почти одновременно с возникновением самого веба (моментом возникновения веба считают момент создания формата html и появления первого браузера).
В первую очередь для создания динамических страниц была придумана идея (и стандарт) CGI – Common Gateway Interface ("Общий интерфейс шлюзов" – по-моему, бесполезно искать глубокого смысла в этом словосочетании). Суть CGI в следующем: когда к HTTP-серверу приходит запрос, и он решает, что этот запрос нужно отдать CGI-скрипту, он запускает этот CGI-скрипт, устанавливает в переменных среды информацию о параметрах запроса и, если это был POST, то отправляет на стандартный ввод CGI-скрипта данные POST; затем он собирает выдачу со стандартного вывода скрипта, создаёт ответ и отправляет обратно. Выдача CGI-скрипта должна выглядеть так: HTTP-заголовки, пустая строка, тело страницы. Сам же CGI-скрипт – это любая программа, написанная на любом языке программирования.
По какому признаку HTTP-сервер решает, что он хочет обрабатывать запрос CGI-скриптом – это вопрос настроек сервера. В случае, например, Apache (самого популярного на сегодня HTTP-сервера), это делается по сочетанию фактов, что запрос расшифровался в файл, на котором стоит (с точки зрения ОС) исполняемый бит, для которого с точки зрения сервера выставлена настройка ExecCGI, и для которого установлен обработчик cgi. (Сервер Apache знаменит тем, что профессиональные администраторы этого сервера умеют довольно быстро добиваться от него нужного состояния – а все остальные считают его настройки ужасно запутанными).
Например, если мы положим вот такую программу куда-нибудь, откуда исполняются CGI-скрипты, и откроем в браузере соответствующий адрес, то мы увидим страницу с датой сервера.
Вдобавок к тому, что CGI-скрипты довольно просто писать на питоне непосредственно, в питоне есть ещё модуль cgi, который предоставляет в относительно удобном виде данные запроса: адрес запроса (где лежит скрипт), имя хоста в запросе (например, kodomo может быть виден под именами kodomo.fbb.msu.ru и kodomo.cmm.msu.ru и наш скрипт может вести себя по-разному в зависимости от того, по какому из этих адресов его запсутили), параметры адресной строки (параметры форм, отправленных через GET – вспоминаем предыдущий рассказ), адрес клиента, браузер клиента и т.п.
CGI – это протокол взаимодействия HTTP-сервера и скрипта, который генерирует динамические страницы для этого сервера. Это хороший протокол, но у него есть некоторые ограничения.
Во-первых, это именно протокол. Он говорит, что нужно делать, но не даёт никаких средств, как это делать. Если бы мы делали большой сайт из CGI-скриптов, нам бы пришлось выдумывать средства, как использовать стандартные шапки на большинстве страниц; кроме того, оказалось бы, что выдавать из питона html не очень удобно, и мы бы придумывали ещё и какой-нибудь способ писать большинство вёрстки на html и встраивать в неё относительно небольшие куски, меняемые питоном.
Во-вторых, CGI в сущности описывает, как обрабатывать одной программой один адрес. Если мы хотим сделать динамический набор страниц, то скорее всего, мы хотели бы, чтобы все эти страницы генерировались одной и той же программой (в которой какая-нибудь функция выбирала бы, что показывать в какой-нибудь части страницы в зависимости от адреса запроса). Это не является большой проблемой в CGI и на самом деле, для этой проблемы есть решение – некоторые HTTP-сервера умеют понимать, что CGI скриптом является не последнее имя в адресе запроса, а середина.
В-третьих, – это вопрос оптимизации – запуск одного процесса – это довольно дорогостоящая операция с точки зрения устройства ОС. Если наш HTTP-сервер обслуживает несколько тысяч запросов в секунду, то запускать на каждый запрос новый процесс оказывается слишком дорого. Чтобы решить эту проблему, придумали некоторое количество альтернативных протоколов к CGI, которые подразумевают, что скрипт, создающий динамические страницы, работает всегда, и HTTP-сервер в момент поступления к нему запроса, отправляет (например, так же через стандартный ввод-вывод) этот запрос к нему. Примеры таких протоколов: FastCGI или WSGI.
Фреймворки
Чтобы решить эти проблемы – в первую очередь, первую – люди стали делать библиотеки и фреймворки для создания динамических страниц.
Фреймворк – это в первую очередь модное слово, которое значит примерно то же, что и библиотека. Некоторые люди любят иногда сочинять такие вот слова. Можно говорить о том, что есть некоторые тонкости значений: фреймворк подразумевает кроме библиотеки ещё набор каких-то вспомогательных средств – вспомогательных скриптов, заготовок программ. Или, иногда дают такое определение: если ваша программа вызывает функцию, которую написал другой человек – то это вы пользуетесь библиотекой, а если вы написали функцию, а вызывает эту функцию чужая программа – то это фреймворк.
Для питона существует большое множество веб-фреймворков, на официальной вики питона их перечислено несколько десятков.
Мы расскажем вам о фреймворке django – в первую очередь потому, что для него есть хорошая документация начального уровня и ещё, пожалуй, потому, что он очень популярен (а значит, хорошо поддерживается, в нём меньше дыр и он стабильнее работает). Многие другие фреймворки похожи, но отличаются по мелочам, некоторые чем-то сильно лучше, некоторые чем-то сильно хуже.
MVC
Django построен вокруг идеологии MVC – Model-View-Controller. Это "модный"1 подход к тому, как должны быть устроены программы в целом:
Модель – это часть программы, которая отвечает за основное содержание и собственное видение мира, она же знает о состоянии программы – и, в случае django, подразуменвается, что она связывается с базой данных.
Представление (View) – это часть программы, которая умеет, если её попросить, красиво отобразить часть данных из модели.
Поведение (Controller) – это часть программы, которая следит за тем, что вообще происходит в мире (а именно, что делает пользователь), и по необходимости пинает модель или вид, чтобы они чего-нибудь делали.
Django
В django эта идеология преобразуется следующим образом:
В проекте должен быть модуль urls.py, в котором хранится отображение из адресов запроса в обработчики, которые для этих запросов нужно использовать. Т.е. в нём хранится таблица, в которой записано: если адрес такой-то (регулярное выражение), то рисовать этот адрес нужно такой-то функцией. Этот модуль выполняет в django роль контроллера (буква C в MVC).
В проекте должен быть модуль views.py, в котором описываются функции, обрабатывающие запрос. Как правило, это простые функции, которые используют шаблоны, написанные на надмножестве языка html, которые лежат в соседней директории. Этот модуль символизирует собой представление (букву V в MVC).
В проекте может быть модуль models.py, в котором будут лежать функции работы с базами данных. Сейчас мы не будем рассматривать эти функции вовсе (так как работа с классическими реляционными базами данных – это много мороки и всё равно зачастую можно обойтись без них).
Кроме этого, в проекте будет модуль manage.py – это точка входа в программу. (Django создаст его за нас, и нам не нужно в нём ничего менять, но мы будем его запускать).
И модуль settings.py – настройки проекта – django тоже создаёт за нас начальную версию.
Создание нового проекта django
Как видите, для создания любого, даже самого простого сайта с помощью django, нам нужно создать как минимум пять файлов. Чтобы облегчить эту процедуру, в django есть утилита django-admin: команда django-admin.py startproject site-name создаёт директорию site-name и в ней заготовку для создания сайта.
TCP сервер
Когда мы создали заготовку сайта, мы можем сразу запустить его:
python manage.py runserver 42001
Эта команда обозначает следующее: запустить HTTP-сервер, который слушает входящие соединения на порту 42001 с локальной машины.
Слово "порт" – это понятие из протокола TCP. Проткол HTTP определяет, какие слова нужно послать в какую сторону, чтобы запросить или получить веб-страницу. Среда, в которой шлются эти слова – это протокол TCP.
Протокол TCP определяет две роли: клиент и сервер. Роль клиента состоит в том, чтобы сказать: я хочу присоединиться к серверу по такому-то адресу (или доменному имени) и такому-то порту. Роль сервера состоит в том, чтобы сказать: я готов принимать новые соединения на такой-то порт.
Сервер, как и любой компьютер, считает себя частью интернета. Даже если он не подключён к сети, у него есть виртуальное сетевое устройство: loopback. В этом сетевом устройстве есть машина с одним адресом – 127.0.0.1 (он же localhost, он же localhost.localdomain). По этому адресу любой компьютер видит сам себя. Кроме этого адреса, у компьютера могут быть и какие-нибудь ещё адреса. Поэтому, когда TCP-сервер говорит: "я принимаю соединения на такой-то порт", он ещё при этом сообщает "если соединение было с таким-то адресом".
Например, если мы начнём слушать на нашем компьюетере порт 80 по адресу 127.0.0.1, и к нам придёт запрос на соединение с портом 80 извне, то ОС ответит на этот запрос, что, нет, порт 80 у нас никто не слушает. (Скорее всего это в конце концов будет переведено в слова connection refused).
Порт 80 используется по умолчанию для HTTP-соединений. Когда вы пишете в адресной строке браузера http://kodomo.fbb.msu.ru/wiki, браузер сам догадывается, что соединяться нужно на порт 80. Вы можете сказать ему, чтобы он соединялся на другой порт, например, http://kodomo.fbb.msu.ru:42001/wiki – и если на kodomo есть какой-нибудь сервер, который слушает порт 42001 и умеет отвечать по протоколу HTTP, то вы может быть получите на такой запрос содержательный ответ.
Чтобы TCP-сервер мог слушать соединения на заданный порт откуда угодно, придумали специальный адрес 0.0.0.0 – этот адрес имеет значение "где угодно".
Возвращаясь, потихоньку, к нашей теме, напомню, что HTTP – это описание языка, а средой передачи данных для этого языка является TCP, так что любой HTTP-сервер является автоматически так же и TCP-сервером.
А потребовалось всё это лирическое отступление вот зачем: когда мы говорим python manage.py 42001, django по умолчанию (так как мы указали только порт) начинает слушать соединения на порт 42001 на адрес 127.0.0.1. То есть если вы запустили такой сервер на kodomo, то вы никак не сможете к нему присоединиться.
Мы можем сказать django явным образом, чтобы он слушал соединения на любой адрес: python manage.py 0.0.0.0:42001
После этого мы можем соединиться с этим сервером (в предположении, что он был запущен на kodomo, мы можем открыть в браузере адрес http://kodomo.fbb.msu.ru:42001) – и убедиться, что на любой адрес он отвечает 404 – Not Found.
urls и HttpResponse
Когда вы только сказали django-admin startproject hello2, django создал для ваc файл urls.py примерно такого содержания:
В этом месте мы можем задавать отображения разных адресов страниц в нашем сайте на функции, которые их обрабатывают. Отображения имеют формат (регулярное выражение на адрес, имя представления) (представление – это функция-обработчик, которая рисует страницу по данному адресу). Например, если мы напишем:
То это значит, что для обработки адреса http://kodomo.fbb.msu.ru:42001/index (или адреса http://kodomo.fbb.msu.ru/) будет использоваться функция index из модуля views.py из директории hello (которую нам уже создал django).
Чтобы эти два адреса действительно работали, мы должны создать модуль views.py и создать в нём функцию index. Функция получает на вход параметр request – это объект класса HttpRequest, содержащий всю информацию о запросе (cookies, обозначение браузера пользователя, обратный адрес и т.п.), а возвращает объект типа HttpResponse (точнее, django.http.HttpResponse), в котором содержится вся информация об ответе: заголовки (cookies, даты, параметры кэширования и т.п.) и тело страницы.
Соответственно, самый простой views.py может выглядеть примерно так:
Шаблоны (templates)
Таким образом мы добились от django примерно такого же поведения, как и от простейшего CGI-скрипта.
Но для того, чтобы django было действительно удобно пользоваться, в нём есть несколько полезностей. Первая из них – шаблоны страниц.
Шаблоны описываются на html, в котором добавлено несколько инструкций, говорящих о том, какие данные из питона куда нужно вставить.
Чтобы использовать шаблоны, нужно прописать в settings.py в список TEMPLATE_DIRS полный путь к директории, в которой будут храниться шаблоны. Я использую тот факт, что в переменной file в питоне лежит абсолютный путь к файлу, который в этот момент исполняется, и пишу:
После этого в поддиректории html в нашем проекте можно создавать шаблоны. Например, в html/base.html:
В шаблоне текст {% block имя %} содержимое {% endblock %} задаёт "блок" – часть текста, которую можно на что-нибудь заменить. Содержимое блока задаёт текст, который подставляется в него по умолчанию, если его ни на что не заменять.
Наш шаблон преобразуется в такой HTML:
То есть это фактически просто пустая страница с заголовком.
Заменить блок на другое содержимое можно в другом шаблоне, который "наследуется" (или "расширяет") наш шаблон. Например, создадим html/base-menu.html:
Строка {% extends "base.html" %} (она обязана идти первой в шаблоне, если она есть) говорит о том, что наш шаблон выглядит так же, как и base.html, только некоторые блоки в нём заменены на другие. Далее идёт описание того, что на что менять: мы меняем блок menu на более содержательный.
Это первое применение шаблонов – разделять страницу на несколько частей. (Язык html довольно громоздкий, когда вы хотите сделать в нём что-нибудь содержательное, и возможность нарезать файл на несколько частей уже полезна). Этого же эффекта мы могли бы добиться, если бы использовали блок {% include "файл" %} – при отрисовке шаблона он заменяется на содержимое файла. (Шаблон может расширять только один другой шаблон, но может содержать много include'ов).
Наконец, мы можем сделать страницу с содержанием html/hello.html:
Чтобы всё это отправить на веб, нам нужно привязать рисование этого шаблона к какому-нибудь адресу. Добавим в urls.py:
Выдаются шаблоны функцией django.shortcuts.render_to_response, опишем представление hello, которое использует его (в файле views.py):
Переменные в шаблонах
Однако, я обещал, что шаблоны нужны для того, чтобы использовать в них данные из питона, а пока что мы увидели только пример того, как использовать шаблоны для разделения html страницы на несколько частей (которые можно использовать в разных страницах).
Для этого в шаблонах можно использовать переменные. Текст
{{ variable }} или {{ variable.field }} в шаблоне
заменяется на значение переменной или значение поля из объекта (или результат вызова метода объекта – если поле оказалось функцией). Переменные в шаблоне берутся из контекста. Контекст – это либо словарь, либо объект, который мы явным образом передаём в render_to_response, например, поправим таким образом определение представления base:
Тогда в hello.html мы можем написать:
Или, например, мы можем внести использование этой переменной в общую часть нескольких шаблонов в base.html:
Разбор адресов
Пока что у нас получается, что на каждую страницу, которую мы хотим рисовать, мы должны описывать одну функцию представления. Что довольно скучно, если мы хотим просто использовать шаблон с заданным именем (тем же, что в адресе), или залезть в базу данных и вытащить оттуда тело страницы.
Вспоминаем, что отображение адресов на представления делается у нас регулярными выражениями. В регулярных выражениях есть понятие группы – часть выражения, которая взята в скобки и которая сохраняется в результате разбора выражениях.
Если в urlpatterns в регулярном выражении на адрес есть группы (скобки), то их содержимое передаётся в функцию представления в качестве дополнительных позиционных параметров. Например, мы можем поправить urls.py таким образом:
1 from django.conf.urls.defaults import *
2
3 # Uncomment the next two lines to enable the admin:
4 # from django.contrib import admin
5 # admin.autodiscover()
6
7 urlpatterns = patterns('',
8 (r'^$|^index$', 'hello.views.index'),
9 (r'^base$', 'hello.views.base'),
10 (r'^(.*)', 'hello.views.default'),
11 )
И тогда мы должны описать в views.py функцию представления default с двумя аргументами:
(Так как мы добавили использование переменной {{ time }} в корневой шаблон html/base.html, то мы доложны передавать эту переменную при рисовании всех шаблонов. Если мы переменную не передадим, вместо неё подставится пустая строка, и текст на странице будет выглядеть довольно глупо: "This page is rendered at".)
Теперь, если мы опишем шаблон html/foobar.html:
Запустим наш сервер python manage.py runserver 0.0.0.0:42001 и посмотрим по адресу http://kodomo.fbb.msu.ru:42001/foobar.html, то мы увидим там страницу со стандартной менюшкой, заголовком "Foo Bar" и содержательной частью "There are some foobars around".
Если вы знаете, что такое именованные группы в регулярных выражениях, то django отображает их в именованные параметры функции представления.
Формы
Ещё одна важная возможность динамических страниц – это возможность делать формы.
Например, мы можем описать шаблон html/search.html такого вида:
У нас уже всё настроено, чтобы эта страница отобразилась: на ней будет нарисована строка ввода и кнопка Search – и нажатие на кнопку Search будет отсылать данные формы через строку запроса (методом GET) на страницу /search-results.
Теперь нам нужно определить функцию представления для страницы search-results таким образом, чтобы она могла получить данные формы, которые мы на неё отправили. Чтобы она могла получить данные из формы, в неё и передаётся объект request. Мы можем написать в views.py:
Как нетрудно догадаться по примеру исползования, request.GET – это словарь, в котором ключи – имена полей формы, а значения – соответствующие им значения полей формы. В данном случае мы просто передаём данные формы в шаблон в виде переменной. Нам осталось описать отображение адреса /search-results на эту функцию представления (оставляю в качестве упражнения) и шаблон html/search-results.html:
Дополнительные возможности шаблонов
Кроме простых подстановок, язык шаблонов позволяет делать довольно много чего ещё. В нём есть циклы, условия, небольшой набор встроенных преобразований значений переменных и много прочих удобств.
Покажу два маленьких примера.
Как первый пример, давайте поправим наш последний пример таким образом, чтобы если мы не отправили никакого запроса, страница с результатами запроса выглядела бы так же, как и сам поисковая страница:
Здесь два замаечания.
Во-первых, блок {% if значение %} может принимать на вход константы или имена переменных, но не выражения. Так придумали авторы языка шаблонов.
Во-вторых, мы включаем в данном случае целиком весь search.html, а в нём уже есть всё оформление и меню. Результат получается совсем не такой, как мы хотим. Лучше всего в данном случае было бы вынести в отдельный файл содержимое формы запроса и включать её как часть и в search.html, и в search-results.html. Или, ещё лучше было бы заметить, что при нашем подходе нам уже на самом-то деле уже и не нужен search.html – обычная ссылка на search-results.html даст тот же результат. Поэтому можно было бы от него избавиться и оставить только search-results.html. Оставлю на выбор и упражнение читателю добиться в данном случае правильного вида страниц.
В качестве второго примера, давайте вспомним про наш base-menu.html. До сих пор мы там руками явно перечисляли все ссылки, и, вообще говоря, мы забывали его обновлять, когда добавляли новые страницы. Первое, что мы можем с ним сделать – это добавить в него все новые страницы, какие есть – только на этот раз мы постараемся это сделать не дублируя постоянно один и тот же текст html:
Django позволяет описывать питонского вида циклы и для каждой итерации цикла будет добавлять в выдачу ещё раз содержимое тела цикла. Однако список, по которому проходит цикл, обязан храниться в переменной, которую мы передаём через контекст, поэтому мы идём в views.py и добавляем туда:
И в каждый контекст, который мы передаём на отображение шаблона, мы добавляем context = { ..., 'pages': pages } .
Далее было бы логичным сделать отображение путей в имена шаблонов более однозначным, и тогда мы могли бы вместо явного перечисления всех пунктов меню в views.py смотреть файлы в директории html. Например, для крепких духом и стойких верой, это могло бы выглядеть так3:
1 import glob
2 import os.path
3 from django.template import Context
4 from django.template.loader import get_template
5 from django.template.loader_tags import BlockNode
6
7 def generate_pages():
8 for filepath in glob.glob("html/*"):
9 filename = os.path.basename(filepath)
10 template = get_template(filename)
11 for node in template.nodelist.get_nodes_by_type(BlockNode):
12 if node.name == "title":
13 title = node.render(Context())
14 yield filename, title
15
16 pages = list(generate_pages())
Послесловие
Хотя сам рассказ и был довольно длинным, но на самом деле, мы создали сайт из пяти страниц, выглядящих во многом подобным образом, и на всё про всё нам потребовалось меньше сотни строк в десятке файлов. В чистом html уже такой сайт требовал бы несколько сотен строк – и мы не могли бы сделать в нём динамического содержимого (у нас в этой роли были дата отрисовки страницы и данные из формы).
На самом деле, django позволяет гораздо больше – и мы отправляем вас к документации django.
Литература
Настолько модный, что вы можете увидеть где-нибудь курсы "MVC за месяц". -- Не ходите на такие курсы. (1)
Здесь и далее в примерах я использую hello в качестве названия django-проекта. (2)
У меня ушёл примерно час чтения документации django и копания в его исходниках на то, чтобы понять, как из шаблона вытащить содержимое блока по имени -- и это не выглядит такой уж простой операцией в конце концов. (3)