Консалтинг и автоматизация в области управления
эффективностью банковского бизнеса

Журнал ВРМ World

Из опыта Web-мастера: переползаем на Python

Осенью прошлого года был запущен сайт www.iso.ru, разработанный компанией ADT. "Движок" сайта, представляющий собой набор CGI скриптов, был написан на Perl. По прошествии полугода эксплуатации возникла необходимость расширить функциональность сайта. Поэтому встал вопрос о выборе языка для написания скриптов.

Perl хорошо подходит для обработки текстов и широко используется для web-программирования, однако программы, написанные на Perl, трудночитаемы и неудобны для сопровождение из-за специфического синтаксиса Perl'a. Если стоит задача быстро написать небольшой скрипт усилиями одного человека и у вас специфический склад мышления, то, возможно, Perl - это то, что вам нужно. Если же требуется разработать достаточно сложную систему и затем организовать ее поддержку коллективом специалистов, то для этих целей, на мой взгляд, более подходит Python.

Python сочетает в себе понятный синтаксис и мощь, имеет развитые средства обработки текста и создания web-приложений. Python доступен для различных операционных систем, таких как UNIX (Linux), MacOS, MS-Windows 3.1, Windows NT, OS/2 и даже MS-DOS. Скрипты, написаные на Python являются хорошо переносимыми между платформами. Если бы возникла задача перенести сайт www.iso.ru с платформы Linux на Windows NT, потребовались бы минимальные изменения кода (по существу, пришлось бы только исправить пути к файлам шаблонов). Впрочем, более подробно о достоинствах Python'а вы можете узнать из статьи "Знакомьтесь - Python" Якова Марковича, опубликованной в Журнале №1.

Таким образом, решено было использовать Python для написания скриптов для сайта www.iso.ru. "Движок" сайта состоял из следующих логических частей (скриптов):

  • Главная страница - вывод главной страницы сайта, списка новостей и событий.
  • Новости - вывод текста новости, списка архивных новостей сайта, клуба, технологий.
  • События - вывод текста события.
  • Регистрация - осуществление процедуры регистрации посетителя: запись в базу данных информации о посетителе, контроль уникальности учетных записей.
  • Работа с посетителями - проверка входного имени и пароля, организация скачивания файлов с сайта, подписка на новости, доступ к страницам технической поддержки.
  • Формоотправитель - скрипт, занимающийся отправкой заполненых посетителем форм на e-mail поддержки сайта.
  • Журнал - работа с базой данных журнала статей по IT-технологиям.
  • Гостевая книга - запись и просмотр комментариев к статьям журнала.
  • Конференции
  • Административный интерфейс - редактирование таблиц базы данных сайта, загрузка новых статей, новостей, событий, выгрузка данных из базы в формат CSV, управление конференциями.

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

Все скрипты написаны достаточно стандартным образом: создается экземпляр класса FieldStorage, который читает содержимое формы, затем, в зависимости от наличия и содержания определенных ключей организуется ветвление, обработка данных и вывод результата. В этой статье мне хотелось бы только поделиться опытом преодоления некоторых трудностей, возникших при разработке скриптов для www.iso.ru.

Формирование HTML из шаблонов

Как известно, вывод любой CGI программы состоит из двух частей: заголовка и данных, которые разделяются пустой строкой. Сначала программа должна сообщить клиенту, какой тип данных он будет получать. Это достигается печатью набора HTTP заголовков в стандартный вывод. Например, строка

print 'Content-Type: text/html\n'

сообщает браузеру, что он будет получать стандартный HTML.

Затем, после разделительной строки, идут произвольные данные (обычно, код HTML). Конечно, можно просто вывести HTML текст оператором print, вставленным в тело программы, но это подходит лишь для небольших фрагментов HTML кода. Когда же необходимо вывести несколько десятков строчек, исходный текст программы становиться очень большим и нечитаемым, к тому же иногда необходимо использовать одни и те же фрагменты кода с незначительными изменениями. В этом случае удобно использовать шаблоны.

Шаблон представляет собой текстовый файл, содержащий HTML код с переменными, которые впоследствии будут заменяться необходимыми значениями. Имена переменных желательно сделать такими, чтобы исключить их случайное повторение внутри HTML кода (например, не нужно использовать переменную с именем table, так как потом в результате пострадают все определения таблиц). Для переменных в своих шаблонах я использую следующее соглашение: имя переменной начинается и заканчивается символом $ (например, $var_name$). Это исключает возмоожность совпадения с тегами HTML и словами в тексте документа.

Вот пример шаблона гостевой книги, хранящийся в файле guestbook.tmpl:


<table width=100% align=center>
  <tr>
    <td width=100%>
      <table width=100% align=center>
        <tr>
          <td><b>$date$</b> $time$ $author$</td>
        </tr>
      </table>
      <table width =100%>
        <tr>
          <td>
            <div align="justify"><p>$message$<br><br>
            <table>
              <tr>
                <td bgcolor="#074473">
                   <img src="/img/gif.gif" width=200 height=1>
                </td>
              </tr>
            </table>
          </td>
        </tr>
      </table>
      <img src="/img/gif.gif" width="1" height="5" alt="" border="0">
    </td>
    <td><img src="/img/gif.gif" width="30" height="1" alt="" border="0"></td>
  </tr>
</table>

Для удобства работы можно написать функцию, читающую шаблон и заменяющую все переменные в нем на нужные значения. Она получает в качестве аргументов имя шаблона и словарь, представляющий собой набор пар <имя переменной> : <значение>. Возвращает функция сконструированный текст. Функция может выгладеть примерно так:


def replace_tmpl( tmpl, var_list ):
    lines = open( tmpl ).readlines()
    src = "%s"*len(lines) % tuple(lines)
    for key, var in var_list.items():
        src = string.replace( src, key, var )
    return src

А вот скрипт, выводящий запись в гостевой книге:


print 'Content-Type: text/html\n\n'
guestbook = '/usr/local/apache/cgi-bin/templates/guestbook.tmpl'
body = replace_tmpl(guestbook, {'$date$'    : '2001-06-07',
                                '$author$'  : 'Артемов Олег',                                 '$message$' : 'Очень полезная статья'}) print body

Все динамические странички на сайте www.iso.ru формируются таким способом.

Работа с сервером баз данных MySQL

Практически любой сайт, содержащий элементы взаимодействия с пользователем использует какие-либо базы данных. Сайт www.iso.ru не является исключением. В базе данных хранятся новости, события, статьи журнала, информация по зарегистрированным пользователям и многое другое. Мы используем сервер баз данных MySQL.

На данный момент MySQL является наиболее популярной платформой для создания web-приложений, так как это простой и в тоже время довольно мощный и надежный SQL сервер. MySQL как и Python поддерживает широчайший спектр платформ, включая Linux и Windows NT. Для работы с MySQL в Python используется библиотека MySQLdb, существующая как для Linux, так и для Win32.

Работа с базой данных проходит достаточно стандартно. Сначала создается объект, устанавливающий соединение с БД (Connection Object):


mydb=MySQLdb.Connect(db='iso',host='localhost',
                     user='root',unix_socket='/tmp/mysql.sock')


Затем создается объект-курсор:


cursor = mydb.cursor()

После этого можно выполнять любые SQL запросы к базе данных:


cursor.execute('SELECT * FROM guestbook ORDER by date DESC')

Далее получаем результат запроса:


resultset = cursor.fetchall()

Метод fetchall возвращает кортеж записей, состоящих из кортежей полей, которые можно перебрать в цикле. Приведенная ниже программа выбирает из базы гостевой книги все сообщения автора "Иванов", подставляет их в шаблон и передает браузеру.


print 'Content-Type: text/html\n\n'
guestbook = '/usr/local/apache/cgi-bin/templates/guestbook.tmpl'
mydb=MySQLdb.Connect(db='iso',host='localhost',
                     user='root',unix_socket='/tmp/mysql.sock')
cursor.execute('SELECT date, author, massage FROM guestbook WHERE author="Иванов"')
resultset = cursor.fetchall()
body = ''
for row in resultset:
    body = body+replace_tmpl(guestbook, {'$date$' : row[0].strftime("%d-%m-%Y"),                                             '$author$' : str(row[1]),
                                            '$message$' : str(row[2]))
print body

Отправка форм

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


<form action="/cgi-bin/forms.cgi" METHOD="GET">
  Организация: <input type="Text" size="20" name="org">
  Ваше имя: <input type="Text" size="20" name="name">
  Ваш e-mail: <input type="Text" size="20" name="email">
  Тема: <input type="Text" size="20" name="tema">
  Сообщение: <textarea cols="20" rows="6" name="message"></textarea>
  <input type="reset" value="Очистить">$nbsp; $nbsp; $nbsp; $nbsp;
  <input type="submit" value="Отправить">
</form>

Для отправки сообщений по протоколу SMTP нужно использовать библиотеку smtplib. Определяя экземпляр класса SMTP, устанавливаем соединение с SMTP сервером:


import smtplib

mail=smtplib.SMTP("smtpserver.ru")

Формируем тело сообщения из полученных данных в соответствии с RFC822:


form = cgi.FieldStorage()
keys = {}
for k in form.keys():
    keys[k] = form[k].value
msg = """Subject: Новость\n                #тема сообщения


         From: Intersoft Web Server <admin@iso.ru>\n    #отправитель
         MIME-Version: 1.0\n              #версия MIME
         Content-Type: text/html\n\n      #тип сообщения
         <br>Имя: %s                      #тело сообщения
         <br>E-mail: %s
         <br>Тема: %s  
         <br>Сообщение: %s""" % \
(keys['name'], keys['email'], keys['tema'], keys['message'])

Затем вызываем метод sendmail для отсылки сообщения:


mail.sendmail( 'admin@iso.ru', 'market@iso.ru', msg )

Здесь admin@iso.ru - адрес отправителя, market@iso.ru - адрес получателя. Можно реализовать и более сложную функциональность с пересылкой вложенных файлов. Для начала в форму нужно добавить поле ввода file:


<input name="attach" type="file" size="12">

Для создания почтового сообщения с вложениями удобно пользоваться классом MimeWriter, определенным в модуле MimeWriter. Чтобы избежать загромождения тела программы, можно написать функцию, получающую на входе текст сообщения (text), содержание поля file формы (file), имя файла (name), тему письма (subj), адрес получателя (address) и отправляющую по этому адресу письмо с вложением. Отправителем в данном случае всегда является admin@iso.ru.

Функция представляет файл в кодировке base64 и конструирует многокомпонентный документ MIME, который отправляет по адресу address.

import mimetools, MimeWriter, StringIO, smtplib, cgi, os
#задаем каталог для временных файлов
temp_dir = '/tmp/'

def send_attach(text, file, name, subj, address):
    #полное имя временного файла
    tmp_file = temp_dir + name + '.txt'
    #имя загружаемого файла
    src_file = temp_dir + name
    #принимаем загружаемый файл
    infile = open(src_file, 'wb')
    #и записываем его в каталог для временных файлов
    infile.write(file)
    infile.close()
    #создаем экземпляр класса MimeWriter
    outfile = open(tmp_file, 'wb')
    mw = MimeWriter.MimeWriter(outfile)
    #создаем объект для записи многокомпонентного сообщения
    mw.startmultipartbody("mixed")
    #записываем заголовки
    mw.flushheaders()
    #создаем часть сообщения типа text/html и записываем туда text
    subpart = mw.nextpart()
    pout = subpart.startbody("text/html", [])
    pout.write(text)
    #создаем следующую часть сообщения
    subpart = mw.nextpart()
    #добавляем заголовок
    subpart.addheader('Content-transfer-encoding', 'base64')
    #определяем тип как application/octet-stream
    pout = subpart.startbody("application/octet-stream", [("name", name)])
    #открываем загружаемый файл и кодируем его в base64,
    #результат записываем в pout
    infile = open(src_file, "rb")
    mimetools.encode(infile,pout,'base64')
    infile.close()
    #завершаем многокомпонентное сообщение
    mw.lastpart()
    outfile.close()
 
    #далее посылаем стандартное сообщение с помощью класса SMTP,
    #тело сообщения читаем из сформированного ранее файла.
    f = open(tmp_file, 'rb')
    msg = f.read()
    f.close()
    mail=smtplib.SMTP("smtpserver.ru")
    out = StringIO.StringIO()
    out.write( "Subject: %sn" % subj )
    out.write( "From: %sn" % 'Intersoft Web Server ' )
    out.write( "MIME-Version: 1.0\n" )
    out.write( msg )
    mail.sendmail( 'market@iso.ru', address, out.getvalue() )
    out.close()
    #удаляем временные файлы
    os.unlink(src_file)
    os.unlink(tmp_file)


А вот пример использования этой функции:


send_attach(msg, form['attach'].value, form['attach'].filename.split('')[-1], 'Attachment', 'market@iso.ru')

Проверка корректности форм

Большинство CGI-скриптов работают с данными, полученными из форм. Для успешного использования этих данных необходимо проверить их корректность. В простейшем случае, это - просто проверка существования заполненного поля:


form = cgi.FieldStorage()
if form.has_key('keyname'):
    #действия с данными

В более сложных случаях, таких как проверка корректности введенного e-mail адреса требуется использование модуля re для сопоставления полученных данных с регулярными выражениями. Например, регулярное выражение [-_w0-9]+@[-_w0-9]+.w+ определяет e-mail адрес следующим образом: [-_w0-9] обозначает любую букву, цифру или знак "-" и "_", + - одно или более повторений предыдущего выражения, @ - "собака", . - точка, w+ - не менее одной буквы. Таким образом все адреса вида name@domain1.domain2 попадают под это определение.

Административный интерфейс

Для облегчения сопровождения сайта был разработан административный интерфейс - набор скриптов для контроля за контентом. Он состоит из следующих частей:

  1. Главный административный скрипт; скрипт позволяет:
    • добавить новость, событие, статью в журнал, создать новую тему для журнала и новый журнал со статьей;
    • редактировать (и удалять) новости, события, статьи журнала, темы журнала, атрибуты пользователей, комментарии к статьям.
  2. Скрипт управления конференциями; скрипт обеспечивает просмотр и удаление сообщений, добавление новых конференций.
  3. Скрипт выгрузки данных; скрипт отвечает за экспорт данных из таблиц в формат CSV.

Все скрипты достаточно жестко привязаны к структуре сайта, поэтому нет смысла подробно разбирать их. Хочу затронуть только один момент: удобно сделать отдельную таблицу для размещения информации о полях таблиц, используемых скриптами сайта. Это позволит писать универсальные функции администрирования для разных таблиц. Например, функция, реализующая вывод списка записей на нашем сайте построена следующим образом: на входе она получает имя таблицы, по нему читается информация о таблице из служебной таблицы (названия полей, выводить ли поле в списке и т.д.) и на основании полученной информации выводится список записей. Так как список, состоящий из всех полей, очень громоздкий (статья в журнале может занимать несколько страниц), в списке выводятся только те поля, для которых в служебной таблице стоит флаг вывода в списке. Благодаря этому добавление новых таблиц в базу требует лишь записи информации о них в служебную таблицу.