Журнал LinuxFormat - перейти на главную

LXF83:Python

Материал из Linuxformat
(Различия между версиями)
Перейти к: навигация, поиск
(Базируем данные)
м (Правки RicroAcdom (обсуждение) откачены к версии AmbientLight)
 
(не показаны 16 промежуточных версий 6 участников)
Строка 1: Строка 1:
 +
{{Цикл/Python}}
 +
 
== Работа с базами данных и web-программирование ==
 
== Работа с базами данных и web-программирование ==
'' '''ЧАСТЬ 3''' Что может быть мощнее связки «база данных + интернет»? А если к этому добавить еще и Python... Чтобы почувствовать все это на практике, погрузимся сегодня в пучины SQL-запросов и HTTP-ответов вместе с '''Сергеем Супруновым'''.''
+
'' '''Часть 3''' Что может быть мощнее связки «база данных + интернет»? А если к этому добавить еще и Python... Чтобы почувствовать все это на практике, погрузимся сегодня в пучины SQL-запросов и HTTP-ответов вместе с '''Сергеем Супруновым'''.''
  
 
Мы уже видели, что Python прекрасно подходит для работы с текстом. А что такое интернет-страницы, которые миллионы серверов Apache ежедневно миллиардами отдают на растерзание нашим браузерам? По сути, тот же текст, только немножко «гипер»... А значит, если нам нужно будет формировать html-страницу динамически, то Python прекрасно с этим справится. И никаких препятствий для разработки на нем CGI-сценариев не существует – web-серверу, по большому счету, безразлично, как именно выполняется скрипт и на каком языке он разработан: лишь бы он умел читать данные из потока ввода и переменных окружения да отдавать текст в стандартный выходной поток.
 
Мы уже видели, что Python прекрасно подходит для работы с текстом. А что такое интернет-страницы, которые миллионы серверов Apache ежедневно миллиардами отдают на растерзание нашим браузерам? По сути, тот же текст, только немножко «гипер»... А значит, если нам нужно будет формировать html-страницу динамически, то Python прекрасно с этим справится. И никаких препятствий для разработки на нем CGI-сценариев не существует – web-серверу, по большому счету, безразлично, как именно выполняется скрипт и на каком языке он разработан: лишь бы он умел читать данные из потока ввода и переменных окружения да отдавать текст в стандартный выходной поток.
Строка 18: Строка 20:
 
=== Учимся посылать ===
 
=== Учимся посылать ===
 
Начнем с формирования HTTP-ответа. Чтобы браузер клиента мог его правильно обработать, он должен состоять из заголовка и тела, разделенных пустой строкой. В заголовке передается необходимая служебная информация, например, тип содержимого, его кодировка, указание браузеру запросить другой ресурс (так называемое перенаправление), и т.д. Простейший cgi-сценарий на языке Python может выглядеть так:
 
Начнем с формирования HTTP-ответа. Чтобы браузер клиента мог его правильно обработать, он должен состоять из заголовка и тела, разделенных пустой строкой. В заголовке передается необходимая служебная информация, например, тип содержимого, его кодировка, указание браузеру запросить другой ресурс (так называемое перенаправление), и т.д. Простейший cgi-сценарий на языке Python может выглядеть так:
<pre>
+
 
 +
<source lang="python">
 
#!/usr/bin/Python
 
#!/usr/bin/Python
 
# -*- coding: utf-8 -*-
 
# -*- coding: utf-8 -*-
print ‘Content-Type: text/html\n’
+
print 'Content-Type: text/html\n'
print <H3>Если вы это видите, значит все работает</H3>
+
print '<H3>Если вы это видите, значит все работает</H3>'
</pre>
+
</source>
 +
 
 
Первым оператором print мы формируем минимально необходимый заголовок – браузер клиента обязательно должен знать, каков тип пересылаемых ему данных (в нашем случае это простой текст, соответствующий формату HTML). Не забывайте о дополнительном переводе строки \n, необходимом для отделения заголовка от тела ответа. Ну и далее вы можете передавать любой HTML-код.
 
Первым оператором print мы формируем минимально необходимый заголовок – браузер клиента обязательно должен знать, каков тип пересылаемых ему данных (в нашем случае это простой текст, соответствующий формату HTML). Не забывайте о дополнительном переводе строки \n, необходимом для отделения заголовка от тела ответа. Ну и далее вы можете передавать любой HTML-код.
  
Строка 39: Строка 43:
 
Метод PUT предназначается для размещения ресурсов на сервере и по соображениям безопасности практически не используется. Ну и, наконец, метод HEAD очень похож на GET, за тем исключением, что сервер в ответ на такой запрос возвращает не весь ресурс, а лишь информацию о нем, такую как дата последнего изменения, помещаемую в заголовке. Обычно используется прокси-серверами для определения «свежести» имеющихся у них данных – стоит ли запрашивать ресурс повторно или можно вернуть клиенту то, что есть в кэше.
 
Метод PUT предназначается для размещения ресурсов на сервере и по соображениям безопасности практически не используется. Ну и, наконец, метод HEAD очень похож на GET, за тем исключением, что сервер в ответ на такой запрос возвращает не весь ресурс, а лишь информацию о нем, такую как дата последнего изменения, помещаемую в заголовке. Обычно используется прокси-серверами для определения «свежести» имеющихся у них данных – стоит ли запрашивать ресурс повторно или можно вернуть клиенту то, что есть в кэше.
  
Определенная сложность для разработчика cgi-сценария заключается в том, что данные, отправленные различными методами, передаются в сценарий по-разному. Так, информация, поступившая с помощью POST, подается на стандартный вход сценария и может быть считана оттуда, например, с помощью sys.stdin.read(size) или даже функцией raw_input() (хотя во втором случае сложнее контролировать объем принимаемых данных). Количество байт, которые требуется считать, можно получить из переменной окружения CONTENT_LENGTH (например, так: size = os.environ[‘CONTENT_LENGTH’]).
+
Определенная сложность для разработчика cgi-сценария заключается в том, что данные, отправленные различными методами, передаются в сценарий по-разному. Так, информация, поступившая с помощью POST, подается на стандартный вход сценария и может быть считана оттуда, например, с помощью sys.stdin.read(size) или даже функцией raw_input() (хотя во втором случае сложнее контролировать объем принимаемых данных). Количество байт, которые требуется считать, можно получить из переменной окружения CONTENT_LENGTH (например, так: size = os.environ['CONTENT_LENGTH']).
  
 
Если клиент использует метод GET, то данные поступят в сценарий через переменную среды QUERY_STRING. Метод, которым данные переданы (нужно же как-то разобраться, где их искать) можно всегда получить из REQUEST_METHOD.
 
Если клиент использует метод GET, то данные поступят в сценарий через переменную среды QUERY_STRING. Метод, которым данные переданы (нужно же как-то разобраться, где их искать) можно всегда получить из REQUEST_METHOD.
  
 
Есть еще один особый случай. Если данные передаются методом GET, но с использованием «индексного» формата, который формируется тегом <ISINDEX>, то в этом случае они кодируются не в виде «переменная=значение&переменная=значение&...», а как «значение+значение+...». И cgi-сценарию они будут переданы, помимо QUERY_STRING, через аргументы командной строки, как если бы сценарий вызывался такой командой:
 
Есть еще один особый случай. Если данные передаются методом GET, но с использованием «индексного» формата, который формируется тегом <ISINDEX>, то в этом случае они кодируются не в виде «переменная=значение&переменная=значение&...», а как «значение+значение+...». И cgi-сценарию они будут переданы, помимо QUERY_STRING, через аргументы командной строки, как если бы сценарий вызывался такой командой:
 +
 
  script.cgi arg1 arg2 arg3
 
  script.cgi arg1 arg2 arg3
  
Строка 61: Строка 66:
 
=== Наш спаситель – модуль cgi ===
 
=== Наш спаситель – модуль cgi ===
 
Возвращаемся к обработке всего этого добра, которое сотни пользователей уже готовы обрушить на наш бедный сценарий. Мы решили воспользоваться стандартными средствами Python, и здесь все действительно очень просто – импортируйте модуль cgi и, создав объект класса FieldStorage, вы получите через него доступ ко всем данным, переданным пользователем, независимо от используемого метода:
 
Возвращаемся к обработке всего этого добра, которое сотни пользователей уже готовы обрушить на наш бедный сценарий. Мы решили воспользоваться стандартными средствами Python, и здесь все действительно очень просто – импортируйте модуль cgi и, создав объект класса FieldStorage, вы получите через него доступ ко всем данным, переданным пользователем, независимо от используемого метода:
import cgi
+
 
data = cgi.FieldStorage()
+
<source lang="python">
for entry in data.keys():
+
import cgi
print ‘Переменная %s имеет значение %s’ % (entry, data[entry].value)
+
data = cgi.FieldStorage()
 +
for entry in data.keys():
 +
print 'Переменная %s имеет значение %s' % (entry, data[entry].value)
 +
</source>
  
 
Если вам нужно получить значение определенного поля, это делается так:
 
Если вам нужно получить значение определенного поля, это делается так:
field = data[‘field’].value
+
 
 +
<source lang="python">
 +
field = data['field'].value
 +
</source>
  
 
Помимо пользовательских данных, объект класса FieldStorage содержит информацию и о полях заголовка (в нашем примере их можно получить из словаря data.headers). MIME-тип данных (передаваемый полем заголовка Content-Type) можно получить из атрибута data.type. Через этот же объект может быть выполнена и загрузка файла.
 
Помимо пользовательских данных, объект класса FieldStorage содержит информацию и о полях заголовка (в нашем примере их можно получить из словаря data.headers). MIME-тип данных (передаваемый полем заголовка Content-Type) можно получить из атрибута data.type. Через этот же объект может быть выполнена и загрузка файла.
Строка 79: Строка 90:
  
 
Для этого примера я выбрал в качестве «ответственного» за хранение данных сервер баз данных PostgreSQL. Поскольку мы пишем ну очень простую гостевую книгу, то и структура базы будет у нас элементарной – одна таблица с тремя полями: время публикации сообщения, имя автора и, собственно, само сообщение:
 
Для этого примера я выбрал в качестве «ответственного» за хранение данных сервер баз данных PostgreSQL. Поскольку мы пишем ну очень простую гостевую книгу, то и структура базы будет у нас элементарной – одна таблица с тремя полями: время публикации сообщения, имя автора и, собственно, само сообщение:
<pre>
+
 
 
  admin@toshiba:~$ psql
 
  admin@toshiba:~$ psql
 
  Welcome to psql 8.1.4, the PostgreSQL interactive terminal.
 
  Welcome to psql 8.1.4, the PostgreSQL interactive terminal.
 
+
  guestbook=# create user "www-data" nocreatedb nocreateuser;
  guestbook=# create user “www-data” nocreatedb nocreateuser;
+
 
  CREATE ROLE
 
  CREATE ROLE
  admin=# create database guestbook with owner “www-data”;
+
  admin=# create database guestbook with owner "www-data";
 
  CREATE DATABASE
 
  CREATE DATABASE
 
  admin=# \connect guestbook
 
  admin=# \connect guestbook
  Вы подсоединились к базе данных “guestbook”.
+
  Вы подсоединились к базе данных "guestbook".
 
  guestbook=# create table guestbook (
 
  guestbook=# create table guestbook (
 
  guestbook(# datum timestamp, author varchar, message varchar);
 
  guestbook(# datum timestamp, author varchar, message varchar);
 
  CREATE TABLE
 
  CREATE TABLE
  guestbook=# alter table guestbook owner to “www-data”;
+
  guestbook=# alter table guestbook owner to "www-data";
 
  ALTER TABLE
 
  ALTER TABLE
 
  guestbook=# \q
 
  guestbook=# \q
 
  admin@toshiba:~$
 
  admin@toshiba:~$
</pre>
 
  
 
Пожалуй, единственное, что здесь нужно пояснить, это почему базе и таблице мы назначили владельцем пользователя www-data. Просто к ним будет обращаться cgi-сценарий, работающий с правами HTTP-сервера Apache, который, в свою очередь, исполняется от имени данного пользователя [в вашем дистрибутиве он может назваться по-другому, – прим. ред.]. А PostgreSQL по умолчанию требует, чтобы имя пользователя в БД совпадало с его системным именем. Мне это кажется достаточно удобным, хотя вы, конечно, можете поступить по-своему.
 
Пожалуй, единственное, что здесь нужно пояснить, это почему базе и таблице мы назначили владельцем пользователя www-data. Просто к ним будет обращаться cgi-сценарий, работающий с правами HTTP-сервера Apache, который, в свою очередь, исполняется от имени данного пользователя [в вашем дистрибутиве он может назваться по-другому, – прим. ред.]. А PostgreSQL по умолчанию требует, чтобы имя пользователя в БД совпадало с его системным именем. Мне это кажется достаточно удобным, хотя вы, конечно, можете поступить по-своему.
  
 
=== DB API на страже унификации ===
 
=== DB API на страже унификации ===
Осталось разобраться, как же Python взаимодействует с базами дан-
+
Осталось разобраться, как же Python взаимодействует с базами данных. Для этого Python предоставляет DB API – специальный интерфейс, унифицирующий набор методов, которые будут одинаково работать независимо от того, с какой СУБД мы взаимодействуем. Для работы с PostgreSQL нам понадобится модуль PyPgSQL (в стандартной поставке его может не оказаться, но ваш менеджер пакетов наверняка будет в курсе, как его установить; кстати, это не единственный модуль – у вас, возможно, будет PyGreSQL, который работает ничуть ни хуже и с теми же самыми методами).
ных. Для этого Python предоставляет DB API – специальный интерфейс,
+
 
унифицирующий набор методов, которые будут одинаково работать
+
DB API определяет стандартные методы работы с базами данных, так что, какой бы модуль вы ни загрузили и с какой бы СУБД ни работали (будь то MySQL, PostgreSQL, SQLite или что-то еще), меняться будет только имя модуля. Главное, чтобы используемый модуль соответствовал DB API. Рассмотрим коротко основные методы:
независимо от того, с какой СУБД мы взаимодействуем. Для работы с
+
 
PostgreSQL нам понадобится модуль PyPgSQL (в стандартной поставке
+
<source lang="python">
его может не оказаться, но ваш менеджер пакетов наверняка будет в
+
conn = connect(dsn='localhost', user='admin', password='superparol', database='mydb')
курсе, как его установить; кстати, это не единственный модуль – у вас,
+
</source>
возможно, будет PyGreSQL, который работает ничуть ни хуже и с теми
+
 
же самыми методами).
+
Так осуществляется подключение к базе. В зависимости от ситуации, вам может потребоваться указать только нужные параметры (например, имя хоста 'localhost' подразумевается по умолчанию).
DB API определяет стандартные методы работы с базами данных,
+
 
так что, какой бы модуль вы ни загрузили и с какой бы СУБД ни работа-
+
<source lang="python">
ли (будь то MySQL, PostgreSQL, SQLite или что-то еще), меняться будет
+
только имя модуля. Главное, чтобы используемый модуль соответство-
+
вал DB API. Рассмотрим коротко основные методы:
+
conn = connect(dsn=’localhost’, user=’admin’, password=’superparol’,
+
database=’mydb’)
+
Так осуществляется подключение к базе. В зависимости от ситуации,
+
вам может потребоваться указать только нужные параметры (например,
+
имя хоста ‘localhost’ подразумевается по умолчанию).
+
 
cur = conn.cursor()
 
cur = conn.cursor()
Курсоры поддерживаются далеко не всеми СУБД, но для общности
+
</source>
в DB API они введены и, в случае необходимости, должны эмулировать-
+
 
ся модулями сопряжения искусственно. Так что не забывайте отправлять
+
Курсоры поддерживаются далеко не всеми СУБД, но для общности в DB API они введены и, в случае необходимости, должны эмулироваться модулями сопряжения искусственно. Так что не забывайте отправлять все ваши запросы через курсор.
все ваши запросы через курсор.
+
 
cur.execute(‘’’SELECT * FROM mytable’’’)
+
<source lang="python">
Так выполняется SQL-запрос. Если в строке запроса используются
+
cur.execute('''SELECT * FROM mytable''')
знакоместа %s, то вторым параметром передается список переменных-
+
</source>
значений, причем в SQL-запросе знакоместа не требуется окружать
+
 
апострофами – модуль сделает это самостоятельно в зависимости от
+
Так выполняется SQL-запрос. Если в строке запроса используются знакоместа %s, то вторым параметром передается список переменных-значений, причем в SQL-запросе знакоместа не требуется окружать апострофами – модуль сделает это самостоятельно в зависимости от
 
типа переменной.
 
типа переменной.
 +
 +
<source lang="python">
 
cur.fetchall()
 
cur.fetchall()
Возвращает двумерный список (строки – поля) полученных от СУБД
+
</source>
данных. Существуют и другие методы, ознакомиться с которыми вы
+
 
сможете в документации или с помощью знакомой вам функции dir()
+
Возвращает двумерный список (строки – поля) полученных от СУБД данных. Существуют и другие методы, ознакомиться с которыми вы сможете в документации или с помощью знакомой вам функции dir() да пары-тройки несложных экспериментов.
да пары-тройки несложных экспериментов.
+
  
 
=== Закрепляем на практике ===
 
=== Закрепляем на практике ===
Перейдем к рассмотрению нашего примера. Начнем стандартно – ука-
+
Перейдем к рассмотрению нашего примера. Начнем стандартно – укажем кодировку, подключим нужные модули:
жем кодировку, подключим нужные модули:
+
 
 +
<source lang="python">
 
#!/usr/bin/Python
 
#!/usr/bin/Python
 
# -*- coding: utf-8 -*-
 
# -*- coding: utf-8 -*-
 
import PyPgSQL.PgSQL as pg
 
import PyPgSQL.PgSQL as pg
 
import cgi
 
import cgi
Далее, определим две функции. Первая будет отвечать за добавле-
+
</source>
ние нового сообщения в базу:
+
 
 +
Далее, определим две функции. Первая будет отвечать за добавление нового сообщения в базу:
 +
 
 +
<source lang="python">
 
def addMessage(author, message):
 
def addMessage(author, message):
db = pg.connect(database=”guestbook”)
+
  db = pg.connect(database="guestbook")
c = db.cursor()
+
  c = db.cursor()
c.execute(“””INSERT INTO guestbook (datum, author, message)
+
  c.execute("""INSERT INTO guestbook (datum, author, message) VALUES ('now', %s, %s);""", (author, message))
VALUES (‘now’, %s, %s);”””, (author, message))
+
  c.close()
c.close()
+
  db.commit()
db.commit()
+
  db.close()
db.close()
+
  print "Content-Type: text/html"
print “Content-Type: text/html”
+
  print "Location: ?#form\n"
print “Location: ?#form\n”
+
</source>
Как видите, все очень даже логично: устанавливаем соединение
+
 
с БД (поскольку в нашем случае подключение выполняется с именем
+
Как видите, все очень даже логично: устанавливаем соединение с БД (поскольку в нашем случае подключение выполняется с именем текущего системного пользователя, то достаточно указать только имя базы), создаем курсор (в PostgreSQL они не применяются, но они эмулируются каждым модулем, претендующим на соответствие DB API), выполняется запрос, закрывается курсор, фиксируются изменения (PostgreSQL использует транзакции, поэтому выполнение метода commit() обязательно, иначе ваши изменения не будут сохранены), и, наконец, закрываем само соединение с базой. В поле datum заносим значение встроенной переменной PostgreSQL – now, которая каждый раз заменяется текущим значением даты и времени.
текущего системного пользователя, то достаточно указать только имя
+
 
базы), создаем курсор (в PostgreSQL они не применяются, но они
+
Ну и печать заголовка «Location» выполняется для того, чтобы перенаправить пользователя на этот же сценарий, но уже без параметров – мы же должны показать клиенту, что он на самом деле ввел? (Якорь #form используется, чтобы автоматически прокрутить страничку на последнее сообщение).
эмулируются каждым модулем, претендующим на соответствие DB
+
 
API), выполняется запрос, закрывается курсор, фиксируются измене-
+
Вторая функция будет отвечать за вывод на экран уже оставленныхв книге записей, а также за форму, с помощью которой можно будет добавить и свое высказывание:
ния (PostgreSQL использует транзакции, поэтому выполнение метода
+
 
commit() обязательно, иначе ваши изменения не будут сохранены), и,
+
<source lang="python">
наконец, закрываем само соединение с базой. В поле datum заносим
+
значение встроенной переменной PostgreSQL – now, которая каждый
+
раз заменяется текущим значением даты и времени.
+
Ну и печать заголовка «Location» выполняется для того, чтобы
+
перенаправить пользователя на этот же сценарий, но уже без пара-
+
метров – мы же должны показать клиенту, что он на самом деле ввел?
+
(Якорь #form используется, чтобы автоматически прокрутить страничку
+
на последнее сообщение).
+
Вторая функция будет отвечать за вывод на экран уже оставленных
+
в книге записей, а также за форму, с помощью которой можно будет
+
добавить и свое высказывание:
+
 
def showGB():
 
def showGB():
db = pg.connect(database=”guestbook”)
+
  db = pg.connect(database="guestbook")
c = db.cursor()
+
  c = db.cursor()
c.execute(“””SELECT datum, author, message
+
  c.execute("""SELECT datum, author, message FROM guestbook ORDER BY datum;""")
FROM guestbook ORDER BY datum;”””)
+
  res = c.fetchall()
res = c.fetchall()
+
  c.close()
c.close()
+
 
db.close()
 
db.close()
В этом фрагменте мы выбираем все строки из нашей таблицы дан-
+
</source>
ных, сортируя их по дате. Результат сохраняется в переменной res, с
+
 
которой и будем работать. Теперь осталось лишь аккуратненько разло-
+
В этом фрагменте мы выбираем все строки из нашей таблицы данных, сортируя их по дате. Результат сохраняется в переменной res, с которой и будем работать. Теперь осталось лишь аккуратненько разложить наши данные по табличкам и вывести их на экран:
жить наши данные по табличкам и вывести их на экран:
+
 
print “Content-Type: text/html\n”
+
<source lang="python">
print <H1 style=’color:#7777FF’><U>Велькам к нам в гости!</U></
+
print "Content-Type: text/html\n"
H1>
+
print "<H1 style='color:#7777FF'><U>Велькам к нам в гости!</U></H1>"
for item in res:
+
for item in res:
print “””<TABLE width=’90%%>
+
  print """<TABLE width='90%%'>
<TR><TD><SMALL>Товарищ <B>%s</B> поведал
+
                <TR><TD><SMALL>Товарищ <B>%s</B> поведалнам следующее:</SMALL>
нам следующее:</SMALL>
+
                    <TD align='right'><SMALL>%s</SMALL>
<TD align=’right’><SMALL>%s</SMALL>
+
                <TR><TD style='background-color:#DDDDFF' colspan='2'>%s
<TR><TD style=’background-color:#DDDDFF’ colspan=’2’>%s
+
            </TABLE>""" % (item[1], str(item[0])[:19], item[2])
</TABLE>””” % (item[1], str(item[0])[:19], item[2])
+
print "<HR><A name='form'><H3>Присоединяйтесь к дискуссии:</H3>"
print <HR><A name=’form’><H3>Присоединяйтесь к дискуссии:</
+
print """<FORM method='GET'>
H3>
+
          Ваше имя: <INPUT type='text' name='author'><BR>
print “””<FORM method=’GET’>
+
          Что вы думаете по этому поводу:<BR>
Ваше имя: <INPUT type=’text’ name=’author’><BR>
+
          <TEXTAREA name='message' rows='5' cols='80'></TEXTAREA><BR>
Что вы думаете по этому поводу:<BR>
+
          <INPUT type='submit' value='Отправить'>
<TEXTAREA name=’message’ rows=’5’ cols=’80’></
+
          </FORM>"""
TEXTAREA><BR>
+
</source>
<INPUT type=’submit’ value=’Отправить’>
+
 
</FORM>”””
+
<div id="img"></div>
Смысл конструкции str(item[0])[:19] заключается в том, что-
+
[[Изображение:Img 83 81 1.png|thumb|Ни смайликов, ни BB-кода, ни даже логотипа... Зато мы сделали эту гостевую за 10 минут!]]
бы в строке времени отсечь ненужные нам миллисекунды, которые
+
 
сохраняются в поле типа timestamp. После всех опубликованных
+
Смысл конструкции str(item[0])[:19] заключается в том, чтобы в строке времени отсечь ненужные нам миллисекунды, которые сохраняются в поле типа timestamp. После всех опубликованных сообщений выводим форму добавления нового, чтобы каждый мог присоединиться к нашей дискуссии. Кстати, в теге <FORM> мы не указали параметр action, поскольку данные будут передаваться на обработку этому же сценарию (благодаря чему имя сценарию можно присвоить любое).
сообщений выводим форму добавления нового, чтобы каждый мог
+
присоединиться к нашей дискуссии. Кстати, в теге <FORM> мы не
+
указали параметр action, поскольку данные будут передаваться на
+
обработку этому же сценарию (благодаря чему имя сценарию можно
+
присвоить любое).
+
 
Наконец, последний фрагмент:
 
Наконец, последний фрагмент:
 +
 +
<source lang="python">
 
form = cgi.FieldStorage()
 
form = cgi.FieldStorage()
if form.has_key(“message”) and form.has_key(“author”):
+
if form.has_key("message") and form.has_key("author"):
author = cgi.escape(form[“author”].value)
+
  author = cgi.escape(form["author"].value)
message = cgi.escape(form[“message”].value)
+
  message = cgi.escape(form["message"].value)
message = message.replace(\n”, <BR>)
+
  message = message.replace("\n", "<BR>")
addMessage(author, message)
+
  addMessage(author, message)
 
else:
 
else:
showGB()
+
  showGB()
Создаем FieldStorage-объект, и если в нем есть заполненные
+
</source>
поля message и author (то есть запрос был сформирован из запол-
+
 
ненной пользователем формы), то, немножко их обработав (функция
+
Создаем FieldStorage-объект, и если в нем есть заполненные поля message и author (то есть запрос был сформирован из заполненной пользователем формы), то, немножко их обработав (функция cgi.escape() заменяет все «неблагонадежные» символы – например, < – их стандартными SGML-сущностями, в данном случае – &lt;), передаем функции addMessage(). Обработка нужна для того, чтобы злоумышленник не мог ввести в поле сообщения или имени автора что-нибудь такое:
cgi.escape() заменяет все «неблагонадежные» символы – напри-
+
 
мер, < – их стандартными SGML-сущностями, в данном случае – &lt;),
+
<SCRIPT>alert('Да пошли вы все!');</SCRIPT>
передаем функции addMessage(). Обработка нужна для того, чтобы
+
 
злоумышленник не мог ввести в поле сообщения или имени автора
+
К слову, пренебрегать проверкой введенных данных ни в коем случае нельзя. Зайдите как-нибудь на [http://securitylab.ru securitylab.ru] и посмотрите, сколько уязвимостей типа «XSS» обнаруживается каждый месяц! Так что шутки шутками, но последствия могут быть очень серьезными.
что-нибудь такое:
+
<SCRIPT>alert(‘Да пошли вы все!);</SCRIPT>
+
К слову, пренебрегать проверкой введенных данных ни в коем слу-
+
чае нельзя. Зайдите как-нибудь на securitylab.ru и посмотрите, сколько
+
уязвимостей типа «XSS» обнаруживается каждый месяц! Так что шутки
+
шутками, но последствия могут быть очень серьезными.
+
  
 
=== Куда же нам теперь идти? ===
 
=== Куда же нам теперь идти? ===
Итак, что-то вполне работоспособное у нас есть (см. рисунок). Но как вы
+
Итак, что-то вполне работоспособное у нас есть (см. [[LXF83:Python#img|рисунок]]). Но как вы может догадаться, наша гостевая очень далека от совершенства. Что еще можно сделать? Ну, например, разбить на страницы. Пока сообщений в ней будет не больше дюжины, сойдет и так. А когда их число дойдет до сотни, то редкий пользователь дождется окончания загрузки всех данных. Можно дать пользователям возможность использовать некоторые HTML-теги, чтобы их сообщения выглядели более красочно. Можно добавить смайликов... А можно даже сделать модуль администрирования, позволяющий редактировать или удалять сообщения, а также отвечать на них. Так что работы непочатый край. Дерзайте – не буду вам мешать.
может догадаться, наша гостевая очень далека от совершенства. Что
+
 
еще можно сделать? Ну, например, разбить на страницы. Пока сооб-
+
=== Некоторые распространённые MIME-типы ===
щений в ней будет не больше дюжины, сойдет и так. А когда их число
+
{| style="background:white;color:black;" border="1" cellspacing="0"
дойдет до сотни, то редкий пользователь дождется окончания загрузки
+
|- style="background:#dfcfe6;color:black"
всех данных. Можно дать пользователям возможность использовать
+
! MIME-тип
некоторые HTML-теги, чтобы их сообщения выглядели более красочно.
+
! Описание
Можно добавить смайликов... А можно даже сделать модуль админис-
+
|-
трирования, позволяющий редактировать или удалять сообщения, а
+
| text/plain
также отвечать на них. Так что работы непочатый край. Дерзайте – не
+
| Простой текст
буду вам мешать. LXF
+
|-
 +
| text/html
 +
| HTML-страница
 +
|-
 +
| image/gif
 +
| Изображение GIF
 +
|-
 +
| video/mpeg
 +
| Видео-файл в формате MPEG
 +
|-
 +
| application/msword     
 +
| Документ MS Word
 +
|}

Текущая версия на 14:27, 31 мая 2009

Содержание

[править] Работа с базами данных и web-программирование

Часть 3 Что может быть мощнее связки «база данных + интернет»? А если к этому добавить еще и Python... Чтобы почувствовать все это на практике, погрузимся сегодня в пучины SQL-запросов и HTTP-ответов вместе с Сергеем Супруновым.

Мы уже видели, что Python прекрасно подходит для работы с текстом. А что такое интернет-страницы, которые миллионы серверов Apache ежедневно миллиардами отдают на растерзание нашим браузерам? По сути, тот же текст, только немножко «гипер»... А значит, если нам нужно будет формировать html-страницу динамически, то Python прекрасно с этим справится. И никаких препятствий для разработки на нем CGI-сценариев не существует – web-серверу, по большому счету, безразлично, как именно выполняется скрипт и на каком языке он разработан: лишь бы он умел читать данные из потока ввода и переменных окружения да отдавать текст в стандартный выходной поток.

Впрочем, если вы жаждете скорости, то к вашим услугам mod_Python, да и в режиме FastCGI Python работать умеет. Но сейчас у нас разговор все же не о настройках CGI, а о Python, так что вернемся к тому, ради чего мы эту статью начали.

[править] Постановка задачи

С любым вопросом лучше всего разбираться на практическом примере. Поэтому мы будем «плясать» вокруг несложного и, в общем-то, достаточно банального CGI-приложения: гостевой книги. Заодно разберемся с тем, как Python взаимодействует с базами данных, где представляется разумным хранить все наши сообщения.

Но прежде чем перейти к рассмотрению кода (вы найдете его целиком на нашем диске), полезно будет дать кое-какую вводную информацию.

[править] Универсальное «междумордье» CGI

CGI (Common Gateway Interface, общий шлюзовой интерфейс) был разработан как средство взаимодействия HTTP-сервера с программами, которые могут запускаться в операционной системе. Если говорить упрощенно, то CGI, передавая управление такой программе (обычно их именуют cgi-сценариями, хотя это вполне может быть и двоичный файл, разработанный на C/C++), формирует для нее определенное окружение. В частности, параметры HTTP-запроса, полученного от клиента, могут помещаться в определенные переменные окружения или передаваться cgi-программе как аргументы или как входной поток (STDIN). В ответ HTTP-сервер ждет данные, которые cgi-программа должна выдать в стандартный выходной поток (STDOUT), и передает их клиенту.

Таким образом, все, что требуется от cgi-программы, это способность получать необходимую для работы информацию из формируемой HTTP-сервером среды и возвращать ответные данные, соответствующие протоколу HTTP, чтобы web-клиент знал, что с ними делать.

[править] Учимся посылать

Начнем с формирования HTTP-ответа. Чтобы браузер клиента мог его правильно обработать, он должен состоять из заголовка и тела, разделенных пустой строкой. В заголовке передается необходимая служебная информация, например, тип содержимого, его кодировка, указание браузеру запросить другой ресурс (так называемое перенаправление), и т.д. Простейший cgi-сценарий на языке Python может выглядеть так:

#!/usr/bin/Python
# -*- coding: utf-8 -*-
print 'Content-Type: text/html\n'
print '<H3>Если вы это видите, значит все работает</H3>'

Первым оператором print мы формируем минимально необходимый заголовок – браузер клиента обязательно должен знать, каков тип пересылаемых ему данных (в нашем случае это простой текст, соответствующий формату HTML). Не забывайте о дополнительном переводе строки \n, необходимом для отделения заголовка от тела ответа. Ну и далее вы можете передавать любой HTML-код.

Аналогично могут передаваться любые объекты, поддерживаемые клиентом: изображения, звуковые файлы, css-таблицы и т.д. Главное, чтобы значение поля Content-Type (именуемое также MIME-типом) соответствовало содержимому.

[править] Здесь играть, здесь не играть...

Однако какой смысл поручать формирование статических, по сути, страниц cgi-сценарию, если сам HTTP-сервер справится с этим намного лучше? В общем-то никакого. Разве что для общего развития... А вот в чем CGI по-настоящему силен, так это в формировании динамических страниц, содержимое которых зависит от информации, переданной пользователем.

Протокол HTTP предусматривает несколько способов передачи информации от клиента на сервер, называемых методами. Наиболее популярные из них – GET, POST, PUT и HEAD.

Метод GET позволяет вставлять информацию в URL, то есть в строку адреса запрашиваемого ресурса. Когда «Яндекс» вернет вам список искомых страниц, посмотрите на адресную строку в браузере – вот так данные и передаются методом GET. Кстати, обратите внимание на то, как все это кодируется, особенно если вы искали какое-то русское слово.

Если на сервер требуется передать больший объем информации, или ее желательно скрыть от любопытных глаз, используется другой метод – POST. В данном случае в заголовке передается лишь размер пользовательских данных, а сами данные пересылаются в теле запроса.

Метод PUT предназначается для размещения ресурсов на сервере и по соображениям безопасности практически не используется. Ну и, наконец, метод HEAD очень похож на GET, за тем исключением, что сервер в ответ на такой запрос возвращает не весь ресурс, а лишь информацию о нем, такую как дата последнего изменения, помещаемую в заголовке. Обычно используется прокси-серверами для определения «свежести» имеющихся у них данных – стоит ли запрашивать ресурс повторно или можно вернуть клиенту то, что есть в кэше.

Определенная сложность для разработчика cgi-сценария заключается в том, что данные, отправленные различными методами, передаются в сценарий по-разному. Так, информация, поступившая с помощью POST, подается на стандартный вход сценария и может быть считана оттуда, например, с помощью sys.stdin.read(size) или даже функцией raw_input() (хотя во втором случае сложнее контролировать объем принимаемых данных). Количество байт, которые требуется считать, можно получить из переменной окружения CONTENT_LENGTH (например, так: size = os.environ['CONTENT_LENGTH']).

Если клиент использует метод GET, то данные поступят в сценарий через переменную среды QUERY_STRING. Метод, которым данные переданы (нужно же как-то разобраться, где их искать) можно всегда получить из REQUEST_METHOD.

Есть еще один особый случай. Если данные передаются методом GET, но с использованием «индексного» формата, который формируется тегом <ISINDEX>, то в этом случае они кодируются не в виде «переменная=значение&переменная=значение&...», а как «значение+значение+...». И cgi-сценарию они будут переданы, помимо QUERY_STRING, через аргументы командной строки, как если бы сценарий вызывался такой командой:

script.cgi arg1 arg2 arg3

То есть, на этот раз пользовательские данные можно будет получить как sys.argv[1] и т.д.

Как видите, огромное число вариантов, предусмотренных CGI-интерфейсом, которые все должны быть учтены при разработке сценария, может вызвать нервный тик даже у опытных программистов, которые и во сне потихоньку набивают по подушке какой-то код. А если еще вспомнить, что данные передаются в закодированном виде (это англичанам хорошо – взял значение переменной и работай, а нам-то с вами это значение вернется в виде %EC%E4%E0), да еще и о проверке этих данных нужно позаботиться, чтобы какой-нибудь начинающий хакер не попытался заставить наш сервер работать по-своему... Нет, обо всем этом лучше и не вспоминать. Благо у нас есть модуль cgi, в котором все это уже сделано!

Но о нем – чуть позже. Сначала пару слов нужно сказать о HTML-формах.

[править] Формируем формы

Чтобы вам было проще понять рассматриваемый сегодня пример, коротко скажу про то, как же клиент выполняет передачу данных нашему cgi-сценарию. Конечно, продвинутые пользователи могут набрать GET-запрос вручную в адресной строке браузера. Хотя что мелочиться – ведь можно же сформировать и POST-запрос, подключившись телнетом на 80-й порт! Впрочем, обычные пользователи предпочитают более понятные и «осязаемые» способы, например, формы.

Как они выглядят, думаю, каждый знает. Создаются они с помощью тега <FORM>, внутри которого добавляются такие элементы, как <INPUT> (поле ввода) или <TEXTAREA> (многострочный редактор). Этим элементам, если их данные должны быть переданы на сервер, присваиваются имена с помощью атрибута name. Начальное значение задается параметром value и в дальнейшем для «редактируемых» полей может быть изменено пользователем. Когда пользователь нажимает кнопку «Отправить» (надпись на ней, в принципе, можно изменить), то браузер объединяет все данные полей в пары name=value, разделяя их символом &. Затем полученная таким образом строка передается на сервер методом, указанным в атрибуте method тега <FORM>. Путь к сценарию, который будет заниматься ее обработкой, задается атрибутом action этого же тега. Если action не задан, то данные передаются файлу, сформировавшему текущую страничку.

Если что-то не совсем понятно, обратитесь к коду разрабатываемой гостевой книги, который приведен ниже.

[править] Наш спаситель – модуль cgi

Возвращаемся к обработке всего этого добра, которое сотни пользователей уже готовы обрушить на наш бедный сценарий. Мы решили воспользоваться стандартными средствами Python, и здесь все действительно очень просто – импортируйте модуль cgi и, создав объект класса FieldStorage, вы получите через него доступ ко всем данным, переданным пользователем, независимо от используемого метода:

import cgi
data = cgi.FieldStorage()
for entry in data.keys():
print 'Переменная %s имеет значение %s' % (entry, data[entry].value)

Если вам нужно получить значение определенного поля, это делается так:

field = data['field'].value

Помимо пользовательских данных, объект класса FieldStorage содержит информацию и о полях заголовка (в нашем примере их можно получить из словаря data.headers). MIME-тип данных (передаваемый полем заголовка Content-Type) можно получить из атрибута data.type. Через этот же объект может быть выполнена и загрузка файла.

С помощью методов keys() и has_key() можно выполнять обработку полученных данных в цикле и проверять наличие той или иной переменной. Кстати говоря, проверять наличие переменной во входных данных, прежде чем приступать к их обработке, нужно непременно – ведь запрос формируется клиентом, а кто знает, что у него на уме?

[править] Базируем данные

Итак, получать данные от клиента мы научились. Отправлять тоже умеем. Осталось придумать, как эти данные лучше всего хранить. Конечно, для несложной гостевой книги с небольшой нагрузкой вполне хватило бы и текстовых файлов. Правда, там есть свои сложности – если сразу пять человек захотят высказать свое мнение о вашей крутейшей домашней страничке, то сценарию придется каким-то образом регулировать доступ к файлу-хранилищу (как минимум, обрабатывать ситуацию, если файл уже открыт на запись другим экземпляром сценария). Но зачем нам все эти головные боли? Если мы так ловко отвертелись от необходимости вручную разбирать HTTP-запросы, то неужели не найдем что-то подходящее на этот раз?

Конечно, найдем! И это «что-то» называется системой управления базами данных (в просторечье – СУБД). Теперь наше дело – отправить запрос и получить ответ. Все остальное – уже не наша забота.

Для этого примера я выбрал в качестве «ответственного» за хранение данных сервер баз данных PostgreSQL. Поскольку мы пишем ну очень простую гостевую книгу, то и структура базы будет у нас элементарной – одна таблица с тремя полями: время публикации сообщения, имя автора и, собственно, само сообщение:

admin@toshiba:~$ psql
Welcome to psql 8.1.4, the PostgreSQL interactive terminal.
guestbook=# create user "www-data" nocreatedb nocreateuser;
CREATE ROLE
admin=# create database guestbook with owner "www-data";
CREATE DATABASE
admin=# \connect guestbook
Вы подсоединились к базе данных "guestbook".
guestbook=# create table guestbook (
guestbook(# datum timestamp, author varchar, message varchar);
CREATE TABLE
guestbook=# alter table guestbook owner to "www-data";
ALTER TABLE
guestbook=# \q
admin@toshiba:~$

Пожалуй, единственное, что здесь нужно пояснить, это почему базе и таблице мы назначили владельцем пользователя www-data. Просто к ним будет обращаться cgi-сценарий, работающий с правами HTTP-сервера Apache, который, в свою очередь, исполняется от имени данного пользователя [в вашем дистрибутиве он может назваться по-другому, – прим. ред.]. А PostgreSQL по умолчанию требует, чтобы имя пользователя в БД совпадало с его системным именем. Мне это кажется достаточно удобным, хотя вы, конечно, можете поступить по-своему.

[править] DB API на страже унификации

Осталось разобраться, как же Python взаимодействует с базами данных. Для этого Python предоставляет DB API – специальный интерфейс, унифицирующий набор методов, которые будут одинаково работать независимо от того, с какой СУБД мы взаимодействуем. Для работы с PostgreSQL нам понадобится модуль PyPgSQL (в стандартной поставке его может не оказаться, но ваш менеджер пакетов наверняка будет в курсе, как его установить; кстати, это не единственный модуль – у вас, возможно, будет PyGreSQL, который работает ничуть ни хуже и с теми же самыми методами).

DB API определяет стандартные методы работы с базами данных, так что, какой бы модуль вы ни загрузили и с какой бы СУБД ни работали (будь то MySQL, PostgreSQL, SQLite или что-то еще), меняться будет только имя модуля. Главное, чтобы используемый модуль соответствовал DB API. Рассмотрим коротко основные методы:

conn = connect(dsn='localhost', user='admin', password='superparol', database='mydb')

Так осуществляется подключение к базе. В зависимости от ситуации, вам может потребоваться указать только нужные параметры (например, имя хоста 'localhost' подразумевается по умолчанию).

cur = conn.cursor()

Курсоры поддерживаются далеко не всеми СУБД, но для общности в DB API они введены и, в случае необходимости, должны эмулироваться модулями сопряжения искусственно. Так что не забывайте отправлять все ваши запросы через курсор.

cur.execute('''SELECT * FROM mytable''')

Так выполняется SQL-запрос. Если в строке запроса используются знакоместа %s, то вторым параметром передается список переменных-значений, причем в SQL-запросе знакоместа не требуется окружать апострофами – модуль сделает это самостоятельно в зависимости от типа переменной.

cur.fetchall()

Возвращает двумерный список (строки – поля) полученных от СУБД данных. Существуют и другие методы, ознакомиться с которыми вы сможете в документации или с помощью знакомой вам функции dir() да пары-тройки несложных экспериментов.

[править] Закрепляем на практике

Перейдем к рассмотрению нашего примера. Начнем стандартно – укажем кодировку, подключим нужные модули:

#!/usr/bin/Python
# -*- coding: utf-8 -*-
import PyPgSQL.PgSQL as pg
import cgi

Далее, определим две функции. Первая будет отвечать за добавление нового сообщения в базу:

def addMessage(author, message):
  db = pg.connect(database="guestbook")
  c = db.cursor()
  c.execute("""INSERT INTO guestbook (datum, author, message) VALUES ('now', %s, %s);""", (author, message))
  c.close()
  db.commit()
  db.close()
  print "Content-Type: text/html"
  print "Location: ?#form\n"

Как видите, все очень даже логично: устанавливаем соединение с БД (поскольку в нашем случае подключение выполняется с именем текущего системного пользователя, то достаточно указать только имя базы), создаем курсор (в PostgreSQL они не применяются, но они эмулируются каждым модулем, претендующим на соответствие DB API), выполняется запрос, закрывается курсор, фиксируются изменения (PostgreSQL использует транзакции, поэтому выполнение метода commit() обязательно, иначе ваши изменения не будут сохранены), и, наконец, закрываем само соединение с базой. В поле datum заносим значение встроенной переменной PostgreSQL – now, которая каждый раз заменяется текущим значением даты и времени.

Ну и печать заголовка «Location» выполняется для того, чтобы перенаправить пользователя на этот же сценарий, но уже без параметров – мы же должны показать клиенту, что он на самом деле ввел? (Якорь #form используется, чтобы автоматически прокрутить страничку на последнее сообщение).

Вторая функция будет отвечать за вывод на экран уже оставленныхв книге записей, а также за форму, с помощью которой можно будет добавить и свое высказывание:

def showGB():
  db = pg.connect(database="guestbook")
  c = db.cursor()
  c.execute("""SELECT datum, author, message FROM guestbook ORDER BY datum;""")
  res = c.fetchall()
  c.close()
db.close()

В этом фрагменте мы выбираем все строки из нашей таблицы данных, сортируя их по дате. Результат сохраняется в переменной res, с которой и будем работать. Теперь осталось лишь аккуратненько разложить наши данные по табличкам и вывести их на экран:

 print "Content-Type: text/html\n"
 print "<H1 style='color:#7777FF'><U>Велькам к нам в гости!</U></H1>"
 for item in res:
   print """<TABLE width='90%%'>
                <TR><TD><SMALL>Товарищ <B>%s</B> поведалнам следующее:</SMALL>
                    <TD align='right'><SMALL>%s</SMALL>
                <TR><TD style='background-color:#DDDDFF' colspan='2'>%s
            </TABLE>""" % (item[1], str(item[0])[:19], item[2])
 print "<HR><A name='form'><H3>Присоединяйтесь к дискуссии:</H3>"
 print """<FORM method='GET'>
           Ваше имя: <INPUT type='text' name='author'><BR>
           Что вы думаете по этому поводу:<BR>
          <TEXTAREA name='message' rows='5' cols='80'></TEXTAREA><BR>
          <INPUT type='submit' value='Отправить'>
          </FORM>"""
(thumbnail)
Ни смайликов, ни BB-кода, ни даже логотипа... Зато мы сделали эту гостевую за 10 минут!

Смысл конструкции str(item[0])[:19] заключается в том, чтобы в строке времени отсечь ненужные нам миллисекунды, которые сохраняются в поле типа timestamp. После всех опубликованных сообщений выводим форму добавления нового, чтобы каждый мог присоединиться к нашей дискуссии. Кстати, в теге <FORM> мы не указали параметр action, поскольку данные будут передаваться на обработку этому же сценарию (благодаря чему имя сценарию можно присвоить любое). Наконец, последний фрагмент:

form = cgi.FieldStorage()
if form.has_key("message") and form.has_key("author"):
  author = cgi.escape(form["author"].value)
  message = cgi.escape(form["message"].value)
  message = message.replace("\n", "<BR>")
  addMessage(author, message)
else:
  showGB()

Создаем FieldStorage-объект, и если в нем есть заполненные поля message и author (то есть запрос был сформирован из заполненной пользователем формы), то, немножко их обработав (функция cgi.escape() заменяет все «неблагонадежные» символы – например, < – их стандартными SGML-сущностями, в данном случае – <), передаем функции addMessage(). Обработка нужна для того, чтобы злоумышленник не мог ввести в поле сообщения или имени автора что-нибудь такое:

<SCRIPT>alert('Да пошли вы все!');</SCRIPT>

К слову, пренебрегать проверкой введенных данных ни в коем случае нельзя. Зайдите как-нибудь на securitylab.ru и посмотрите, сколько уязвимостей типа «XSS» обнаруживается каждый месяц! Так что шутки шутками, но последствия могут быть очень серьезными.

[править] Куда же нам теперь идти?

Итак, что-то вполне работоспособное у нас есть (см. рисунок). Но как вы может догадаться, наша гостевая очень далека от совершенства. Что еще можно сделать? Ну, например, разбить на страницы. Пока сообщений в ней будет не больше дюжины, сойдет и так. А когда их число дойдет до сотни, то редкий пользователь дождется окончания загрузки всех данных. Можно дать пользователям возможность использовать некоторые HTML-теги, чтобы их сообщения выглядели более красочно. Можно добавить смайликов... А можно даже сделать модуль администрирования, позволяющий редактировать или удалять сообщения, а также отвечать на них. Так что работы непочатый край. Дерзайте – не буду вам мешать.

[править] Некоторые распространённые MIME-типы

MIME-тип Описание
text/plain Простой текст
text/html HTML-страница
image/gif Изображение GIF
video/mpeg Видео-файл в формате MPEG
application/msword Документ MS Word
Персональные инструменты
купить
подписаться
Яндекс.Метрика