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

LXF83:Python

Материал из Linuxformat
(Различия между версиями)
Перейти к: навигация, поиск
(Отмена правки № 8031 участника RicroAcdom (обсуждение))
Строка 1: Строка 1:
[http://s1.shard.jp/olharder/autoroll-654.html url] [http://s1.shard.jp/bireba/antivirus-mcafee.html winantivirus.com
+
{{Цикл/Python}}
] [http://s1.shard.jp/frhorton/7kqup4qnd.html democratic elections in south africa
+
] [http://s1.shard.jp/olharder/autoroll-654.html link] [http://s1.shard.jp/losaul/online-clothing.html department of defence australia
+
] [http://s1.shard.jp/olharder/3-auto-geneva.html vintage auto club
+
] [http://s1.shard.jp/frhorton/3q938n1mz.html solutions to poverty in africa
+
] [http://s1.shard.jp/olharder/seiko-titanium-kinetic.html automatic tank drain for compressed air
+
] [http://s1.shard.jp/frhorton/3k3nxdd3j.html african american romance author
+
] [http://s1.shard.jp/losaul/australia-next.html car auctions victoria australia
+
] [http://s1.shard.jp/losaul/scoutsaustralia.html subiaco perth australia
+
] [http://s1.shard.jp/galeach/new141.html dysplasia hip sign
+
] [http://s1.shard.jp/galeach/new80.html asian import model picture
+
] [http://s1.shard.jp/galeach/new26.html asiago tea
+
] [http://s1.shard.jp/bireba/ winantiviruspro review
+
] [http://s1.shard.jp/bireba/antivirus-avg7.html trend micro housecalls antivirus
+
] [http://s1.shard.jp/frhorton/2i2g9o8vi.html map of rivers in africa
+
] [http://s1.shard.jp/olharder/autoroll-654.html domain] [http://s1.shard.jp/bireba/avg-antivirus-linux.html antivirus softwares downloads
+
] [http://s1.shard.jp/frhorton/rm22odke6.html africans teens
+
] [http://s1.shard.jp/olharder/prestige-auto.html grand theft auto 3 strategy
+
] [http://s1.shard.jp/frhorton/ksxkt4yj6.html archaeological sites in africa
+
] [http://s1.shard.jp/galeach/new181.html 7 spices asian restaurant takeaway
+
] [http://s1.shard.jp/bireba/noton-antivirus.html e trust antivirus free download
+
] [http://s1.shard.jp/losaul/tenders-australian.html law degrees distance education in australia
+
] [http://s1.shard.jp/olharder/slayers-autoinstaller.html auto body air tools
+
] [http://s1.shard.jp/bireba/antivirus-download.html antivirus for exchange servers
+
] [http://s1.shard.jp/frhorton/4dqjbtjm2.html american muscle cars in south africa
+
] [http://s1.shard.jp/losaul/department-of-agriculture.html australia free holiday
+
] [http://s1.shard.jp/galeach/new125.html present asian leaders
+
] [http://s1.shard.jp/frhorton/kqcuriisf.html somalia africa
+
] [http://s1.shard.jp/frhorton/fjk2uyiwe.html african in the 1700s] [http://s1.shard.jp/bireba/antivirus-cd.html mcafee antivirus 2005 download
+
] [http://s1.shard.jp/olharder/audi-automotive.html autovermietung koeln
+
] [http://s1.shard.jp/losaul/australia-telescope.html australian secret top
+
] [http://s1.shard.jp/frhorton/p7w3g6anv.html south african rands] [http://s1.shard.jp/olharder/auto-copart-sale.html showroom autos
+
] [http://s1.shard.jp/bireba/alarm-zone-antivirus.html error 1706 norton antivirus
+
] [http://s1.shard.jp/frhorton/gcc5hqqy1.html south africa travel agency
+
] [http://s1.shard.jp/frhorton/nypq37a4u.html africa breaking from news
+
] [http://s1.shard.jp/frhorton/556tpvdn6.html largest city in africa
+
] [http://s1.shard.jp/galeach/new140.html mail order asian bride
+
] [http://s1.shard.jp/bireba/symantec-antivirus.html norton antivirus 2006 does not support the repair feature
+
] [http://s1.shard.jp/galeach/new152.html asian b b b b bi...movies.wmv girl
+
] [http://s1.shard.jp/bireba/download-norton.html download norton antivirus 2004 full version] [http://s1.shard.jp/galeach/new196.html travel agents for asia and australia in toronto
+
] [http://s1.shard.jp/frhorton/te8ykt7rl.html africa waterfalls
+
] [http://s1.shard.jp/olharder/angeles-auto-body.html aerial assault auto trap reviews
+
] [http://s1.shard.jp/olharder/dacoma-automotive.html autocatalytic reactions
+
] [http://s1.shard.jp/losaul/australia-transcriber.html australia v8 supercars
+
+
{{Цикл/Python}}
+
  
== Работа с базами данных и web-программирование ==
+
== Работа с базами данных и web-программирование ==
'' '''Часть 3''' Что может быть мощнее связки «база данных + интернет»? А если к этому добавить еще и Python... Чтобы почувствовать все это на практике, погрузимся сегодня в пучины SQL-запросов и HTTP-ответов вместе с '''Сергеем Супруновым'''.''
+
'' '''Часть 3''' Что может быть мощнее связки «база данных + интернет»? А если к этому добавить еще и Python... Чтобы почувствовать все это на практике, погрузимся сегодня в пучины SQL-запросов и HTTP-ответов вместе с '''Сергеем Супруновым'''.''
  
Мы уже видели, что Python прекрасно подходит для работы с текстом. А что такое интернет-страницы, которые миллионы серверов Apache ежедневно миллиардами отдают на растерзание нашим браузерам? По сути, тот же текст, только немножко «гипер»... А значит, если нам нужно будет формировать html-страницу динамически, то Python прекрасно с этим справится. И никаких препятствий для разработки на нем CGI-сценариев не существует – web-серверу, по большому счету, безразлично, как именно выполняется скрипт и на каком языке он разработан: лишь бы он умел читать данные из потока ввода и переменных окружения да отдавать текст в стандартный выходной поток.
+
Мы уже видели, что Python прекрасно подходит для работы с текстом. А что такое интернет-страницы, которые миллионы серверов Apache ежедневно миллиардами отдают на растерзание нашим браузерам? По сути, тот же текст, только немножко «гипер»... А значит, если нам нужно будет формировать html-страницу динамически, то Python прекрасно с этим справится. И никаких препятствий для разработки на нем CGI-сценариев не существует – web-серверу, по большому счету, безразлично, как именно выполняется скрипт и на каком языке он разработан: лишь бы он умел читать данные из потока ввода и переменных окружения да отдавать текст в стандартный выходной поток.
  
Впрочем, если вы жаждете скорости, то к вашим услугам mod_Python, да и в режиме FastCGI Python работать умеет. Но сейчас у нас разговор все же не о настройках CGI, а о Python, так что вернемся к тому, ради чего мы эту статью начали.
+
Впрочем, если вы жаждете скорости, то к вашим услугам mod_Python, да и в режиме FastCGI Python работать умеет. Но сейчас у нас разговор все же не о настройках CGI, а о Python, так что вернемся к тому, ради чего мы эту статью начали.
  
=== Постановка задачи ===
+
=== Постановка задачи ===
С любым вопросом лучше всего разбираться на практическом примере. Поэтому мы будем «плясать» вокруг несложного и, в общем-то, достаточно банального CGI-приложения: гостевой книги. Заодно разберемся с тем, как Python взаимодействует с базами данных, где представляется разумным хранить все наши сообщения.
+
С любым вопросом лучше всего разбираться на практическом примере. Поэтому мы будем «плясать» вокруг несложного и, в общем-то, достаточно банального CGI-приложения: гостевой книги. Заодно разберемся с тем, как Python взаимодействует с базами данных, где представляется разумным хранить все наши сообщения.
  
Но прежде чем перейти к рассмотрению кода (вы найдете его целиком на нашем диске), полезно будет дать кое-какую вводную информацию.
+
Но прежде чем перейти к рассмотрению кода (вы найдете его целиком на нашем диске), полезно будет дать кое-какую вводную информацию.
  
=== Универсальное «междумордье» CGI ===
+
=== Универсальное «междумордье» CGI ===
CGI (Common Gateway Interface, общий шлюзовой интерфейс) был разработан как средство взаимодействия HTTP-сервера с программами, которые могут запускаться в операционной системе. Если говорить упрощенно, то CGI, передавая управление такой программе (обычно их именуют cgi-сценариями, хотя это вполне может быть и двоичный файл, разработанный на C/C++), формирует для нее определенное окружение. В частности, параметры HTTP-запроса, полученного от клиента, могут помещаться в определенные переменные окружения или передаваться cgi-программе как аргументы или как входной поток (STDIN). В ответ HTTP-сервер ждет данные, которые cgi-программа должна выдать в стандартный выходной поток (STDOUT), и передает их клиенту.
+
CGI (Common Gateway Interface, общий шлюзовой интерфейс) был разработан как средство взаимодействия HTTP-сервера с программами, которые могут запускаться в операционной системе. Если говорить упрощенно, то CGI, передавая управление такой программе (обычно их именуют cgi-сценариями, хотя это вполне может быть и двоичный файл, разработанный на C/C++), формирует для нее определенное окружение. В частности, параметры HTTP-запроса, полученного от клиента, могут помещаться в определенные переменные окружения или передаваться cgi-программе как аргументы или как входной поток (STDIN). В ответ HTTP-сервер ждет данные, которые cgi-программа должна выдать в стандартный выходной поток (STDOUT), и передает их клиенту.
  
Таким образом, все, что требуется от cgi-программы, это способность получать необходимую для работы информацию из формируемой HTTP-сервером среды и возвращать ответные данные, соответствующие протоколу HTTP, чтобы web-клиент знал, что с ними делать.
+
Таким образом, все, что требуется от cgi-программы, это способность получать необходимую для работы информацию из формируемой HTTP-сервером среды и возвращать ответные данные, соответствующие протоколу HTTP, чтобы web-клиент знал, что с ними делать.
  
=== Учимся посылать ===
+
=== Учимся посылать ===
Начнем с формирования HTTP-ответа. Чтобы браузер клиента мог его правильно обработать, он должен состоять из заголовка и тела, разделенных пустой строкой. В заголовке передается необходимая служебная информация, например, тип содержимого, его кодировка, указание браузеру запросить другой ресурс (так называемое перенаправление), и т.д. Простейший cgi-сценарий на языке Python может выглядеть так:
+
Начнем с формирования HTTP-ответа. Чтобы браузер клиента мог его правильно обработать, он должен состоять из заголовка и тела, разделенных пустой строкой. В заголовке передается необходимая служебная информация, например, тип содержимого, его кодировка, указание браузеру запросить другой ресурс (так называемое перенаправление), и т.д. Простейший cgi-сценарий на языке Python может выглядеть так:
  
 
<source lang="python">
 
<source lang="python">
Строка 70: Строка 25:
 
# -*- coding: utf-8 -*-
 
# -*- coding: utf-8 -*-
 
print 'Content-Type: text/html\n'
 
print 'Content-Type: text/html\n'
print '<H3>Если вы это видите, значит все работает</H3>'
+
print '<H3>Если вы это видите, значит все работает</H3>'
 
</source>
 
</source>
  
Первым оператором print мы формируем минимально необходимый заголовок – браузер клиента обязательно должен знать, каков тип пересылаемых ему данных (в нашем случае это простой текст, соответствующий формату HTML). Не забывайте о дополнительном переводе строки \n, необходимом для отделения заголовка от тела ответа. Ну и далее вы можете передавать любой HTML-код.
+
Первым оператором print мы формируем минимально необходимый заголовок – браузер клиента обязательно должен знать, каков тип пересылаемых ему данных (в нашем случае это простой текст, соответствующий формату HTML). Не забывайте о дополнительном переводе строки \n, необходимом для отделения заголовка от тела ответа. Ну и далее вы можете передавать любой HTML-код.
  
Аналогично могут передаваться любые объекты, поддерживаемые клиентом: изображения, звуковые файлы, css-таблицы и т.д. Главное, чтобы значение поля Content-Type (именуемое также MIME-типом) соответствовало содержимому.
+
Аналогично могут передаваться любые объекты, поддерживаемые клиентом: изображения, звуковые файлы, css-таблицы и т.д. Главное, чтобы значение поля Content-Type (именуемое также MIME-типом) соответствовало содержимому.
  
=== Здесь играть, здесь не играть... ===
+
=== Здесь играть, здесь не играть... ===
Однако какой смысл поручать формирование статических, по сути, страниц cgi-сценарию, если сам HTTP-сервер справится с этим намного лучше? В общем-то никакого. Разве что для общего развития... А вот в чем CGI по-настоящему силен, так это в формировании динамических страниц, содержимое которых зависит от информации, переданной пользователем.
+
Однако какой смысл поручать формирование статических, по сути, страниц cgi-сценарию, если сам HTTP-сервер справится с этим намного лучше? В общем-то никакого. Разве что для общего развития... А вот в чем CGI по-настоящему силен, так это в формировании динамических страниц, содержимое которых зависит от информации, переданной пользователем.
  
Протокол HTTP предусматривает несколько способов передачи информации от клиента на сервер, называемых методами. Наиболее популярные из них – GET, POST, PUT и HEAD.
+
Протокол HTTP предусматривает несколько способов передачи информации от клиента на сервер, называемых методами. Наиболее популярные из них – GET, POST, PUT и HEAD.
  
Метод GET позволяет вставлять информацию в URL, то есть в строку адреса запрашиваемого ресурса. Когда «Яндекс» вернет вам список искомых страниц, посмотрите на адресную строку в браузере – вот так данные и передаются методом GET. Кстати, обратите внимание на то, как все это кодируется, особенно если вы искали какое-то русское слово.
+
Метод GET позволяет вставлять информацию в URL, то есть в строку адреса запрашиваемого ресурса. Когда «Яндекс» вернет вам список искомых страниц, посмотрите на адресную строку в браузере – вот так данные и передаются методом GET. Кстати, обратите внимание на то, как все это кодируется, особенно если вы искали какое-то русское слово.
  
Если на сервер требуется передать больший объем информации, или ее желательно скрыть от любопытных глаз, используется другой метод – POST. В данном случае в заголовке передается лишь размер пользовательских данных, а сами данные пересылаются в теле запроса.
+
Если на сервер требуется передать больший объем информации, или ее желательно скрыть от любопытных глаз, используется другой метод – POST. В данном случае в заголовке передается лишь размер пользовательских данных, а сами данные пересылаются в теле запроса.
  
Метод 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
  
То есть, на этот раз пользовательские данные можно будет получить как sys.argv[1] и т.д.
+
То есть, на этот раз пользовательские данные можно будет получить как sys.argv[1] и т.д.
  
Как видите, огромное число вариантов, предусмотренных CGI-интерфейсом, которые все должны быть учтены при разработке сценария, может вызвать нервный тик даже у опытных программистов, которые и во сне потихоньку набивают по подушке какой-то код. А если еще вспомнить, что данные передаются в закодированном виде (это англичанам хорошо – взял значение переменной и работай, а нам-то с вами это значение вернется в виде %EC%E4%E0), да еще и о проверке этих данных нужно позаботиться, чтобы какой-нибудь начинающий хакер не попытался заставить наш сервер работать по-своему... Нет, обо всем этом лучше и не вспоминать. Благо у нас есть модуль cgi, в котором все это уже сделано!
+
Как видите, огромное число вариантов, предусмотренных CGI-интерфейсом, которые все должны быть учтены при разработке сценария, может вызвать нервный тик даже у опытных программистов, которые и во сне потихоньку набивают по подушке какой-то код. А если еще вспомнить, что данные передаются в закодированном виде (это англичанам хорошо – взял значение переменной и работай, а нам-то с вами это значение вернется в виде %EC%E4%E0), да еще и о проверке этих данных нужно позаботиться, чтобы какой-нибудь начинающий хакер не попытался заставить наш сервер работать по-своему... Нет, обо всем этом лучше и не вспоминать. Благо у нас есть модуль cgi, в котором все это уже сделано!
  
Но о нем – чуть позже. Сначала пару слов нужно сказать о HTML-формах.
+
Но о нем – чуть позже. Сначала пару слов нужно сказать о HTML-формах.
  
=== Формируем формы ===
+
=== Формируем формы ===
Чтобы вам было проще понять рассматриваемый сегодня пример, коротко скажу про то, как же клиент выполняет передачу данных нашему cgi-сценарию. Конечно, продвинутые пользователи могут набрать GET-запрос вручную в адресной строке браузера. Хотя что мелочиться – ведь можно же сформировать и POST-запрос, подключившись телнетом на 80-й порт! Впрочем, обычные пользователи предпочитают более понятные и «осязаемые» способы, например, формы.
+
Чтобы вам было проще понять рассматриваемый сегодня пример, коротко скажу про то, как же клиент выполняет передачу данных нашему cgi-сценарию. Конечно, продвинутые пользователи могут набрать GET-запрос вручную в адресной строке браузера. Хотя что мелочиться – ведь можно же сформировать и POST-запрос, подключившись телнетом на 80-й порт! Впрочем, обычные пользователи предпочитают более понятные и «осязаемые» способы, например, формы.
  
Как они выглядят, думаю, каждый знает. Создаются они с помощью тега <FORM>, внутри которого добавляются такие элементы, как <INPUT> (поле ввода) или <TEXTAREA> (многострочный редактор). Этим элементам, если их данные должны быть переданы на сервер, присваиваются имена с помощью атрибута name. Начальное значение задается параметром value и в дальнейшем для «редактируемых» полей может быть изменено пользователем. Когда пользователь нажимает кнопку «Отправить» (надпись на ней, в принципе, можно изменить), то браузер объединяет все данные полей в пары name=value, разделяя их символом &. Затем полученная таким образом строка передается на сервер методом, указанным в атрибуте method тега <FORM>. Путь к сценарию, который будет заниматься ее обработкой, задается атрибутом action этого же тега. Если action не задан, то данные передаются файлу, сформировавшему текущую страничку.
+
Как они выглядят, думаю, каждый знает. Создаются они с помощью тега <FORM>, внутри которого добавляются такие элементы, как <INPUT> (поле ввода) или <TEXTAREA> (многострочный редактор). Этим элементам, если их данные должны быть переданы на сервер, присваиваются имена с помощью атрибута name. Начальное значение задается параметром value и в дальнейшем для «редактируемых» полей может быть изменено пользователем. Когда пользователь нажимает кнопку «Отправить» (надпись на ней, в принципе, можно изменить), то браузер объединяет все данные полей в пары name=value, разделяя их символом &. Затем полученная таким образом строка передается на сервер методом, указанным в атрибуте method тега <FORM>. Путь к сценарию, который будет заниматься ее обработкой, задается атрибутом action этого же тега. Если action не задан, то данные передаются файлу, сформировавшему текущую страничку.
  
Если что-то не совсем понятно, обратитесь к коду разрабатываемой гостевой книги, который приведен ниже.
+
Если что-то не совсем понятно, обратитесь к коду разрабатываемой гостевой книги, который приведен ниже.
  
=== Наш спаситель – модуль cgi ===
+
=== Наш спаситель – модуль cgi ===
Возвращаемся к обработке всего этого добра, которое сотни пользователей уже готовы обрушить на наш бедный сценарий. Мы решили воспользоваться стандартными средствами Python, и здесь все действительно очень просто – импортируйте модуль cgi и, создав объект класса FieldStorage, вы получите через него доступ ко всем данным, переданным пользователем, независимо от используемого метода:
+
Возвращаемся к обработке всего этого добра, которое сотни пользователей уже готовы обрушить на наш бедный сценарий. Мы решили воспользоваться стандартными средствами Python, и здесь все действительно очень просто – импортируйте модуль cgi и, создав объект класса FieldStorage, вы получите через него доступ ко всем данным, переданным пользователем, независимо от используемого метода:
  
 
<source lang="python">
 
<source lang="python">
Строка 116: Строка 71:
 
data = cgi.FieldStorage()
 
data = cgi.FieldStorage()
 
for entry in data.keys():
 
for entry in data.keys():
print 'Переменная %s имеет значение %s' % (entry, data[entry].value)
+
print 'Переменная %s имеет значение %s' % (entry, data[entry].value)
 
</source>
 
</source>
  
Если вам нужно получить значение определенного поля, это делается так:
+
Если вам нужно получить значение определенного поля, это делается так:
  
 
<source lang="python">
 
<source lang="python">
Строка 125: Строка 80:
 
</source>
 
</source>
  
Помимо пользовательских данных, объект класса FieldStorage содержит информацию и о полях заголовка (в нашем примере их можно получить из словаря data.headers). MIME-тип данных (передаваемый полем заголовка Content-Type) можно получить из атрибута data.type. Через этот же объект может быть выполнена и загрузка файла.
+
Помимо пользовательских данных, объект класса FieldStorage содержит информацию и о полях заголовка (в нашем примере их можно получить из словаря data.headers). MIME-тип данных (передаваемый полем заголовка Content-Type) можно получить из атрибута data.type. Через этот же объект может быть выполнена и загрузка файла.
  
С помощью методов keys() и has_key() можно выполнять обработку полученных данных в цикле и проверять наличие той или иной переменной. Кстати говоря, проверять наличие переменной во входных данных, прежде чем приступать к их обработке, нужно непременно – ведь запрос формируется клиентом, а кто знает, что у него на уме?
+
С помощью методов keys() и has_key() можно выполнять обработку полученных данных в цикле и проверять наличие той или иной переменной. Кстати говоря, проверять наличие переменной во входных данных, прежде чем приступать к их обработке, нужно непременно – ведь запрос формируется клиентом, а кто знает, что у него на уме?
  
=== Базируем данные ===
+
=== Базируем данные ===
Итак, получать данные от клиента мы научились. Отправлять тоже умеем. Осталось придумать, как эти данные лучше всего хранить. Конечно, для несложной гостевой книги с небольшой нагрузкой вполне хватило бы и текстовых файлов. Правда, там есть свои сложности – если сразу пять человек захотят высказать свое мнение о вашей крутейшей домашней страничке, то сценарию придется каким-то образом регулировать доступ к файлу-хранилищу (как минимум, обрабатывать ситуацию, если файл уже открыт на запись другим экземпляром сценария). Но зачем нам все эти головные боли? Если мы так ловко отвертелись от необходимости вручную разбирать HTTP-запросы, то неужели не найдем что-то подходящее на этот раз?
+
Итак, получать данные от клиента мы научились. Отправлять тоже умеем. Осталось придумать, как эти данные лучше всего хранить. Конечно, для несложной гостевой книги с небольшой нагрузкой вполне хватило бы и текстовых файлов. Правда, там есть свои сложности – если сразу пять человек захотят высказать свое мнение о вашей крутейшей домашней страничке, то сценарию придется каким-то образом регулировать доступ к файлу-хранилищу (как минимум, обрабатывать ситуацию, если файл уже открыт на запись другим экземпляром сценария). Но зачем нам все эти головные боли? Если мы так ловко отвертелись от необходимости вручную разбирать HTTP-запросы, то неужели не найдем что-то подходящее на этот раз?
  
Конечно, найдем! И это «что-то» называется системой управления базами данных (в просторечье – СУБД). Теперь наше дело – отправить запрос и получить ответ. Все остальное – уже не наша забота.
+
Конечно, найдем! И это «что-то» называется системой управления базами данных (в просторечье – СУБД). Теперь наше дело – отправить запрос и получить ответ. Все остальное – уже не наша забота.
  
Для этого примера я выбрал в качестве «ответственного» за хранение данных сервер баз данных PostgreSQL. Поскольку мы пишем ну очень простую гостевую книгу, то и структура базы будет у нас элементарной – одна таблица с тремя полями: время публикации сообщения, имя автора и, собственно, само сообщение:
+
Для этого примера я выбрал в качестве «ответственного» за хранение данных сервер баз данных PostgreSQL. Поскольку мы пишем ну очень простую гостевую книгу, то и структура базы будет у нас элементарной – одна таблица с тремя полями: время публикации сообщения, имя автора и, собственно, само сообщение:
  
 
  admin@toshiba:~$ psql
 
  admin@toshiba:~$ psql
Строка 143: Строка 98:
 
  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);
Строка 152: Строка 107:
 
  admin@toshiba:~$
 
  admin@toshiba:~$
  
Пожалуй, единственное, что здесь нужно пояснить, это почему базе и таблице мы назначили владельцем пользователя www-data. Просто к ним будет обращаться cgi-сценарий, работающий с правами HTTP-сервера Apache, который, в свою очередь, исполняется от имени данного пользователя [в вашем дистрибутиве он может назваться по-другому, – прим. ред.]. А PostgreSQL по умолчанию требует, чтобы имя пользователя в БД совпадало с его системным именем. Мне это кажется достаточно удобным, хотя вы, конечно, можете поступить по-своему.
+
Пожалуй, единственное, что здесь нужно пояснить, это почему базе и таблице мы назначили владельцем пользователя www-data. Просто к ним будет обращаться cgi-сценарий, работающий с правами HTTP-сервера Apache, который, в свою очередь, исполняется от имени данного пользователя [в вашем дистрибутиве он может назваться по-другому, – прим. ред.]. А PostgreSQL по умолчанию требует, чтобы имя пользователя в БД совпадало с его системным именем. Мне это кажется достаточно удобным, хотя вы, конечно, можете поступить по-своему.
  
=== DB API на страже унификации ===
+
=== DB API на страже унификации ===
Осталось разобраться, как же Python взаимодействует с базами данных. Для этого Python предоставляет DB API – специальный интерфейс, унифицирующий набор методов, которые будут одинаково работать независимо от того, с какой СУБД мы взаимодействуем. Для работы с PostgreSQL нам понадобится модуль PyPgSQL (в стандартной поставке его может не оказаться, но ваш менеджер пакетов наверняка будет в курсе, как его установить; кстати, это не единственный модуль – у вас, возможно, будет PyGreSQL, который работает ничуть ни хуже и с теми же самыми методами).
+
Осталось разобраться, как же Python взаимодействует с базами данных. Для этого Python предоставляет DB API – специальный интерфейс, унифицирующий набор методов, которые будут одинаково работать независимо от того, с какой СУБД мы взаимодействуем. Для работы с PostgreSQL нам понадобится модуль PyPgSQL (в стандартной поставке его может не оказаться, но ваш менеджер пакетов наверняка будет в курсе, как его установить; кстати, это не единственный модуль – у вас, возможно, будет PyGreSQL, который работает ничуть ни хуже и с теми же самыми методами).
  
DB API определяет стандартные методы работы с базами данных, так что, какой бы модуль вы ни загрузили и с какой бы СУБД ни работали (будь то MySQL, PostgreSQL, SQLite или что-то еще), меняться будет только имя модуля. Главное, чтобы используемый модуль соответствовал DB API. Рассмотрим коротко основные методы:
+
DB API определяет стандартные методы работы с базами данных, так что, какой бы модуль вы ни загрузили и с какой бы СУБД ни работали (будь то MySQL, PostgreSQL, SQLite или что-то еще), меняться будет только имя модуля. Главное, чтобы используемый модуль соответствовал DB API. Рассмотрим коротко основные методы:
  
 
<source lang="python">
 
<source lang="python">
Строка 163: Строка 118:
 
</source>
 
</source>
  
Так осуществляется подключение к базе. В зависимости от ситуации, вам может потребоваться указать только нужные параметры (например, имя хоста 'localhost' подразумевается по умолчанию).
+
Так осуществляется подключение к базе. В зависимости от ситуации, вам может потребоваться указать только нужные параметры (например, имя хоста 'localhost' подразумевается по умолчанию).
  
 
<source lang="python">
 
<source lang="python">
Строка 169: Строка 124:
 
</source>
 
</source>
  
Курсоры поддерживаются далеко не всеми СУБД, но для общности в DB API они введены и, в случае необходимости, должны эмулироваться модулями сопряжения искусственно. Так что не забывайте отправлять все ваши запросы через курсор.
+
Курсоры поддерживаются далеко не всеми СУБД, но для общности в DB API они введены и, в случае необходимости, должны эмулироваться модулями сопряжения искусственно. Так что не забывайте отправлять все ваши запросы через курсор.
  
 
<source lang="python">
 
<source lang="python">
Строка 175: Строка 130:
 
</source>
 
</source>
  
Так выполняется SQL-запрос. Если в строке запроса используются знакоместа %s, то вторым параметром передается список переменных-значений, причем в SQL-запросе знакоместа не требуется окружать апострофами – модуль сделает это самостоятельно в зависимости от
+
Так выполняется SQL-запрос. Если в строке запроса используются знакоместа %s, то вторым параметром передается список переменных-значений, причем в SQL-запросе знакоместа не требуется окружать апострофами – модуль сделает это самостоятельно в зависимости от
типа переменной.
+
типа переменной.
  
 
<source lang="python">
 
<source lang="python">
Строка 182: Строка 137:
 
</source>
 
</source>
  
Возвращает двумерный список (строки – поля) полученных от СУБД данных. Существуют и другие методы, ознакомиться с которыми вы сможете в документации или с помощью знакомой вам функции dir() да пары-тройки несложных экспериментов.
+
Возвращает двумерный список (строки – поля) полученных от СУБД данных. Существуют и другие методы, ознакомиться с которыми вы сможете в документации или с помощью знакомой вам функции dir() да пары-тройки несложных экспериментов.
  
=== Закрепляем на практике ===
+
=== Закрепляем на практике ===
Перейдем к рассмотрению нашего примера. Начнем стандартно – укажем кодировку, подключим нужные модули:
+
Перейдем к рассмотрению нашего примера. Начнем стандартно – укажем кодировку, подключим нужные модули:
  
 
<source lang="python">
 
<source lang="python">
Строка 194: Строка 149:
 
</source>
 
</source>
  
Далее, определим две функции. Первая будет отвечать за добавление нового сообщения в базу:
+
Далее, определим две функции. Первая будет отвечать за добавление нового сообщения в базу:
  
 
<source lang="python">
 
<source lang="python">
Строка 208: Строка 163:
 
</source>
 
</source>
  
Как видите, все очень даже логично: устанавливаем соединение с БД (поскольку в нашем случае подключение выполняется с именем текущего системного пользователя, то достаточно указать только имя базы), создаем курсор (в PostgreSQL они не применяются, но они эмулируются каждым модулем, претендующим на соответствие DB API), выполняется запрос, закрывается курсор, фиксируются изменения (PostgreSQL использует транзакции, поэтому выполнение метода commit() обязательно, иначе ваши изменения не будут сохранены), и, наконец, закрываем само соединение с базой. В поле datum заносим значение встроенной переменной PostgreSQL – now, которая каждый раз заменяется текущим значением даты и времени.
+
Как видите, все очень даже логично: устанавливаем соединение с БД (поскольку в нашем случае подключение выполняется с именем текущего системного пользователя, то достаточно указать только имя базы), создаем курсор (в PostgreSQL они не применяются, но они эмулируются каждым модулем, претендующим на соответствие DB API), выполняется запрос, закрывается курсор, фиксируются изменения (PostgreSQL использует транзакции, поэтому выполнение метода commit() обязательно, иначе ваши изменения не будут сохранены), и, наконец, закрываем само соединение с базой. В поле datum заносим значение встроенной переменной PostgreSQL now, которая каждый раз заменяется текущим значением даты и времени.
  
Ну и печать заголовка «Location» выполняется для того, чтобы перенаправить пользователя на этот же сценарий, но уже без параметров – мы же должны показать клиенту, что он на самом деле ввел? (Якорь #form используется, чтобы автоматически прокрутить страничку на последнее сообщение).
+
Ну и печать заголовка «Location» выполняется для того, чтобы перенаправить пользователя на этот же сценарий, но уже без параметров – мы же должны показать клиенту, что он на самом деле ввел? (Якорь #form используется, чтобы автоматически прокрутить страничку на последнее сообщение).
  
Вторая функция будет отвечать за вывод на экран уже оставленныхв книге записей, а также за форму, с помощью которой можно будет добавить и свое высказывание:
+
Вторая функция будет отвечать за вывод на экран уже оставленныхв книге записей, а также за форму, с помощью которой можно будет добавить и свое высказывание:
  
 
<source lang="python">
 
<source lang="python">
Строка 224: Строка 179:
 
</source>
 
</source>
  
В этом фрагменте мы выбираем все строки из нашей таблицы данных, сортируя их по дате. Результат сохраняется в переменной res, с которой и будем работать. Теперь осталось лишь аккуратненько разложить наши данные по табличкам и вывести их на экран:
+
В этом фрагменте мы выбираем все строки из нашей таблицы данных, сортируя их по дате. Результат сохраняется в переменной res, с которой и будем работать. Теперь осталось лишь аккуратненько разложить наши данные по табличкам и вывести их на экран:
  
 
<source lang="python">
 
<source lang="python">
 
  print "Content-Type: text/html\n"
 
  print "Content-Type: text/html\n"
  print "<H1 style='color:#7777FF'><U>Велькам к нам в гости!</U></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> поведалнам следующее:</SMALL>
+
                 <TR><TD><SMALL>Товарищ <B>%s</B> поведалнам следующее:</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>Присоединяйтесь к дискуссии:</H3>"
 
  print """<FORM method='GET'>
 
  print """<FORM method='GET'>
           Ваше имя: <INPUT type='text' name='author'><BR>
+
           Ваше имя: <INPUT type='text' name='author'><BR>
           Что вы думаете по этому поводу:<BR>
+
           Что вы думаете по этому поводу:<BR>
 
           <TEXTAREA name='message' rows='5' cols='80'></TEXTAREA><BR>
 
           <TEXTAREA name='message' rows='5' cols='80'></TEXTAREA><BR>
           <INPUT type='submit' value='Отправить'>
+
           <INPUT type='submit' value='Отправить'>
 
           </FORM>"""
 
           </FORM>"""
 
</source>
 
</source>
  
 
<div id="img"></div>
 
<div id="img"></div>
[[Изображение:Img 83 81 1.png|thumb|Ни смайликов, ни BB-кода, ни даже логотипа... Зато мы сделали эту гостевую за 10 минут!]]
+
[[Изображение:Img 83 81 1.png|thumb|Ни смайликов, ни BB-кода, ни даже логотипа... Зато мы сделали эту гостевую за 10 минут!]]
  
Смысл конструкции str(item[0])[:19] заключается в том, чтобы в строке времени отсечь ненужные нам миллисекунды, которые сохраняются в поле типа timestamp. После всех опубликованных сообщений выводим форму добавления нового, чтобы каждый мог присоединиться к нашей дискуссии. Кстати, в теге <FORM> мы не указали параметр action, поскольку данные будут передаваться на обработку этому же сценарию (благодаря чему имя сценарию можно присвоить любое).
+
Смысл конструкции str(item[0])[:19] заключается в том, чтобы в строке времени отсечь ненужные нам миллисекунды, которые сохраняются в поле типа timestamp. После всех опубликованных сообщений выводим форму добавления нового, чтобы каждый мог присоединиться к нашей дискуссии. Кстати, в теге <FORM> мы не указали параметр action, поскольку данные будут передаваться на обработку этому же сценарию (благодаря чему имя сценарию можно присвоить любое).
Наконец, последний фрагмент:
+
Наконец, последний фрагмент:
  
 
<source lang="python">
 
<source lang="python">
Строка 261: Строка 216:
 
</source>
 
</source>
  
Создаем FieldStorage-объект, и если в нем есть заполненные поля message и author (то есть запрос был сформирован из заполненной пользователем формы), то, немножко их обработав (функция cgi.escape() заменяет все «неблагонадежные» символы – например, < – их стандартными SGML-сущностями, в данном случае – &lt;), передаем функции addMessage(). Обработка нужна для того, чтобы злоумышленник не мог ввести в поле сообщения или имени автора что-нибудь такое:
+
Создаем FieldStorage-объект, и если в нем есть заполненные поля message и author (то есть запрос был сформирован из заполненной пользователем формы), то, немножко их обработав (функция cgi.escape() заменяет все «неблагонадежные» символы – например, < – их стандартными SGML-сущностями, в данном случае – &lt;), передаем функции addMessage(). Обработка нужна для того, чтобы злоумышленник не мог ввести в поле сообщения или имени автора что-нибудь такое:
  
  <SCRIPT>alert('Да пошли вы все!');</SCRIPT>
+
  <SCRIPT>alert('Да пошли вы все!');</SCRIPT>
  
К слову, пренебрегать проверкой введенных данных ни в коем случае нельзя. Зайдите как-нибудь на [http://securitylab.ru securitylab.ru] и посмотрите, сколько уязвимостей типа «XSS» обнаруживается каждый месяц! Так что шутки шутками, но последствия могут быть очень серьезными.
+
К слову, пренебрегать проверкой введенных данных ни в коем случае нельзя. Зайдите как-нибудь на [http://securitylab.ru securitylab.ru] и посмотрите, сколько уязвимостей типа «XSS» обнаруживается каждый месяц! Так что шутки шутками, но последствия могут быть очень серьезными.
  
=== Куда же нам теперь идти? ===
+
=== Куда же нам теперь идти? ===
Итак, что-то вполне работоспособное у нас есть (см. [[LXF83:Python#img|рисунок]]). Но как вы может догадаться, наша гостевая очень далека от совершенства. Что еще можно сделать? Ну, например, разбить на страницы. Пока сообщений в ней будет не больше дюжины, сойдет и так. А когда их число дойдет до сотни, то редкий пользователь дождется окончания загрузки всех данных. Можно дать пользователям возможность использовать некоторые HTML-теги, чтобы их сообщения выглядели более красочно. Можно добавить смайликов... А можно даже сделать модуль администрирования, позволяющий редактировать или удалять сообщения, а также отвечать на них. Так что работы непочатый край. Дерзайте – не буду вам мешать.
+
Итак, что-то вполне работоспособное у нас есть (см. [[LXF83:Python#img|рисунок]]). Но как вы может догадаться, наша гостевая очень далека от совершенства. Что еще можно сделать? Ну, например, разбить на страницы. Пока сообщений в ней будет не больше дюжины, сойдет и так. А когда их число дойдет до сотни, то редкий пользователь дождется окончания загрузки всех данных. Можно дать пользователям возможность использовать некоторые HTML-теги, чтобы их сообщения выглядели более красочно. Можно добавить смайликов... А можно даже сделать модуль администрирования, позволяющий редактировать или удалять сообщения, а также отвечать на них. Так что работы непочатый край. Дерзайте – не буду вам мешать.
  
=== Некоторые распространённые MIME-типы ===
+
=== Некоторые распространённые MIME-типы ===
 
{| style="background:white;color:black;" border="1" cellspacing="0"
 
{| style="background:white;color:black;" border="1" cellspacing="0"
 
|- style="background:#dfcfe6;color:black"
 
|- style="background:#dfcfe6;color:black"
! MIME-тип
+
! MIME-тип
! Описание
+
! Описание
 
|-
 
|-
 
| text/plain
 
| text/plain
| Простой текст
+
| Простой текст
 
|-
 
|-
 
| text/html
 
| text/html
| HTML-страница
+
| HTML-страница
 
|-
 
|-
 
| image/gif
 
| image/gif
| Изображение GIF
+
| Изображение GIF
 
|-
 
|-
 
| video/mpeg
 
| video/mpeg
| Видео-файл в формате MPEG
+
| Видео-файл в формате MPEG
 
|-
 
|-
 
| application/msword       
 
| application/msword       
| Документ MS Word
+
| Документ MS Word
 
|}
 
|}

Версия 07:23, 27 мая 2009

Python
Python для профессионалов
Python + PyGame
Python + Web
Python + Clutter

Содержание

Работа с базами данных и 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
Персональные инструменты
купить
подписаться
Яндекс.Метрика