Анализ XML-документа
Для работы с готовым XML-документом нужно воспользоваться XML-анализаторами. Анализ XML-документа с порождением объекта класса Document происходит всего в одной строчке, с помощью функции parse(). Здесь стоит заметить, что кроме стандартного пакета xml можно поставить пакет PyXML или альтернативные коммерческие пакеты. Тем не менее, разработчики стараются придерживаться единого API, который продиктован стандартом DOM Level 2:
import xml.dom.minidom dom = xml.dom.minidom.parse("expression.xml")
dom.normalize()
def output_tree(node, level=0): if node.nodeType == node.TEXT_NODE: if node.nodeValue.strip(): print ". "*level, node.nodeValue.strip() else: # ELEMENT_NODE или DOCUMENT_NODE atts = node.attributes or {} att_string = ", ".join( ["%s=%s " % (k, v) for k, v in atts.items()]) print ". "*level, node.nodeName, att_string for child in node.childNodes: output_tree(child, level+1)
output_tree(dom)
В этом примере дерево выводится с помощью определенной функции output_tree(), которая принимает на входе узел и вызывается рекурсивно для всех вложенных узлов.
В результате получается примерно следующее:
#document . expression . . operation type=+ . . . operand . . . . 2 . . . operand . . . . operation type=* . . . . . operand . . . . . . 3 . . . . . operand . . . . . . 4
Здесь же применяется метод normalize() для того, чтобы все текстовые фрагменты были слиты воедино (в противном случае может следовать подряд несколько узлов с текстом).
Можно заметить, что даже в небольшом примере использовались атрибуты узлов: node.nodeType указывает тип узла, node.nodeValue применяется для доступа к данным, node.nodeName дает имя узла (соответствует названию тега), node.attributes дает доступ к атрибутам узла. node.childNodes применяется для доступа к дочерним узлам. Этих свойств достаточно, чтобы рекурсивно обойти дерево.
Все узлы являются экземплярами подклассов класса Node. Они могут быть следующих типов:
ELEMENT_NODE | Элемент | createElement(tagname) |
ATTRIBUTE_NODE | Атрибут | createAttribute(name) |
TEXT_NODE | Текстовый узел | createTextNode(data) |
CDATA_SECTION_NODE | Раздел CDATA | |
ENTITY_REFERENCE_NODE | Ссылка на сущность | |
ENTITY_NODE | Сущность | |
PROCESSING_INSTRUCTION_NODE | Инструкция по обработке | createProcessingInstruction(target, data) |
COMMENT_NODE | Комментарий | createComment(comment) |
DOCUMENT_NODE | Документ | |
DOCUMENT_TYPE_NODE | Тип документа | |
DOCUMENT_FRAGMENT_NODE | Фрагмент документа | |
NOTATION_NODE | Нотация |
В DOM документ является деревом, в узлах которого стоят объекты нескольких возможных типов. Узлы могут иметь атрибуты или данные. Доступ к узлам можно осуществлять через атрибуты вроде childNodes (дочерние узлы), firstChild (первый дочерний узел), lastChild (последний дочерний узел), parentNode (родитель), nextSibling (следующий брат), previousSibling (предыдущий брат).
Выше уже говорилось о методе appendChild(). К нему можно добавить методы insertBefore(newChild, refChild) (вставить newChild до refChild), removeChild(oldChild) (удалить дочерний узел), replaceChild(newChild, oldChild) (заметить oldChild на newChild). Есть еще метод cloneNode(deep), который клонирует узел (вместе с дочерними узлами, если задан deep=1).
Узел типа ELEMENT_NODE, помимо перечисленных методов "просто" узла, имеет много других методов. Вот основные из них:
tagName
Имя типа элемента.
getElementsByTagName(tagname)
Получает элементы с указанным именем tagname среди всех потомков данного элемента.
getAttribute(attname)
Получить значение атрибута с именем attname.
getAttributeNode(attrname)
Возвращает атрибут с именем attrname в виде объекта-узла.
removeAttribute(attname)
Удалить атрибут с именем attname.
removeAttributeNode(oldAttr)
Удалить атрибут oldAttr (задан в виде объекта-узла).
setAttribute(attname, value)
Устанавливает значение атрибута attname равным строке value.
setAttributeNode(newAttr)
Добавляет новый узел-атрибут к элементу. Старый атрибут заменяется, если имеет то же имя.
Здесь стоит заметить, что атрибуты в рамках элемента повторяться не должны. Их порядок также не важен с точки зрения информационной модели XML.
В качестве упражнения предлагается составить функцию, которая будет вычислять значение выражения, заданного в XML-представлении.
Формат CSV
Файл в формате CSV (comma-separated values - значения, разделенные запятыми) - универсальное средство для переноса табличной информации между приложениями (электронными таблицами, СУБД, адресными книгами и т.п.). К сожалению, формат файла не имеет строго определенного стандарта, поэтому между файлами, порождаемыми различными приложениями, существуют некоторые тонкие различия. Внутри файл выглядит примерно так (файл pr.csv):
name,number,text a,1,something here b,2,"one, two, three" c,3,"no commas here"
Для работы с CSV-файлами имеются две основные функции:
reader(csvfile[, dialect='excel'[, fmtparam]])
Возвращает читающий объект, который является итератором по всем строкам заданного файла. В качестве csvfile может выступать любой объект, который поддерживает протокол итератора и возвращает строку при обращении к его методу next(). Необязательный аргумент dialect, по умолчанию равный 'excel', указывает на необходимость использования того или иного набора свойств. Узнать доступные варианты можно с помощью csv.list_dialects(). Аргумент может быть одной из строк, возвращаемых указанной функцией, либо экземпляром подкласса класса csv.Dialect. Необязательный аргумент fmtparam служит для переназначения отдельных свойств по сравнению с заданным параметром dialect набором. Все получаемые данные являются строками.
writer(csvfile[, dialect='excel'[, fmtparam]])
Возвращает пишущий объект для записи пользовательских данных с использованием разделителя в заданный файлоподобный объект. Параметры dialect и fmtparam имеют тот же смысл, что и выше. Все данные, кроме строк, обрабатывают функцией str() перед помещением в файл.
В следующем примере читается CSV-файл и записывается другой, где числа второго столбца увеличены на единицу:
import csv input_file = open("pr.csv", "rb") rdr = csv.reader(input_file) output_file = open("pr1.csv", "wb") wrtr = csv.writer(output_file) for rec in rdr: try: rec[1] = int(rec[1]) + 1 except: pass wrtr.writerow(rec) input_file.close() output_file.close()
В результате получится файл pr1.csv следующего содержания:
name,number,text a,2,something here b,3,"one, two, three" c,4,no commas here
Модуль также определяет два класса для более удобного чтения и записи значений с использованием словаря. Вызовы конструкторов следующие:
class DictReader(csvfile, fieldnames[, restkey=None[, restval=None[, dialect='excel']]]])
Создает читающий объект, подобный тому, что рассматривался выше, но помещающий считываемые значения в словарь. Параметры csvfile и dialect те же, что и раньше. Параметр fieldnames задает имена полей списком. Параметр restkey задает значение ключа для помещения списка значений, для которых не хватило имен полей. Параметр restval используется как значение в том случае, если в записи не хватает значений для всех полей. Если параметр fieldnames не задан, имена полей будут прочитаны из первой записи CSV-файла. Начиная с Python 2.4, параметр fieldnames необязателен. Если он отсутствует, ключи берутся из первой строки CSV-файла.
class DictWriter(csvfile, fieldnames[, restval=""[, extrasaction='raise'[, dialect='excel']]])
Создает пишущий объект, который записывает в CSV-файл строки, получая данные из словаря. Параметры аналогичны DictReader, но fieldnames обязателен, так как он задает порядок следования полей. Параметр extrasaction указывает на то, какое действие нужно произвести в случае, когда требуемого значения нет в словаре: 'raise' - возбудить исключение ValueError, 'ignore' - игнорировать.
Соответствующий пример дан ниже. В файле pr.csv имена полей заданы в первой строке файла, поэтому можно не задавать fieldnames:
import csv input_file = open("pr.csv", "rb") rdr = csv.DictReader(input_file, fieldnames=['name', 'number', 'text']) output_file = open("pr1.csv", "wb") wrtr = csv.DictWriter(output_file, fieldnames=['name', 'number', 'text']) for rec in rdr: try: rec['number'] = int(rec['number']) + 1 except: pass wrtr.writerow(rec) input_file.close() output_file.close()
Модуль имеет также другие классы и функции, которые можно изучить по документации. На примере этого модуля можно увидеть общий подход к работе с файлом в некотором формате. Следует обратить внимание на следующие моменты:
Модули для работы с форматами данных обычно содержат функции или конструкторы классов, в частности Reader и Writer.Эти функции и конструкторы возвращают объекты-итераторы для чтения данных из файла и объекты со специальными методами для записи в файл.Для разных нужд обычно требуется иметь несколько вариантов классов читающих и пишущих объектов. Новые классы могут получаться наследованием от базовых классов либо обертыванием функций, предоставляемых модулем расширения (написанным на C). В приведенном примере DictReader и DictWriter являются обертками для функций reader() и writer() и объектов, которые они порождают.
Формирование сообщения
Часто возникает ситуация, когда нужно сформировать сообщение с вложенным файлом. В следующем примере строится сообщение с текстом и вложением. В качестве класса для порождения сообщения можно использовать не только Message из модуля email.Message, но и MIMEMultipart из email.MIMEMultipart (для сообщений из нескольких частей), MIMEImage (для сообщения с графическим изображением), MIMEAudio (для аудиофайлов), MIMEText (для текстовых частей):
# Загружаются необходимые модули и функции из модулей from email.Header import make_header as mkh from email.MIMEMultipart import MIMEMultipart from email.MIMEText import MIMEText from email.MIMEBase import MIMEBase from email.Encoders import encode_base64
# Создается главное сообщение и задаются некоторые поля msg = MIMEMultipart() msg["Subject"] = mkh([("Привет", "koi8-r")]) msg["From"] = mkh([("Друг", "koi8-r"), ("<friend@mail.ru>", "us-ascii")]) msg["To"] = mkh([("Друг2", "koi8-r"), ("<friend2@yandex.ru>", "us-ascii")])
# То, чего будет не видно, если почтовая программа поддерживает MIME msg.preamble = "Multipart message" msg.epilogue = ""
# Текстовая часть сообщения text = u"""К письму приложен файл с архивом.""".encode("koi8-r") to_attach = MIMEText(text, _charset="koi8-r") msg.attach(to_attach)
# Прикладывается файл fp = open("archive_file.zip", "rb") to_attach = MIMEBase("application", "octet-stream") to_attach.set_payload(fp.read()) encode_base64(to_attach) to_attach.add_header("Content-Disposition", "attachment", filename="archive_file.zip") fp.close() msg.attach(to_attach)
print msg.as_string()
В этом примере видно сразу несколько модулей пакета email. Функция make_header() из email.Header позволяет закодировать содержимое для заголовка:
>>> from email.Header import make_header >>> print make_header([("Друг", "koi8-r"), ("<friend@mail.ru>", "us-ascii")]) =?koi8-r?b?5NLVxw==?= <friend@mail.ru> >>> print make_header([(u"Друг", ""), ("<friend@mail.ru>", "us-ascii")]) =?utf-8?b?w6TDksOVw4c=?= <friend@mail.ru>
Функция email.Encoders.encode_base64() воздействует на переданное ей сообщение и кодирует тело с помощью base64. Другие варианты: encode_quopri() - кодировать quoted printable, encode_7or8bit() - оставить семь или восемь бит. Эти функции добавляют необходимые поля.
Аргументы конструкторов классов из MIME-модулей пакета email:
class MIMEBase(_maintype, _subtype, **_params)
Базовый класс для всех использующих MIME сообщений (подклассов Message). Тип содержимого задается через _maintype и _subtype.
class MIMENonMultipart()
Подкласс для MIMEBase, в котором запрещен метод attach(), отчего он гарантированно состоит из одной части.
class MIMEMultipart([_subtype[, boundary[, _subparts[, _params]]]])
Подкласс для MIMEBase, который является базовым для MIME-сообщений из нескольких частей. Главный тип multipart, подтип указывается с помощью _subtype.
class MIMEAudio(_audiodata[, _subtype[, _encoder[, **_params]]])
Подкласс MIMENonMultipart. Используется для создания MIME-сообщений, содержащих аудио данные. Главный тип - audio, подтип указывается с помощью _subtype. Данные задаются параметром _audiodata.
class MIMEImage(_imagedata[, _subtype[, _encoder[, **_params]]])
Подкласс MIMENonMultipart. Используется для создания MIME-сообщений с графическим изображением. Главный тип - image, подтип указывается с помощью _subtype. Данные задаются параметром _imagedata.
class MIMEMessage(_msg[, _subtype])
Подкласс MIMENonMultipart для класса MIMENonMultipart используется для создания MIME-объектов с главным типом message. Параметр _msg применяется в качестве тела и должен являться экземпляром класса Message или его потомков.Подтип задается с помощью _subtype, по умолчанию 'rfc822'.
class MIMEText(_text[, _subtype[, _charset]])
Подкласс MIMENonMultipart. Используется для создания MIME-сообщений текстового типа. Главный тип - text, подтип указывается с помощью _subtype. Данные задаются параметром _text. Посредством _charset можно указать кодировку (по умолчанию 'us-ascii').
Формирование XML-документа
Концептуально существуют два пути обработки XML-документа: последовательная обработка и работа с объектной моделью документа.
В первом случае обычно используется SAX (Simple API for XML, простой программный интерфейс для XML). Работа SAX заключается в чтении источников данных (input source) XML-анализаторами (XML-reader) и генерации последовательности событий (events), которые обрабатываются объектами-обработчиками (handlers). SAX дает последовательный доступ к XML-документу.
Во втором случае анализатор XML строит DOM (Document Object Model, объектная модель документа), предлагая для XML-документа конкретную объектную модель. В рамках этой модели узлы DOM-дерева доступны для произвольного доступа,а для переходов между узлами предусмотрен ряд методов.
Можно применить оба этих подхода для формирования приведенного выше XML-документа.
С помощью SAX документ сформируется так:
import sys from xml.sax.saxutils import XMLGenerator g = XMLGenerator(sys.stdout) g.startDocument() g.startElement("expression", {}) g.startElement("operation", {"type": "+"}) g.startElement("operand", {}) g.characters("2") g.endElement("operand") g.startElement("operand", {}) g.startElement("operation", {"type": "*"}) g.startElement("operand", {}) g.characters("3") g.endElement("operand") g.startElement("operand", {}) g.characters("4") g.endElement("operand") g.endElement("operation") g.endElement("operand") g.endElement("operation") g.endElement("expression") g.endDocument()
Построение дерева объектной модели документа может выглядеть, например, так:
from xml.dom import minidom dom = minidom.Document() e1 = dom.createElement("expression") dom.appendChild(e1) p1 = dom.createElement("operation") p1.setAttribute('type', '+') x1 = dom.createElement("operand") x1.appendChild(dom.createTextNode("2")) p1.appendChild(x1) e1.appendChild(p1) p2 = dom.createElement("operation") p2.setAttribute('type', '*') x2 = dom.createElement("operand") x2.appendChild(dom.createTextNode("3")) p2.appendChild(x2) x3 = dom.createElement("operand") x3.appendChild(dom.createTextNode("4")) p2.appendChild(x3) x4 = dom.createElement("operand") x4.appendChild(p2) p1.appendChild(x4) print dom.toprettyxml()
Легко заметить, что при использовании SAX команды на генерацию тегов и других частей выдаются последовательно, а вот построение одной и той же DOM можно выполнять различными последовательностями команд формирования узла и его соединения с другими узлами.
Конечно, указанные примеры носят довольно теоретический характер, так как на практике строить XML-документы таким образом обычно не приходится.
Язык XML
В рамках одной лекции довольно сложно объяснить, что такое XML, и то, как с ним работать. В примерах используется входящий в стандартную поставку пакет xml.
XML (Extensible Markup Language, расширяемый язык разметки) позволяет налаживать взаимодействие между приложениями различных производителей, хранить и подвергать обработке сложно структурированные данные.
Язык XML (как и HTML) является подмножеством SGML, но его применения не ограничены системой WWW. В XML можно создавать собственные наборы тегов для конкретной предметной области. В XML можно хранить и подвергать обработке базы данных и знаний, протоколы взаимодействия между объектами, описания ресурсов и многое другое.
Новичкам не всегда понятно, зачем нужно использовать такой достаточно многословный формат, когда можно создать свой, компактный формат для хранения тех же самых данных. Преимущество XML состоит в том, что вместе с данными он хранит и контекстную информацию: теги и их атрибуты имеют имена. Немаловажно также, что XML сегодня - единый общепринятый стандарт, для которого создано немало инструментальных средств.
Говоря об XML, надо иметь в виду, что XML-документы бывают формально-правильными (well-formed) и состоятельными (valid). Состоятельный XML-документ - это формально-правильный XML-документ, имеющий объявление типа документа (DTD, Document Type Definition). Объявление типа документа задает грамматику, которой текст документа на XML должен удовлетворять. Для простоты изложения здесь не будет рассматриваться DTD, предпочтительнее ограничиться формально-правильными документами.
Для представления букв и других символов XML использует Unicode, что сокращает проблемы с представлением символов различных алфавитов. Однако это обстоятельство необходимо помнить и не употреблять в XML восьмибитную кодировку (во всяком случае, без явного указания).
Следующий пример достаточно простого XML-документа дает представление об этом формате (файл expression.xml):
<?xml version="1.0" encoding="iso-8859-1"?> <expression> <operation type="+"> <operand>2</operand> <operand> <operation type="*"> <operand>3</operand> <operand>4</operand> </operation> </operand> </operation> </expression>
XML-документ всегда имеет структуру дерева, в корне которого сам документ. Его части, описываемые вложенными парами тегов, образуют узлы. Таким образом, ребра дерева обозначают "непосредственное вложение". Атрибуты тега можно считать листьями, как и наиболее вложенные части, не имеющие в своем составе других частей. Получается, что документ имеет древесную структуру.
Примечание: Следует заметить, что в отличие от HTML, в XML одиночные (непарные) теги записываются с косой чертой: <BR/>, а атрибуты - в кавычках. В XML имеет значение регистр букв в названиях тегов и атрибутов. |
Пакет email
Модули пакета email помогут разобрать, изменить и сгенерировать сообщение в формате RFC 2822. Наиболее часто RFC 2822 применяется в сообщениях электронной почты в Интернете.
В пакете есть несколько модулей, назначение которых (кратко) указано ниже:
Message
Модуль определяет класс Message - основной класс для представления сообщения в пакете email.
Parser
Модуль для разбора представленного в виде текста сообщения с получением объектной структуры сообщения.
Header
Модуль для работы с полями, в которых используется кодировка, отличная от ASCII.
Generator
Порождает текст сообщения RFC 2822 на основании объектной модели.
Utils
Различные утилиты, которые решают разнообразные небольшие задачи, связанные с сообщениями.
В пакете есть и другие модули, которые здесь рассматриваться не будут.
Пространства имен
Еще одной интересной особенностью XML, о которой нельзя не упомянуть, являются пространства имен. Они позволяют составлять XML-документы из кусков различных схем. Например, таким образом в XML-документ можно включить кусок HTML, указав во всех элементах HTML принадлежность особому пространству имен.
Следующий пример XML-кода показывает синтаксис пространств имен (файл foaf.rdf):
<?xml version="1.0" encoding="UTF-8"?> <rdf:RDF xmlns:dc="http://http://purl.org/dc/elements/1.1/" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" xmlns:foaf="http://xmlns.com/foaf/0.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" > <rdf:Description rdf:nodeID="_:jCBxPziO1"> <foaf:nick>donna</foaf:nick> <foaf:name>Donna Fales</foaf:name> <rdf:type rdf:resource="http://xmlns.com/foaf/0.1/Person"/> </rdf:Description> </rdf:RDF>
Примечание: Пример позаимствован из пакета cwm, созданного командой разработчиков во главе с Тимом Бернерс-Ли, создателем технологии WWW. Кстати, cwm тоже написан на Python. Пакет cwm служит обработчиком данных общего назначения для семантической сети - новой идеи, продвигаемой Тимом Бернерс-Ли. Коротко суть идеи состоит в том, чтобы сделать современный "веб" много полезнее, формализовав знания в виде распределенной базы XML-документов, по аналогии с тем как WWW представляет собой распределенную базу документов. Отличие глобальной семантической сети от WWW в том, что она даст машинам возможность обрабатывать знания, делая логические выводы на основании заложенной в документах информации. |
Названия пространств имен следуют в виде префиксов к названиям элементов. Эти названия - не просто имена. Они соответствуют идентификаторам, которые должны быть заданы в виде URI (Universal Resource Locator, универсальный указатель ресурса). В примере выше упоминаются пять пространств имен (xmlns, dc, rdfs, foaf и rdf), из которых только первое не требует объявления, так как является встроенным.
Из них реально использованы только три: (xmlns, foaf и rdf).
Пространства имен позволяют выделять из XML-документа части, относящиеся к различным схемам, что важно для тех инструментов, которые интерпретируют XML.
В пакете xml есть методы, понимающие механизм пространств имен. Обычно такие методы и атрибуты имеют в своем имени буквы NS.
Получить URI, который соответствует пространству имен данного элемента, можно с помощью атрибута namespaceURI.
В следующем примере печатается URI элементов:
import xml.dom.minidom dom = xml.dom.minidom.parse("ex.xml")
def output_ns(node): if node.nodeType == node.ELEMENT_NODE: print node.nodeName, node.namespaceURI for child in node.childNodes: output_ns(child)
output_ns(dom)
Пример выведет:
rdf:RDF http://www.w3.org/1999/02/22-rdf-syntax-ns# rdf:Description http://www.w3.org/1999/02/22-rdf-syntax-ns# foaf:nick http://xmlns.com/foaf/0.1/ foaf:name http://xmlns.com/foaf/0.1/ rdf:type http://www.w3.org/1999/02/22-rdf-syntax-ns#
Следует заметить, что указание пространства имен может быть сделано для имен не только элементов, но и атрибутов.
Подробнее узнать о работе с пространствами имен в xml-пакетах для Python можно из документации.
Разбор поля заголовка
В примере выше поле Subject формировалось с помощью email.Header.make_header(). Разбор поля поможет провести другая функция: email.Header.decode_header(). Эта функция возвращает список кортежей, в каждом из них указан кусочек текста поля и кодировка, в которой этот текст был задан. Следующий пример поможет понять суть дела:
subj = """=?koi8-r?Q?=FC=D4=CF_=D0=D2=C9=CD=C5=D2_=CF=DE=C5=CE=D8_=C4=CC=C9?= =?koi8-r?Q?=CE=CE=CF=C7=CF_=28164_bytes=29_=D0=CF=CC=D1_=D3_=D4?= =?koi8-r?Q?=C5=CD=CF=CA_=D3=CF=CF=C2=DD=C5=CE=C9=D1=2E_=EF=CE=CF_?= =?koi8-r?Q?=D2=C1=DA=C2=C9=CC=CF=D3=D8_=CE=C1_=CB=D5=D3=CB=C9_=D7?= =?koi8-r?Q?_=D3=CF=CF=C2=DD=C5=CE=C9=C9=2C_=CE=CF_=CC=C5=C7=CB=CF?= =?koi8-r?Q?_=D3=CF=C2=C9=D2=C1=C5=D4=D3=D1_=D7_=D4=C5=CB=D3=D4_?= =?koi8-r?Q?=D3_=D0=CF=CD=CF=DD=D8=C0_email=2EHeader=2Edecode=5Fheader?= =?koi8-r?Q?=28=29?=""" import email.Header for text, enc in email.Header.decode_header(subj): print enc, text
В результате будет выведено:
koi8-r Это пример очень длинного (164 bytes) поля с темой сообщения. Оно разбилось на куски в сообщении, но легко собирается в текст с помощью email.Header.decode_header()
Следует заметить, что кодировку можно не указывать:
>>> email.Header.decode_header("simple text") [('simple text', None)] >>> email.Header.decode_header("пример") [('\xd0\xd2\xc9\xcd\xc5\xd2', None)] >>> email.Header.decode_header("=?KOI8-R?Q?=D0=D2=CF_?=Linux") [('\xd0\xd2\xcf ', 'koi8-r'), ('Linux', None)]
Если в первом случае можно подразумевать us-ascii, то во втором случае о кодировке придется догадываться: вот почему в электронных письмах нельзя просто так использовать восьмибитные кодировки. В третьем примере русские буквы закодированы, а латинские - нет, поэтому в результате email.Header.decode_header() список из двух пар.
В общем случае представить поле сообщения можно только в Unicode. Создание функции для такого преобразования предлагается в качестве упражнения.
Разбор сообщения. Класс Message
Класс Message - центральный во всем пакете email. Он определяет методы для работы с сообщением, которое состоит из заголовка (header) и тела (payload). Поле заголовка имеет название и значение, разделенное двоеточием (двоеточие не входит ни в название, ни в значение). Названия полей нечувствительны к регистру букв при поиске значения, хотя хранятся с учетом регистра. В классе также определены методы для доступа к некоторым часто используемым сведениям (кодировке сообщения, типу содержимого и т.п.).
Следует заметить, что сообщение может иметь одну или несколько частей, в том числе вложенных друг в друга. Например, сообщение об ошибке доставки письма может содержать исходное письмо в качестве вложения.
Пример наиболее употребительных методов экземпляров класса Message с пояснениями:
>>> import email >>> input_file = open("pr1.eml") >>> msg = email.message_from_file(input_file)
Здесь используется функция email.message_from_file() для чтения сообщения из файла pr1.eml. Сообщение можно получить и из строки с помощью функции email.message_from_string(). А теперь следует произвести некоторые операции над этим сообщением (не стоит обращать внимания на странные имена - сообщение было взято из папки СПАМ). Доступ к полям по имени осуществляется так:
>>> print msg['from'] "felton olive" <zinakinch@thecanadianteacher.com> >>> msg.get_all('received') ['from mail.onego.ru\n\tby localhost with POP3 (fetchmail-6.2.5 polling mail.onego.ru account spam)\n\tfor spam@localhost (single-drop); Wed, 01 Sep 2004 15:46:33 +0400 (MSD)', 'from thecanadianteacher.com ([222.65.104.100])\n\tby mail.onego.ru (8.12.11/8.12.11) with SMTP id i817UtUN026093;\n\tWed, 1 Sep 2004 11:30:58 +0400']
Стоит заметить, что в электронном письме может быть несколько полей с именем received (в этом примере их два).
Некоторые важные данные можно получить в готовом виде, например, тип содержимого, кодировку:
>>> msg.get_content_type() 'text/plain' >>> print msg.get_main_type(), msg.get_subtype() text plain >>> print msg.get_charset() None >>> print msg.get_params() [('text/plain', ''), ('charset', 'us-ascii')] >>> msg.is_multipart() False
или список полей:
>>> print msg.keys() ['Received', 'Received', 'Message-ID', 'Date', 'From', 'User-Agent', 'MIME-Version', 'To', 'Subject', 'Content-Type', 'Content-Transfer-Encoding', 'Spam', 'X-Spam']
Так как сообщение состоит из одной части, можно получить его тело в виде строки:
>>> print msg.get_payload() sorgeloosheid hullw ifesh nozama decompresssequenceframes
Believe it or not, I have tried several sites to b"_"uy presription medication. I should say that currently you are still be the best amony ...
Теперь будет рассмотрен другой пример, в котором сообщение состоит из нескольких частей. Это сообщение порождено вирусом. Оно состоит из двух частей: HTML-текста и вложенного файла с расширением cpl. Для доступа к частям сообщения используется метод walk(), который обходит все его части. Попутно следует собрать типы содержимого (в списке parts), поля Content-Type (в ct_fields) и имена файлов (в filenames):
import email parts = [] ct_fields = [] filenames = [] f = open("virus.eml") msg = email.message_from_file(f) for submsg in msg.walk(): parts.append(submsg.get_content_type()) ct_fields.append(submsg.get('Content-Type', '')) filenames.append(submsg.get_filename()) if submsg.get_filename(): print "Длина файла:", len(submsg.get_payload()) f.close() print parts print ct_fields print filenames
В результате получилось:
Длина файла: 31173 ['multipart/mixed', 'text/html', 'application/octet-stream'] ['multipart/mixed;\n boundary="--------hidejpxkblmvuwfplzue"', 'text/html; charset="us-ascii"', 'application/octet-stream; name="price.cpl"'] [None, None, 'price.cpl']
Из списка parts можно увидеть, что само сообщение имеет тип multipart/mixed, тогда как две его части - text/html и application/octet-stream соответственно. Только с последней частью связано имя файла (price.cpl). Файл читается методом get_payload() и вычисляется его длина.
Кстати, в случае, когда сообщение является контейнером для других частей, get_payload() выдает список объектов-сообщений (то есть экземпляров класса Message).
В этой лекции были рассмотрены
В этой лекции были рассмотрены варианты обработки текстовой информации трех достаточно распространенных форматов: CSV, Unix mailbox и XML. Конечно, форматов данных, даже основанных на тексте, гораздо больше, однако то, что было представлено, поможет быстрее разобраться с любым модулем для обработки формата или построить свой модуль так, чтобы другие могли понять ваши намерения.
CGI-сценарии
Классический путь создания приложений для WWW - написание CGI-сценариев (иногда говорят - скриптов). CGI (Common Gateway Interface, общий шлюзовой интерфейс) - это стандарт, регламентирующий взаимодействие сервера с внешними приложениями. В случае с WWW, web-сервер может направить запрос на генерацию страницы по определенному сценарию. Этот сценарий, получив на вход данные от web-сервера (тот, в свою очередь, мог получить их от пользователя), генерирует готовый объект (изображение, аудиоданные, таблицу стилей и т.п.).
При вызове сценария Web-сервер передает ему информацию через стандартный ввод, переменные окружения и, для ISINDEX, через аргументы командной строки (они доступны через sys.argv).
Два основных метода передачи данных из заполненной в браузере формы Web-серверу (и CGI-сценарию) - GET и POST. В зависимости от метода данные передаются по-разному. В первом случае они кодируются и помещаются прямо в URL, например: http://host/cgi-bin/a.cgi?a=1&b=3. Сценарий получает их в переменной окружения с именем QUERY_STRING. В случае метода POST они передаются на стандартный ввод.
Для корректной работы сценарии помещаются в предназначенный для этого каталог на web-сервере (обычно он называется cgi-bin) или, если это разрешено конфигурацией сервера, в любом месте среди документов HTML. Сценарий должен иметь признак исполняемости. В системе Unix его можно установить с помощью команды chmod a+x.
Следующий простейший сценарий выводит значения из словаря os.environ и позволяет увидеть, что же было ему передано:
#!/usr/bin/python
import os print """Content-Type: text/plain
%s""" % os.environ
С помощью него можно увидеть установленные Web-сервером переменные окружения. Выдаваемый CGI-сценарием web-серверу файл содержит заголовочную часть, в которой указаны поля с мета-информацией (тип содержимого, время последнего обновления документа, кодировка и т.п.).
Основные переменные окружения, достаточные для создания сценариев:
QUERY_STRING
Строка запроса.
REMOTE_ADDR
IP-адрес клиента.
REMOTE_USER
Имя клиента (если он был идентифицирован).
SCRIPT_NAME
Имя сценария.
SCRIPT_FILENAME
Имя файла со сценарием.
SERVER_NAME
Имя сервера.
HTTP_USER_AGENT
Название броузера клиента.
REQUEST_URI
Строка запроса (URI).
HTTP_ACCEPT_LANGUAGE
Желательный язык документа.
Вот что может содержать словарь os.environ в CGI-сценарии:
{ 'DOCUMENT_ROOT': '/var/www/html', 'SERVER_ADDR': '127.0.0.1', 'SERVER_PORT': '80', 'GATEWAY_INTERFACE': 'CGI/1.1', 'HTTP_ACCEPT_LANGUAGE': 'en-us, en;q=0.50', 'REMOTE_ADDR': '127.0.0.1', 'SERVER_NAME': 'rnd.onego.ru', 'HTTP_CONNECTION': 'close', 'HTTP_USER_AGENT': 'Mozilla/5.0 (X11; U; Linux i586; en-US; rv:1.0.1) Gecko/20021003', 'HTTP_ACCEPT_CHARSET': 'ISO-8859-1, utf-8;q=0.66, *;q=0.66', 'HTTP_ACCEPT': 'text/xml,application/xml,application/xhtml+xml, text/html;q=0.9,text/plain;q=0.8,video/x-mng,image/png,image/jpeg, image/gif;q=0.2,text/css,*/*;q=0.1', 'REQUEST_URI': '/cgi-bin/test.py?a=1', 'PATH': '/sbin:/usr/sbin:/bin:/usr/bin:/usr/X11R6/bin', 'QUERY_STRING': 'a=1&b=2', 'SCRIPT_FILENAME': '/var/www/cgi-bin/test.py', 'HTTP_KEEP_ALIVE': '300', 'HTTP_HOST': 'localhost', 'REQUEST_METHOD': 'GET', 'SERVER_SIGNATURE': 'Apache/1.3.23 Server at rnd.onego.ru Port 80', 'SCRIPT_NAME': '/cgi-bin/test.py', 'SERVER_ADMIN': 'root@localhost', 'SERVER_SOFTWARE': 'Apache/1.3.23 (Unix) (Red-Hat/Linux) mod_python/2.7.8 Python/1.5.2 PHP/4.1.2', 'SERVER_PROTOCOL': 'HTTP/1.0', 'REMOTE_PORT': '39251' }
Следующий CGI-сценарий выдает черный квадрат (в нем используется модуль Image для обработки изображений):
#!/usr/bin/python
import sys print """Content-Type: image/jpeg """
import Image i = Image.new("RGB", (10,10)) i.im.draw_rectangle((0,0,10,10), 1) i.save(sys.stdout, "jpeg")
Что после CGI?
К сожалению, строительство интерактивного и посещаемого сайта на основе CGI имеет свои ограничения, главным образом, связанные с производительностью. Ведь для каждого запроса нужно вызвать как минимум один сценарий (а значит - запустить интерпретатор Python), из него, возможно, сделать соединение с базой данных и т.д. Время запуска интерпретатора Python достаточно невелико, тем не менее, на занятом сервере оно может оказывать сильное влияние на загрузку процессора.
Желательно, чтобы интерпретатор уже находился в оперативной памяти, и были доступны соединения с базой данных.
Такие технологии существуют и обычно опираются на модули, встраиваемые в web-сервер.
Для ускорения работы CGI используются различные схемы, например, FastCGI или PCGI (Persistent CGI). В данной лекции предлагается к рассмотрению специальным модуль для web-сервера Apache, называемый mod_python.
Пусть модуль установлен на web-сервере в соответствии с инструкциями, данными в его документации.
Модуль mod_python позволяет сценарию-обработчику вклиниваться в процесс обработки HTTP-запроса сервером Apache на любом этапе, для чего сценарий должен иметь определенным образом названные функции.
Сначала нужно выделить каталог, в котором будет работать сценарий-обработчик. Пусть это каталог /var/www/html/mywebdir. Для того чтобы web-сервер знал, что в этом каталоге необходимо применять mod_python, следует добавить в файл конфигурации Apache следующие строки:
<Directory "/var/www/html/mywebdir"> AddHandler python-program .py PythonHandler mprocess </Directory>
После этого необходимо перезапустить web-сервер и, если все прошло без ошибок, можно приступать к написанию обработчика mprocess.py. Этот сценарий будет реагировать на любой запрос вида http://localhost/*.py.
Следующий сценарий mprocess.py выведет в браузере страницу со словами Hello, world!:
from mod_python import apache
def handler(req): req.content_type = "text/html" req.send_http_header() req.write("""<HTML><HEAD><TITLE>Hello, world!</TITLE></HEAD> <BODY>Hello, world!</BODY></HTML>""") return apache.OK
Отличия сценария-обработчика от CGI-сценария:
Сценарий- обработчик не запускается при каждом HTTP-запросе: он уже находится в памяти, и из него вызываются необходимые функции-обработчики (в приведенном примере такая функция всего одна - handler()). Каждый процесс-потомок web-сервера может иметь свою копию сценария и интерпретатора Python.Как следствие п.1 различные HTTP-запросы делят одни и те же глобальные переменные. Например, таким образом можно инициализировать соединение с базой данных и применять его во всех запросах (хотя в некоторых случаях потребуются блокировки, исключающие одновременное использование соединения разными потоками (нитями) управления).Обработчик задействуется при обращении к любому "файлу" с расширением py, тогда как CGI-сценарий обычно запускается при обращении по конкретному имени.В сценарии-обработчике нельзя рассчитывать на то, что он увидит модули, расположенные в том же каталоге. Возможно, придется добавить некоторые каталоги в sys.path.Текущий рабочий каталог (его можно узнать с помощью функции os.getcwd()) также не находится в одном каталоге с обработчиком.#!-строка в первой строке сценария не определяет версию интерпретатора Python. Работает версия, для которой был скомпилирован mod_python.Все необходимые параметры передаются в обработчик в виде Request-объекта. Возвращаемые значения также передаются через этот объект.Web-сервер замечает, что сценарий-обработчик изменился, но не заметит изменений в импортируемых в него модулях. Команда touch mprocess.py обновит дату изменения файла сценария.Отображение os.environ в обработчике может быть обрезанным. Кроме того, вызываемые из сценария-обработчика другие программы его не наследуют, как это происходит при работе с CGI-сценариями. Переменные можно получить другим путем: req.add_common_vars(); params = req.subprocess_env.Так как сценарий-обработчик не является "одноразовым", как CGI-сценарий, из-за ошибок программирования (как самого сценария, так и других компонентов) могут возникать утечки памяти (программа не освобождает ставшую ненужной память).
Следует установить значение параметра MaxRequestsPerChild (максимальное число запросов, обрабатываемое одним процессом-потомком) больше нуля.
Другой возможный обработчик - сценарий идентификации:
def authenhandler(req): password = req.get_basic_auth_pw() user = req.connection.user if user == "user1" and password == "secret": return apache.OK else: return apache.HTTP_UNAUTHORIZED
Эту функцию следует добавить в модуль mprocess.py, который был рассмотрен ранее. Кроме того, нужно дополнить конфигурацию, назначив обработчик для запросов идентификации (PythonAuthenHandler), а также обычные для Apache директивы AuthType, AuthName, require, определяющие способ авторизации:
<Directory "/var/www/html/mywebdir"> AddHandler python-program .py PythonHandler mprocess PythonAuthenHandler mprocess AuthType Basic AuthName "My page" require valid-user </Directory>
Разумеется, это - всего лишь пример. В реальности идентификация может быть устроена намного сложнее.
Другие возможные обработчики (по документации к mod_python можно уточнить, в какие моменты обработки запроса они вызываются):
PythonPostReadRequestHandler
Обработка полученного запроса сразу после его получения.
PythonTransHandler
Позволяет изменить URI запроса (в том числе имя виртуального сервера).
PythonHeaderParserHandler
Обработка полей запроса.
PythonAccessHandler
Обработка ограничений доступа (например, по IP-адресу).
PythonAuthenHandler
Идентификация пользователя.
PythonTypeHandler
Определение и/или настройка типа документа, языка и т.д.
PythonFixupHandler
Изменение полей непосредственно перед вызовом обработчиков содержимого.
PythonHandler
Основной обработчик запроса.
PythonInitHandler
PythonPostReadRequestHandler или PythonHeaderParserHandler в зависимости от нахождения в конфигурации web-сервера.
PythonLogHandler
Управление записью в логи.
PythonCleanupHandler
Обработчик, вызываемый непосредственно перед уничтожением Request-объекта.
Некоторые из этих обработчиков работают только глобально, так как при вызове даже каталог их приложения может быть неизвестен (таков, например, PythonPostReadRequestHandler).
С помощью mod_python можно строить web-сайты с динамическим содержимым и контролировать некоторые аспекты работы web-сервера Apache через Python-сценарии.
Модуль cgi
В Python имеется поддержка CGI в виде модуля cgi. Следующий пример показывает некоторые из его возможностей:
#!/usr/bin/python # -*- coding: cp1251 -*- import cgi, os
# анализ запроса f = cgi.FieldStorage() if f.has_key("a"): a = f["a"].value else: a = "0"
# обработка запроса b = str(int(a)+1) mytext = open(os.environ["SCRIPT_FILENAME"]).read() mytext_html = cgi.escape(mytext)
# формирование ответа print """Content-Type: text/html
<html><head><title>Решение примера: %(b)s = %(a)s + 1</title></head> <body> %(b)s <table width="80%%"><tr><td> <form action="me.cgi" method="GET"> <input type="text" name="a" value="0" size="6"> <input type="submit" name="b" value="Обработать"> </form></td></tr></table> <pre> %(mytext_html)s </pre> </body></html>""" % vars()
В этом примере к заданному в форме числу прибавляется 1. Кроме того, выводится исходный код самого сценария. Следует заметить, что для экранирования символов >, <, & использована функция cgi.escape(). Для формирования Web-страницы применена операция форматирования. В качестве словаря для выполнения подстановок использован словарь vars() со всеми локальными переменными. Знаки процента пришлось удвоить, чтобы они не интерпретировались командой форматирования. Стоит обратить внимание на то, как получено значение от пользователя. Объект FieldStorage "почти" словарь, с тем исключением, что для получения обычного значения нужно дополнительно посмотреть атрибут value. Дело в том, что в сценарий могут передаваться не только текстовые значения, но и файлы, а также множественные значения с одним и тем же именем.
Осторожно! При обработке входных значений CGI-сценариев нужно внимательно и скрупулезно проверять допустимые значения. Лучше считать, что клиент может передать на вход все, что угодно. Из этого всего необходимо выбрать и проверить только то, что ожидает сценарий. Например, не следует подставлять полученные от пользователя данные в путь к файлу, в качестве аргументов к функции eval() и ей подобных; параметров командной строки; частей в SQL-запросах к базе данных. Также не стоит вставлять полученные данные напрямую в формируемые страницы, если эти страницы будет видеть не только клиент, заказавший URL (например, такая ситуация обычна в web-чатах, форумах, гостевых книгах), и даже в том случае, если единственный читатель этой информации - администратор сайта. Тот, кто смотрит страницы с непроверенным HTML-кодом, поступившим напрямую от пользователя, рискуют обработать в своем браузере зловредный код, использующий брешь в его защите. Даже если CGI-сценарий используется исключительно другими сценариями через запрос на URL, нужно проверять входные значения столь же тщательно, как если бы данные вводил пользователь. (Так как недоброжелатель может подать на web-сервер любые значения). |
В примере выше проверка на допустимость произведена при вызове функции int(): если было бы задано нечисловое значение, сценарий аварийно завершился, а пользователь увидел Internal Server Error.
После анализа входных данных можно выделить фазу их обработки. В этой части CGI-сценария вычисляются переменные для дальнейшего вывода. Здесь необходимо учитывать не только значения переданных переменных, но и факт их присутствия или отсутствия, так как это тоже может влиять на логику сценария.
И, наконец, фаза вывода готового объекта (текста, HTML-документа, изображения, мультимедиа-объекта и т.п.). Проще всего заранее подготовить шаблон страницы (или ее крупных частей), а потом просто заполнить содержимым из переменных.
В приведенных примерах имена появлялись в строке запроса только один раз. Некоторые формы порождают несколько значений для одного имени. Получить все значения можно с помощью метода getlist():
lst = form.getlist("fld")
Список lst будет содержать столько значений, сколько полей с именем fld получено из web-формы (он может быть и пустым, если ни одно поле с заданным именем не было заполнено).
В некоторых случаях необходимо передать на сервер файлы (сделать upload). Следующий пример и комментарий к нему помогут разобраться с этой задачей:
#!/usr/bin/env python import cgi
form = cgi.FieldStorage() file_contents = "" if form.has_key("filename"): fileitem = form["filename"] if fileitem.file: file_contents = """<P>Содержимое переданного файла: <PRE>%s</PRE>""" % fileitem.file.read()
print """Content-Type: text/html
<HTML><HEAD><TITLE>Загрузка файла</TITLE></HEAD> <BODY><H1>Загрузка файла</H1> <P><FORM ENCTYPE="multipart/form-data" ACTION="getfile.cgi" METHOD="POST"> <br>Файл: <INPUT TYPE="file" NAME="filename"> <br><INPUT TYPE="submit" NAME="button" VALUE="Передать файл"> </FORM> %s </BODY></HTML>""" % file_contents
В начале следует рассмотреть web-форму, которая приведена в конце сценария: именно она будет выводиться пользователю при обращении по CGI-сценарию. Форма имеет поле типа file, которое в web-броузере представляется полоской ввода и кнопкой "Browse". Нажимая на кнопку "Browse", пользователь выбирает файл, доступный в ОС на его компьютере. После этого он может нажать кнопку "Передать файл" для передачи файла на сервер.
Для отладки CGI-сценария можно использовать модуль cgitb. При возникновении ошибки этот модуль выдаст красочную HTML-страницу с указанием места возбуждения исключения. В начале отлаживаемого сценария нужно поставить
import cgitb cgitb.enable(1)
Или, если не нужно показывать ошибки в браузере:
import cgitb cgitb.enable(0, logdir="/tmp")
Только необходимо помнить, что следует убрать эти строки, когда сценарий будет отлажен, так как он выдает кусочки кода сценария. Это может быть использовано злоумышленниками, с тем чтобы найти уязвимости в CGI-сценарии или подсмотреть пароли (если таковые присутствуют в сценарии).
Среды разработки
Для создания Web-приложений применяются и более сложные средства, чем web-сервер с расположенными на нем статическими документами и CGI-сценариями. В зависимости от назначения такие программные системы называются серверами web-приложений, системами управления содержимым (CMS, Content Management System), системы web-публикации и средствами для создания WWW-порталов. Причем CMS-система может быть выполнена как web-приложение, а средства для создания порталов могут базироваться на системах web-публикации, для которых CMS-система является подсистемой. Поэтому, выбирая систему для конкретных нужд, стоит уточнить, какие функции она должна выполнять.
Язык Python, хотя и уступает PHP по количеству созданных на нем web-систем, имеет несколько достаточно популярных приложений. Самым ярким примером средства для создания сервера web-приложений является Zope (произносится "зоп") (см. http://zope.org) (Z Object Publishing Environment, среда публикации объектов). Zope имеет встроенный web-сервер, но может работать и с другими Web-серверами, например, Apache. На основе Zope можно строить web-порталы, например, с помощью Plone/Zope, но можно разрабатывать и собственные web-приложения. При этом Zope позволяет разделить Форму, Содержание и Логику до такой степени, что Содержанием могут заниматься одни люди (менеджеры по содержимому), Формой - другие (web-дизайнеры), а Логикой - третьи (программисты). В случае с Zope Логику можно задать с помощью языка Python (или, как вариант, Perl), Форма может быть создана в графических или специализированных web-редакторах, а работа с содержимым организована через Web-формы самого Zope.
В этой лекции были рассмотрены
В этой лекции были рассмотрены различные подходы к использованию Python в web-приложениях. Самый простой способ реализации web-приложения - использование CGI-сценариев. Более сложным является использование специальных модулей для web-сервера, таких как mod_python. Наконец, существуют технологии вроде Zope, которые предоставляют специализированные сервисы, позволяющие создавать web-приложения.
Zope и его объектная модель
В рамках этой лекции невозможно детально рассмотреть такой инструмент как Zope, поэтому стоит лишь заметить, что он достаточно интересен не только в качестве среды разработки web-приложений, но и с точки зрения подходов. Например, уникальная объектно-ориентированная модель Zope позволяет довольно гибко описывать требуемое приложение.
Zope включает в себя следующие возможности:
Web-сервер. Zope может работать с Web-серверами через CGI или использовать свой встроенный Web-сервер (ZServer).Среда разработчика выполнена как Web-приложение. Zope позволяет создавать Web-приложения через Web-интерфейс.Поддержка сценариев. Zope поддерживает несколько языков сценариев: Python, Perl и собственный DTML (Document Template Markup Language, язык разметки шаблона документа).База данных объектов. Zope использует в своей работе устойчивые объекты, хранимые в специальной базе данных (ZODB). Имеется достаточно простой интерфейс для управления этой базой данных.Интеграция с реляционными базами данных. Zope может хранить свои объекты и другие данные в реляционных СУБД: Oracle, PostgreSQL, MySQL, Sybase и т.п.
В ряду других подобных систем Zope на первый взгляд кажется странным и неприступным, однако тем, кто с ним "на ты", он открывает большие возможности.
Разработчики Zope исходили из лежащей в основе WWW объектной модели, в которой загрузку документа по URI можно сравнить с отправкой сообщения объекту. Объекты Zope разложены по папкам (folders), к которым привязаны политики доступа для пользователей, имеющих определенные роли. В качестве объектов могут выступать документы, изображения, мультимедиа-файлы, адаптеры к базам данных и т.п.
Документы Zope можно писать на языке DTML - дополнении HTML с синтаксисом для включения значений подобно SSI (Server-Side Include). Например, для вставки переменной с названием документа можно использовать
<!- #var document_title ->
Следует заметить, что объекты Zope могут иметь свои атрибуты, а также методы, в частности, написанные на языке Python.
Переменные же могут появляться как из заданных пользователем значений, так и из других источников данных (например, из базы данных посредством выполнения выборки функцией SELECT).
Сейчас для описания документа Zope все чаще применяется ZPT (Zope Page Templates, шаблоны страниц Zope), которые в свою очередь используют TAL (Template Attribute Language, язык шаблонных атрибутов). Он позволяет заменять, повторять или пропускать элементы документа описываемого шаблоном документа. "Операторами" языка TAL являются XML-атрибуты из пространства имен TAL. Пространство имен сегодня описывается следующим идентификатором:
xmlns:tal="http://xml.zope.org/namespaces/tal"
Оператор TAL имеет имя и значение (что выражается именем и значением атрибута). Внутри значения обычно записано TAL-выражение, синтаксис которого описывается другим языком - TALES (Template Attribute Language Expression Syntax, синтаксис выражений TAL).
Таким образом, ZPT наполняет содержимым шаблоны, интерпретируя атрибуты TAL. Например, чтобы Zope подставил название документа (тег TITLE), шаблон может иметь следующий код:
<title tal:content="here/title">Doc Title</title>
Стоит заметить, что приведенный код сойдет за код на HTML, то есть, Web-дизайнер может на любом этапе работы над проектом редактировать шаблон в HTML-редакторе (при условии, что тот сохраняет незнакомые атрибуты из пространства имен tal). В этом примере here/titleявляется выражением TALES. Текст Doc Title служит ориентиром для web-дизайнера и заменяется значением выражения here/title, то есть, будет взято свойство title документа Zope.
Примечание: В Zope объекты имеют свойства. Набор свойств зависит от типа объекта, но может быть расширен в индивидуальном порядке. Свойство id присутствует всегда, свойство title обычно тоже указывается. |
<ul> <li tal:define="s modules/string" tal:repeat="el python:s.digits"> <a href="DUMMY" tal:attributes="href string:/digit/$el" tal:content="el">SELECTION</a> </li> </ul>
Этот шаблон породит следующий результат:
<ul> <li><a href="../../../../digit/0">0</a></li> <li><a href="../../../../digit/1">1</a></li> <li><a href="../../../../digit/2">2</a></li> <li><a href="../../../../digit/3">3</a></li> <li><a href="../../../../digit/4">4</a></li> <li><a href="../../../../digit/5">5</a></li> <li><a href="../../../../digit/6">6</a></li> <li><a href="../../../../digit/7">7</a></li> <li><a href="../../../../digit/8">8</a></li> <li><a href="../../../../digit/9">9</a></li> </ul>
Здесь нужно обратить внимание на два основных момента:
в шаблоне можно использовать выражения Python (в данном примере переменная s определена как модуль Python) и переменную-счетчик цикла el, которая проходит итерации по строке string.digits.с помощью TAL можно задавать не только содержимое элемента, но и атрибута тега (в данном примере использовался атрибут href).
Детали можно узнать по документации. Стоит лишь заметить, что итерация может происходить по самым разным источникам данных: содержимому текущей папки, выборке из базы данных или, как в приведенном примере, по объекту Python.
Любой программист знает, что программирование тем эффективнее, чем лучше удалось "расставить скобки", выведя "общий множитель за скобки". Другими словами, хорошие программисты должны быть достаточно "ленивы", чтобы найти оптимальную декомпозицию решаемой задачи.
При проектировании динамического web-сайта Zope позволяет разместить "множители" и "скобки" так, чтобы достигнуть максимального повторного использования кода (как разметки, так и сценариев). Помогает этому уникальный подход к построению взаимоотношений между объектами: заимствование (acquisition).
Пусть некоторый объект (документ, изображение, сценарий, подключение к базе данных и т.п.) расположен в папке Example. Теперь объекты этой папки доступны по имени из любых нижележащих папок. Даже политики безопасности заимствуются более глубоко вложенными папками от папок, которые ближе к корню. Заимствование является очень важной концепцией Zope, без понимания которой Zope сложно грамотно применять, и наоборот, ее понимание позволяет экономить силы и время, повторно используя объекты в разработке.
Самое интересное, что заимствовать объекты можно также из параллельных папок. Пусть, например, рядом с папкой Example находится папка Zigzag, в которой лежит нужный объект (его наименование note). При этом в папке Example программиста интересует объект index_html, внутри которого вызывается note. Обычный путь к объекту index_html будет происходить по URI вроде http://zopeserver/Example/. А вот если нужно использовать note из Zigzag (и в папке Example его нет), то URI будет: http://zopeserver/Zigzag/Example/. Таким образом, указание пути в Zope отличается от традиционного пути, скажем, в Unix: в пути могут присутствовать "зигзаги" через параллельные папки, дающие возможность заимствовать объекты из этих папок. Таким образом, можно сделать конкретную страницу, комбинируя один или несколько независимых аспектов.
Функции для анализа URL
Согласно документу RFC 2396 URL должен строиться по следующему шаблону:
scheme://netloc/path;parameters?query#fragment
где
scheme
Адресная схема. Например: http, ftp, gopher.
netloc
Местонахождение в сети.
path
Путь к ресурсу.
params
Параметры.
query
Строка запроса.
frag
Идентификатор фрагмента.
Одна из функций уже использовалась для формирования URL - urllib.urlencode(). Кроме нее в модуле urllib имеются и другие функции:
quote(s, safe='/')
Функция экранирует символы в URL, чтобы их можно было отправлять на web-сервер. Она предназначена для экранирования пути к ресурсу, поэтому оставляет '/' как есть. Например:
>>> urllib.quote("rnd@onego.ru") 'rnd%40onego.ru' >>> urllib.quote("a = b + c") 'a%20%3D%20b%20%2B%20c' >>> urllib.quote("0/1/1") '0/1/1' >>> urllib.quote("0/1/1", safe="") '0%2F1%2F1' quote_plus(s, safe='')
Функция экранирует некоторые символы в URL (в строке запроса), чтобы их можно было отправлять на web-сервер. Аналогична quote(), но заменяет пробелы на плюсы.
unquote(s)
Преобразование, обратное quote_plus(). Пример:
>>> urllib.unquote('a%20%3D%20b%20%2B%20c') 'a = b + c' unquote_plus(s)
Преобразование, обратное quote_plus(). Пример:
>>> urllib.unquote_plus('a+=+b+%2B+c') 'a = b + c'
Для анализа URL можно использовать функции из модуля urlparse:
urlparse(url, scheme='', allow_fragments=1)
Разбирает URL в 6 компонентов (сохраняя экранирование символов): scheme://netloc/path;params?query#frag
urlsplit(url, scheme='', allow_fragments=1)
Разбирает URL в 6 компонентов (сохраняя экранирование символов): scheme://netloc/path?query#frag
urlunparse((scheme, netloc, url, params, query, fragment))
Собирает URL из 6 компонентов.
urlunsplit((scheme, netloc, url, query, fragment))
Собирает URL из 5 компонентов.
Пример:
>>> from urlparse import urlsplit, urlunsplit >>> URL = "http://google.com/search?q=Python" >>> print urlsplit(URL) ('http', 'google.com', '/search', 'q=Python', '') >>> print urlunsplit( ... ('http', 'google.com', '/search', 'q=Python', '')) http://google.com/search?q=Python
Еще одна функция того же модуля urlparse позволяет корректно соединить две части URL - базовую и относительную:
>>> import urlparse >>> urlparse.urljoin('http://python.onego.ru', 'itertools.html') 'http://python.onego.ru/itertools.html'
Функции для загрузки сетевых объектов
Простой случай получения WWW-объекта по известному URL показан в следующем примере:
import urllib doc = urllib.urlopen("http://python.onego.ru").read() print doc[:40]
Функция urllib.urlopen() создает файлоподобный объект, который читает методом read(). Другие методы этого объекта: readline(), readlines(), fileno(), close() работают как и у обычного файла, а также есть метод info(), который возвращает соответствующий полученному с сервера Message-объект. Этот объект можно использовать для получения дополнительной информации:
>>> import urllib >>> f = urllib.urlopen("http://python.onego.ru") >>> print f.info() Date: Sat, 25 Dec 2004 19:46:11 GMT Server: Apache/1.3.29 (Unix) PHP/4.3.10 Content-Type: text/html; charset=windows-1251 Content-Length: 4291 >>> print f.info()['Content-Type'] text/html; charset=windows-1251
С помощью функции urllib.urlopen() можно делать и более сложные вещи, например, передавать web-серверу данные формы. Как известно, данные заполненной web-формы могут быть переданы на web-сервер с использованием метода GET или метода POST. Метод GET связан с кодированием всех передаваемых параметров после знака "?" в URL, а при методе POST данные передаются в теле HTTP-запроса. Оба варианта передачи представлены ниже:
import urllib data = {"search": "Python"} enc_data = urllib.urlencode(data)
# метод GET f = urllib.urlopen("http://searchengine.com/search" + "?" + enc_data) print f.read()
# метод POST f = urllib.urlopen("http://searchengine.com/search", enc_data) print f.read()
В некоторых случаях данные имеют повторяющиеся имена. В этом случае в качестве параметра urllib.urlencode() можно использовать вместо словаря последовательность пар имя-значение:
>>> import urllib >>> data = [("n", "1"), ("n", "3"), ("n", "4"), ("button", "Привет"),] >>> enc_data = urllib.urlencode(data) >>> print enc_data n=1&n=3&n=4&button=%F0%D2%C9%D7%C5%D4
Модуль urllib позволяет загружать web-объекты через прокси-сервер. Если ничего не указывать, будет использоваться прокси-сервер, который был задан принятым в конкретной ОС способом. В Unix прокси-серверы задаются в переменных окружения http_proxy, ftp_proxy и т.п., в Windows прокси-серверы записаны в реестре, а в Mac OS они берутся из конфигурации Internet. Задать прокси-сервер можно и как именованный параметр proxies к urllib.urlopen():
# Использовать указанный прокси proxies = {'http': 'http://www.proxy.com:3128'} f = urllib.urlopen(some_url, proxies=proxies) # Не использовать прокси f = urllib.urlopen(some_url, proxies={}) # Использовать прокси по умолчанию f = urllib.urlopen(some_url, proxies=None) f = urllib.urlopen(some_url)
Функция urlretrieve() позволяет записать заданный URL сетевой объект в файл. Она имеет следующие параметры:
urllib.urlretrieve(url[, filename[, reporthook[, data]]])
Здесь url - URL сетевого объекта, filename - имя локального файла для помещения объекта, reporthook - функция, которая будет вызываться для сообщения о состоянии загрузки, data - данные для метода POST (если он используется). Функция возвращает кортеж (filepath, headers) , где filepath - имя локального файла, в который закачан объект, headers - результат метода info() для объекта, возвращенного urlopen().
Для обеспечения интерактивности функция urllib.urlretrieve() вызывает время от времени функцию, заданную в reporthook(). Этой функции передаются три аргумента: количество принятых блоков, размер блока и общий размер принимаемого объекта в байтах (если он неизвестен, этот параметр равен -1).
В следующем примере программа принимает большой файл и, чтобы пользователь не скучал, пишет процент от выполненной загрузки и предполагаемое оставшееся время:
FILE = 'boost-1.31.0-9.src.rpm' URL = 'http://download.fedora.redhat.com/pub/fedora/linux/core/3/SRPMS/' + FILE
def download(url, file): import urllib, time start_t = time.time()
def progress(bl, blsize, size): dldsize = min(bl*blsize, size) if size != -1: p = float(dldsize) / size try: elapsed = time.time() - start_t est_t = elapsed / p - elapsed except: est_t = 0 print "%6.2f %% %6.0f s %6.0f s %6i / %-6i bytes" % ( p*100, elapsed, est_t, dldsize, size) else: print "%6i / %-6i bytes" % (dldsize, size)
urllib.urlretrieve(URL, FILE, progress)
download(URL, FILE)
Эта программа выведет примерно следующее (процент от полного объема закачки, прошедшие секунды, предполагаемое оставшееся время, закачанные байты, полное количество байтов):
0.00 % 1 s 0 s 0 / 6952309 bytes 0.12 % 5 s 3941 s 8192 / 6952309 bytes 0.24 % 7 s 3132 s 16384 / 6952309 bytes 0.35 % 10 s 2864 s 24576 / 6952309 bytes 0.47 % 12 s 2631 s 32768 / 6952309 bytes 0.59 % 15 s 2570 s 40960 / 6952309 bytes 0.71 % 18 s 2526 s 49152 / 6952309 bytes 0.82 % 20 s 2441 s 57344 / 6952309 bytes ...
Модуль poplib
Еще один протокол - POP3 (Post Office Protocol, почтовый протокол) - служит для приема почты из почтового ящика на сервере (протокол определен в RFC 1725).
Для работы с почтовым сервером требуется установить с ним соединение и, подобно рассмотренному выше примеру, с помощью SMTP-команд получить требуемые сообщения. Объект-соединение POP3 можно установить посредством конструктора класса POP3 из модуля poplib:
poplib.POP3(host[, port])
Где host - адрес POP3-сервера, port - порт на сервере (по умолчанию 110), pop_obj - объект для управления сеансом работы с POP3-сервером.
Следующий пример демонстрирует основные методы для работы с POP3-соединением:
import poplib, email # Учетные данные пользователя: SERVER = "pop.server.com" USERNAME = "user" USERPASSWORD = "secretword"
p = poplib.POP3(SERVER) print p.getwelcome() # этап идентификации print p.user(USERNAME) print p.pass_(USERPASSWORD) # этап транзакций response, lst, octets = p.list() print response for msgnum, msgsize in [i.split() for i in lst]: print "Сообщение %(msgnum)s имеет длину %(msgsize)s" % vars() print "UIDL =", p.uidl(int(msgnum)).split()[2] if int(msgsize) > 32000: (resp, lines, octets) = p.top(msgnum, 0) else: (resp, lines, octets) = p.retr(msgnum) msgtxt = "\n".join(lines)+"\n\n" msg = email.message_from_string(msgtxt) print "* От: %(from)s\n* Кому: %(to)s\n* Тема: %(subject)s\n" % msg # msg содержит заголовки сообщения или все сообщение (если оно небольшое)
# этап обновления print p.quit()
Примечание: Разумеется, чтобы пример сработал корректно, необходимо внести реальные учетные данные. |
При выполнении сценарий выведет на экран примерно следующее.
+OK POP3 pop.server.com server ready +OK User name accepted, password please +OK Mailbox open, 68 messages +OK Mailbox scan listing follows Сообщение 1 имеет длину 4202 UIDL = 4152a47e00000004 * От: online@kaspersky.com * Кому: user@server.com * Тема: KL Online Activation
...
+OK Sayonara
Эти и другие методы экземпляров класса POP3 описаны ниже:
getwelcome() | Получает строку s с приветствием POP3-сервера | |
user(name) | USER name | Посылает команду USER с указанием имени пользователя name. Возвращает строку с ответом сервера |
pass_(pwd) | PASS pwd | Отправляет пароль пользователя в команде PASS. После этой команды и до выполнения команды QUIT почтовый ящик блокируется |
apop(user, secret) | APOP user secret | Идентификация на сервере по APOP |
rpop(user) | RPOP user | Идентификация по методу RPOP |
stat() | STAT | Возвращает кортеж с информацией о почтовом ящике. В нем m - количество сообщений, l - размер почтового ящика в байтах |
list([num]) | LIST [num] | Возвращает список сообщений в формате (resp, ['num octets', ...]), если не указан num, и "+OK num octets", если указан. Список lst состоит из строк в формате "num octets". |
retr(num) | RETR num | Загружает с сервера сообщение с номером num и возвращает кортеж с ответом сервера (resp, lst, octets) |
dele(num) | DELE num | Удаляет сообщение с номером num |
rset() | RSET | Отменяет пометки удаления сообщений |
noop() | NOOP | Ничего не делает (поддерживает соединение) |
quit() | QUIT | Отключение от сервера. Сервер выполняет все необходимые изменения (удаляет сообщения) и снимает блокировку почтового ящика |
top(num, lines) | TOP num lines | Команда аналогична RETR, но загружает только заголовок и lines строк тела сообщения. Возвращает кортеж (resp, lst, octets) |
uidl([num]) | UIDL [num] | Сокращение от "unique-id listing" (список уникальных идентификаторов сообщений). Формат результата: (resp, lst, octets), если num не указан, и "+OK num uniqid", если указан. Список lst состоит из строк вида "+OK num uniqid" |
Работа с POP3-сервером состоит из трех фаз: идентификации, транзакций и обновления. На этапе идентификации сразу после создания POP3-объекта разрешены только команды USER, PASS (иногда APOP и RPOP). После идентификации сервер получает информацию о пользователе и наступает этап транзакций. Здесь уместны остальные команды. Этап обновления вызывается командой QUIT, после которой POP3-сервер обновляет почтовый ящик пользователя в соответствии с поданными командами, а именно - удаляет помеченные для удаления сообщения.
Модуль smtplib
Сообщения электронной почты в Интернете передаются от клиента к серверу и между серверами в основном по протоколу SMTP (Simple Mail Transfer Protocol, простой протокол передачи почты). Протокол SMTP и ESMTP (расширенный вариант SMTP) описаны в RFC 821 и RFC 1869. Для работы с SMTP в стандартной библиотеке модулей имеется модуль smtplib. Для того чтобы начать SMTP-соединение с сервером электронной почты, необходимо в начале создать объект для управления SMTP-сессией с помощью конструктора класса SMTP:
smtplib.SMTP([host[, port]])
Параметры host и port задают адрес и порт SMTP-сервера, через который будет отправляться почта. По умолчанию, port=25. Если host задан, конструктор сам установит соединение, иначе придется отдельно вызывать метод connect(). Экземпляры класса SMTP имеют методы для всех распространенных команд SMTP-протокола, но для отправки почты достаточно вызова конструктора и методов sendmail() и quit():
# -*- coding: cp1251 -*- from smtplib import SMTP fromaddr = "student@mail.ru" # От кого toaddr = "rnd@onego.ru" # Кому message = """From: Student <%(fromaddr)s> To: Lecturer <%(toaddr)s> Subject: From Python course student MIME-Version: 1.0 Content-Type: text/plain; charset=Windows-1251 Content-Transfer-Encoding: 8bit
Здравствуйте! Я изучаю курс по языку Python и отправляю письмо его автору. """ connect = SMTP('mail.onego.ru') connect.set_debuglevel(1) connect.sendmail(fromaddr, toaddr, message % vars()) connect.quit()
Следует заметить, что toaddr в сообщении (в поле To) и при отправке могут не совпадать. Дело в том, что получатель и отправитель в ходе SMTP-сессии передается командами SMTP-протокола. При запуске указанного выше примера на экране появится отладочная информация (ведь уровень отладки задан равным 1):
send: 'ehlo rnd.onego.ru\r\n' reply: '250-mail.onego.ru Hello as3-042.dialup.onego.ru [195.161.147.4], pleased to meet you\r\n' send: 'mail FROM:<student@mail.ru> size=270\r\n' reply: '250 2.1.0 <student@mail.ru>...
Sender ok\r\n' send: 'rcpt TO:<rnd@onego.ru>\r\n' reply: '250 2.1.5 <rnd@onego.ru>... Recipient ok\r\n' send: 'data\r\n' reply: ' 354 Enter mail, end with "." on a line by itself\r\n' send: 'From: Student <student@mail.ru>\r\n . . . ' reply: '250 2.0.0 iBPFgQ7q028433 Message accepted for delivery\r\n' send: 'quit\r\n' reply: '221 2.0.0 mail.onego.ru closing connection\r\n'
Из этой (несколько сокращенной) отладочной информации можно увидеть, что клиент отправляет (send) команды SMTP-серверу (EHLO, MAIL FROM, RCPT TO, DATA, QUIT), а тот выполняет команды и отвечает (reply), возвращая код возврата.
В ходе одной SMTP-сессии можно отправить сразу несколько писем подряд, если не вызывать quit().
В принципе, команды SMTP можно подавать и отдельно: для этого у объекта-соединения есть методы (helo(), ehlo(), expn(), help(), mail(), rcpt(), vrfy(), send(), noop(), data()), соответствующие одноименным командам SMTP-протокола.
Можно задать и произвольную команду SMTP-серверу с помощью метода docmd(). В следующем примере показан простейший сценарий, который могут использовать те, кто время от времени принимает почту на свой сервер по протоколу SMTP от почтового сервера, на котором хранится очередь сообщений для некоторого домена:
from smtplib import SMTP connect = SMTP('mx.abcde.ru') connect.set_debuglevel(1) connect.docmd("ETRN rnd.abcde.ru") connect.quit()
Этот простенький сценарий предлагает серверу mx.abcde.ru попытаться связаться с основным почтовым сервером домена rnd.abcde.ru и переслать всю накопившуюся для него почту.
При работе с классом smtplib.SMTP могут возбуждаться различные исключения. Назначение некоторых из них приведено ниже:
smtplib.SMTPException
Базовый класс для всех исключений модуля.
smtplib.SMTPServerDisconnected
Сервер неожиданно прервал связь (или связь с сервером не была установлена).
smtplib.SMTPResponseException
Базовый класс для всех исключений, которые имеют код ответа SMTP-сервера.
smtplib.SMTPSenderRefused
Отправитель отвергнут
smtplib.SMTPRecipientsRefused
Все получатели отвергнуты сервером.
smtplib.SMTPDataError
Сервер ответил неизвестным кодом на данные сообщения.
smtplib.SMTPConnectError
Ошибка установления соединения.
smtplib.SMTPHeloError
Сервер не ответил правильно на команду HELO или отверг ее.
Модули для клиента WWW
Стандартные средства языка Python позволяют получать из программы доступ к объектам WWW как в простых случаях, так и при сложных обстоятельствах, в частности при необходимости передавать данные формы, идентификации, доступа через прокси и т.п.
Стоит отметить, что при работе с WWW используется в основном протокол HTTP, однако WWW охватывает не только HTTP, но и многие другие схемы (FTP, gopher, HTTPS и т.п.). Используемая схема обычно указана в самом начале URL.
Работа с сокетами
Применяемая в IP-сетях архитектура клиент-сервер использует IP-пакеты для коммуникации между клиентом и сервером. Клиент отправляет запрос серверу, на который тот отвечает. В случае с TCP/IP между клиентом и сервером устанавливается соединение (обычно с двусторонней передачей данных), а в случае с UDP/IP - клиент и сервер обмениваются пакетами (дейтаграммамми) с негарантированной доставкой.
Каждый сетевой интерфейс IP-сети имеет уникальный в этой сети адрес (IP-адрес). Упрощенно можно считать, что каждый компьютер в сети Интернет имеет собственный IP-адрес. При этом в рамках одного сетевого интерфейса может быть несколько сетевых портов. Для установления сетевого соединения приложение клиента должно выбрать свободный порт и установить соединение с серверным приложением, которое слушает (listen) порт с определенным номером на удаленном сетевом интерфейсе. Пара IP-адрес и порт характеризуют сокет (гнездо) - начальную (конечную) точку сетевой коммуникации. Для создания соединения TCP/IP необходимо два сокета: один на локальной машине, а другой - на удаленной. Таким образом, каждое сетевое соединение имеет IP-адрес и порт на локальной машине, а также IP-адрес и порт на удаленной машине.
Модуль socket обеспечивает возможность работать с сокетами из Python. Сокеты используют транспортный уровень согласно семиуровневой модели OSI (Open Systems Interconnection, взаимодействие открытых систем), то есть относятся к более низкому уровню, чем большинство описываемых в этом разделе протоколов.
Уровни модели OSI:
Физический
Поток битов, передаваемых по физической линии. Определяет параметры физической линии.
Канальный (Ethernet, PPP, ATM и т.п.)
Кодирует и декодирует данные в виде потока битов, справляясь с ошибками, возникающими на физическом уровне в пределах физически единой сети.
Сетевой (IP)
Маршрутизирует информационные пакеты от узла к узлу.
Транспортный (TCP, UDP и т.п.)
Обеспечивает прозрачную передачу данных между двумя точками соединения.
Сеансовый
Управляет сеансом соединения между участниками сети.
Начинает, координирует и завершает соединения.
Представления
Обеспечивает независимость данных от формы их представления путем преобразования форматов. На этом уровне может выполняться прозрачное (с точки зрения вышележащего уровня) шифрование и дешифрование данных.
Приложений (HTTP, FTP, SMTP, NNTP, POP3, IMAP и т.д.)
Поддерживает конкретные сетевые приложения. Протокол зависит от типа сервиса.
Каждый сокет относится к одному из коммуникационных доменов. Модуль socket поддерживает домены UNIX и Internet. Каждый домен подразумевает свое семейство протоколов и адресацию. Данное изложение будет затрагивать только домен Internet, а именно протоколы TCP/IP и UDP/IP, поэтому для указания коммуникационного домена при создании сокета будет указываться константа socket.AF_INET.
В качестве примера следует рассмотреть простейшую клиент-серверную пару. Сервер будет принимать строку и отвечать клиенту. Сетевое устройство иногда называют хостом (host), поэтому будет употребляться этот термин по отношению к компьютеру, на котором работает сетевое приложение.
Сервер:
import socket, string
def do_something(x): lst = map(None, x); lst.reverse(); return string.join(lst, "")
HOST = "" # localhost PORT = 33333 srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.bind((HOST, PORT)) while 1: print "Слушаю порт 33333" srv.listen(1) sock, addr = srv.accept() while 1: pal = sock.recv(1024) if not pal: break print "Получено от %s:%s:" % addr, pal lap = do_something(pal) print "Отправлено %s:%s:" % addr, lap sock.send(lap) sock.close()
Клиент:
import socket
HOST = "" # удаленный компьютер (localhost) PORT = 33333 # порт на удаленном компьютере sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((HOST, PORT)) sock.send("ПАЛИНДРОМ") result = sock.recv(1024) sock.close() print "Получено:", result
Примечание: В примере использованы русские буквы: необходимо указывать кодировку. |
Сервер открывает сокет на локальной машине на порту 33333, и адресе 127.0.0.1. После этого он слушает (listen()) порт. Когда на порту появляются данные, принимается (accept()) входящее соединение. Метод accept() возвращает пару - Socket-объект и адрес удаленного компьютера, устанавливающего соединение (пара - IP-адрес, порт на удаленной машине). После этого можно применять методы recv() и send() для общения с клиентом. В recv() задается число байтов в очередной порции. От клиента может прийти и меньшее количество данных.
Код программы-клиента достаточно очевиден. Метод connect() устанавливает соединение с удаленным хостом (в приведенном примере он расположен на той же машине). Данные передаются методом send() и принимаются методом recv()- аналогично тому, что происходит на сервере.
Модуль socket имеет несколько вспомогательных функций. В частности, функции для работы с системой доменных имен (DNS):
>>> import socket >>> socket.gethostbyaddr('www.onego.ru') ('www.onego.ru', [], ['195.161.136.4']) >>> socket.gethostbyaddr('195.161.136.4') ('www.onego.ru', [], ['195.161.136.4']) >>> socket.gethostname() 'rnd.onego.ru'
В новых версиях Python появилась такая функция как socket.getservbyname(). Она позволяет преобразовывать наименования Интернет-сервисов в общепринятые номера портов:
>>> for srv in 'http', 'ftp', 'imap', 'pop3', 'smtp': ... print socket.getservbyname(srv, 'tcp'), srv ... 80 http 21 ftp 143 imap 110 pop3 25 smtp
Модуль также содержит большое количество констант для указания протоколов, типов сокетов, коммуникационных доменов и т.п. Другие функции модуля socket можно при необходимости изучить по документации.
и urlparse хватает для большинства
Функциональности модулей urllib и urlparse хватает для большинства задач, которые решают сценарии на Python как web-клиенты. Тем не менее, иногда требуется больше. На этот случай можно использовать модуль для работы с протоколом HTTP - httplib - и создать собственный класс для HTTP-запросов (в лекциях модуль httplib не рассматривается). Однако вполне вероятно, что нужная функциональность уже имеется в модуле urllib2.
Одна из полезных возможностей этих модулей - доступ к web-объектам, требующий авторизации. Ниже будет рассмотрен пример, который не только обеспечит доступ с авторизацией, но и обозначит основную идею модуля urllib2: использование обработчиков (handlers), каждый из которых решает узкую специфическую задачу.
Следующий пример показывает, как создать собственный открыватель URL с помощью модуля urllib2 (этот пример взят из документации по Python):
import urllib2
# Подготовка идентификационных данных authinfo = urllib2.HTTPBasicAuthHandler() authinfo.add_password('My page', 'localhost', 'user1', 'secret')
# Доступ через прокси proxy_support = urllib2.ProxyHandler({'http' : 'http://localhost:8080'})
# Создание нового открывателя с указанными обработчиками opener = urllib2.build_opener(proxy_support, authinfo, urllib2.CacheFTPHandler) # Установка поля с названием клиента opener.addheaders = [('User-agent', 'Mozilla/5.0')]
# Установка нового открывателя по умолчанию urllib2.install_opener(opener)
# Использование открывателя f = urllib2.urlopen('http://localhost/mywebdir/') print f.read()[:100]
В этом примере получен доступ к странице, которую охраняет mod_python (см. предыдущую лекцию). Первый аргумент при вызове метода add_password() задает область действия (realm) идентификационных данных (он задан директивой AuthName "My page" в конфигурации web-сервера). Остальные параметры достаточно понятны: имя хоста, на который нужно получить доступ, имя пользователя и его пароль. Разумеется, для корректной работы примера нужно, чтобы на локальном web-сервере был каталог, требующий авторизации.
В данном примере явным образом затронуты всего три обработчика: HTTPBasicAuthHandler, ProxyHandler и CacheFTPHandler. В модуле urllib2 их более десятка, назначение каждого можно узнать из документации к используемой версии Python. Есть и специальный класс для управления открывателями: OpenerDirector. Именно его экземпляр создала функция urllib2.build_opener().
Модуль urllib2 имеет и специальный класс для воплощения запроса на открытие URL. Называется этот класс urllib2.Request. Его экземпляр содержит состояние запроса. Следующий пример показывает, как получить доступ к каталогу с авторизацией, используя добавление заголовка в HTTP-запрос:
import urllib2, base64 req = urllib2.Request('http://localhost/mywebdir') b64 = base64.encodestring('user1:secret').strip() req.add_header('Authorization', 'Basic %s' % b64) req.add_header('User-agent', 'Mozilla/5.0') f = urllib2.urlopen(req) print f.read()[:100]
Как видно из этого примера, ничего загадочного в авторизации нет: web-клиент вносит (закодированные base64) идентификационные данные в поле Authorization HTTP-запроса.
Примечание: Приведенные два примера почти эквивалентны, только во втором примере прокси-сервер не назначен явно. |
XML-RPC сервер
До сих пор высокоуровневые протоколы рассматривались с точки зрения клиента. Не менее просто создавать на Python и их серверные части. Для иллюстрации того, как разработать программу на Python, реализующую сервер, был выбран протокол XML-RPC. Несмотря на свое название, конечному пользователю необязательно знать XML (об этом языке разметки говорилось на одной из предыдущих лекций), так как он скрыт от него. Сокращение RPC (Remote Procedure Call, вызов удаленной процедуры) объясняет суть дела: с помощью XML-RPC можно вызывать процедуры на удаленном хосте. Причем при помощи XML-RPC можно абстрагироваться от конкретного языка программирования за счет использования общепринятых типов данных (строки, числа, логические значения и т.п.). В языке Python вызов удаленной функции по синтаксису ничем не отличается от вызова обычной функции:
import xmlrpclib
# Установить соединение req = xmlrpclib.ServerProxy("http://localhost:8000")
try: # Вызвать удаленную функцию print req.add(1, 3) except xmlrpclib.Error, v: print "ERROR",
А вот как выглядит XML-RPC-сервер (для того чтобы попробовать пример выше, необходимо сначала запустить сервер):
from SimpleXMLRPCServer import SimpleXMLRPCServer srv = SimpleXMLRPCServer(("localhost", 8000)) # Запустить сервер srv.register_function(pow) # Зарегистрировать функцию srv.register_function(lambda x,y: x+y, 'add') # И еще одну srv.serve_forever() # Обслуживать запросы
С помощью XML-RPC (а этот протокол достаточно "легковесный" среди других подобных протоколов) приложения могут общаться друг с другом на понятном им языке вызова функций с параметрами основных общепринятых типов и такими же возвращаемыми значениями. Преимуществом же Python является удобный синтаксис вызова удаленных функций.
Внимание! Разумеется, это только пример. При реальном использовании необходимо позаботиться, чтобы XML-RPC сервер отвечал требованиям безопасности. Кроме того, сервер лучше делать многопоточным, чтобы он мог обрабатывать несколько потоков одновременно. Для многопоточности (она будет обсуждаться в отдельной лекции) не нужно многое переделывать: достаточно определить свой класс, скажем, ThreadingXMLRPCServer, в котором вместо SocketServer.TCPServer использовать SocketServer.ThreadingTCPServer. Это предлагается в качестве упражнения. Наводящий вопрос: где находится определение класса SimpleXMLRPCServer? |
В этой лекции на практических
В этой лекции на практических примерах и сведениях из документации были показаны возможности, которые дает стандартный Python для работы в Интернете. Из сценария на Python можно управлять соединением на уровне сокетов, а также использовать модули для конкретного сетевого протокола или набора протоколов. Для работы с сокетами служит модуль socket, а модули для высокоуровневых протоколов имеют такие названия как smtplib, poplib, httplib и т.п. Для работы с системой WWW можно использовать модули urllib, urllib2, urlparse. Указанные модули рассмотрены с точки зрения типичного применения. Для решения нестандартных задач лучше обратиться к другим источникам: документации, исходному коду модулей, поиску в Интернете. В этой лекции говорилось и о серверной составляющей высокоуровневых сетевых протоколов. В качестве примера приведена клиент-серверная пара для протокола XML-RPC. Этот протокол создан на основе HTTP, но служит специальной цели.
в заголовок аббревиатура объединяет два
Вынесенная в заголовок аббревиатура объединяет два понятия: DB (Database, база данных) и API (Application Program Interface, интерфейс прикладной программы).
Таким образом, DB-API определяет интерфейс прикладной программы с базой данных. Этот интерфейс, описываемый ниже, должен реализовывать все модули расширения, которые служат для связи Python-программ с базами данных. Единый API (в настоящий момент его вторая версия) позволяет абстрагироваться от марки используемой базы данных, при необходимости довольно легко менять одну СУБД на другую, изучив всего один набор функций и методов.
DB-API 2.0 описан в PEP 249 (сайт http://www.python.org/peps/pep-0249.html/), и данное ниже описание основано именно на нем.
Другие СУБД и Python
Модуль sqlite дает прекрасные возможности для построения небольших и быстрых баз данных, однако для полноты изложения предлагается обзор модулей расширения Python для других СУБД.
Выше везде импортировался модуль sqlite, с изменением его имени на db. Это было сделано не случайно. Дело в том, что подобные модули, поддерживающие DB-API 2.0, есть и для других СУБД, и даже не в единственном числе. Согласно информации на сайте www.python.org DB-API 2.0-совместимые модули для Python имеют следующие СУБД или протоколы доступа к БД:
zxJDBC Доступ по JDBC.MySQL Для СУБД MySQL.mxODBC Доступ по ODBC, продается фирмой eGenix (http://www.egenix.com).DCOracle2, cx_Oracle Для СУБД Oracle.PyGresQL, psycopg, pyPgSQL Для СУБД PostgreSQL.Sybase Для Sybase.sapdbapi Для СУБД SAP.KInterbasDB Для СУБД Firebird (это потомок Interbase).PyADO Адаптер к Microsoft ActiveX Data Objects (только под Windows).
Примечание:
Для СУБД PostgreSQL нужно взять не PyGreSQL, а psycopg, так как в первом есть небольшие проблемы с типом для даты и времени при вставке параметров в методе execute(). Кроме того, psycopg оптимизирован для скорости и многопоточности (psycopg.threadsafety=2).
Таким образом, в примерах, используемых в этой лекции, вместо sqlite можно применять, например, psycopg: результат должен быть тем же, если, конечно, соответствующий модуль был установлен.
Однако в общем случае при переходе с одной СУБД на другую могут возникать нестыковки, даже, несмотря на поддержку одной версии DB-API. Например, у модулей могут различаться paramstyle. В этом случае придется немного переделать параметры к вызову execute(). Могут быть и другие причины, поэтому переход на другую СУБД следует тщательно тестировать.
Иметь интерфейс DB-API могут не только базы данных. Например, разработчики проекта fssdb стремятся построить DB-API 2.0 интерфейс к... файловой системе.
Несмотря на достаточно хорошие теоретические основы и стабильные реализации, реляционная модель - не единственная из успешно используемых сегодня.
К примеру, уже рассматривался язык XML и интерфейсы для работы с ним в Python. Древовидная модель данных XML для многих задач является более естественной, и в настоящее время идут исследования, результаты которых позволят работать с XML так же легко и стабильно, как с реляционными СУБД. Язык программирования Python - один из полигонов этих исследований.
Решая конкретную задачу, разработчик программного обеспечения должен сделать выбор средств, наиболее подходящих для решения задачи. Очень многие подходят к этому выбору с предвзятостью, выбирая неоптимальную (для данной задачи или подзадачи) модель данных. В результате данные, которые по своей природе легче представить другой моделью, приходится хранить и обрабатывать в выбранной модели, зачастую невольно моделируя более естественные структуры доступа и хранения. Так, XML можно хранить в реляционной БД, а табличные данные - в XML, однако это неестественно. Из-за этого сложность и подверженность ошибкам программного продукта возрастают, даже если использованные инструменты высокого качества.
Интерфейс модуля
Здесь необходимо сказать о том, что должен предоставлять модуль для удовлетворения требований DB-API 2.0.
Доступ к базе данных осуществляется с помощью объекта-соединения (connection object). DB-API-совместимый модуль должен предоставлять функцию-конструктор connect() для класса объектов-соединений. Конструктор должен иметь следующие именованные параметры:
dsn Название источника данных в виде строкиuser Имя пользователяpassword Парольhost Адрес хоста, на котором работает СУБДdatabase Имя базы данных.
Методы объекта-соединения будут рассмотрены чуть позже.
Модуль определяет константы, содержащие его основные характеристики:
apilevel Версия DB-API ("1.0" или "2.0").threadsafety Целочисленная константа, описывающая возможности модуля при использовании потоков управления: 0 Модуль не поддерживает потоки.1 Потоки могут совместно использовать модуль, но не соединения.2 Потоки могут совместно использовать модуль и соединения.3 Потоки могут совместно использовать модуль, соединения и курсоры. (Под совместным использованием здесь понимается возможность использования упомянутых ресурсов без применения семафоров).paramstyle Тип используемых пометок при подстановке параметров. Возможны следующие значения этой константы:"format" Форматирование в стиле языка ANSI C (например, "%s", "%i")."pyformat" Использование именованных спецификаторов формата в стиле Python ("%(item)s")"qmark" Использование знаков "?" для пометки мест подстановки параметров."numeric" Использование номеров позиций (":1")."named" Использование имен подставляемых параметров (":name").
Модуль должен определять ряд исключений для обозначения типичных исключительных ситуаций: Warning (предупреждение), Error (ошибка), InterfaceError (ошибка интерфейса), DatabaseError (ошибка, относящаяся к базе данных). А также подклассы этого последнего исключения: DataError (ошибка обработки данных), OperationalError (ошибка в работе или сбой соединения с базой данных), IntegrityError (ошибка целостности базы данных), InternalError (внутренняя ошибка базы данных), ProgrammingError (программная ошибка, например, ошибка в синтаксисе SQL-запроса), NotSupportedError (при отсутствии поддержки запрошенного свойства).
Наполнение базы данных
Теперь можно наполнить таблицы значениями. Следует начать с расшифровки числовых значений для дней недели:
weekdays = ["Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"]
import sqlite as db
c = db.connect(database="tvprogram") cu = c.cursor() cu.execute("""DELETE FROM wd;""") cu.executemany("""INSERT INTO wd VALUES (%s, %s);""", enumerate(weekdays)) c.commit() c.close()
Стоит напомнить, что встроенная функция enumerate() создает список пар номер-значение, например:
>>> print [i for i in enumerate(['a', 'b', 'c'])] [(0, 'a'), (1, 'b'), (2, 'c')]
Из приведенного примера ясно, что метод executemany() объекта-курсора использует второй параметр - последовательность - для массового ввода данных с помощью SQL-инструкции INSERT.
Предположим, что телепрограмма задана в файле tv.csv в формате CSV (он уже обсуждался):
10.02.2003 9.00|ОРТ|Новости|Новости|9.15 10.02.2003 9.15|ОРТ|"НЕЖНЫЙ ЯД"|Сериал|10.15 10.02.2003 10.15|ОРТ|"Маски-шоу"|Юмористическая программа|10.45 10.02.2003 10.45|ОРТ|"Человек и закон"||11.30 10.02.2003 11.30|ОРТ|"НОВЫЕ ПРИКЛЮЧЕНИЯ СИНДБАДА"|Сериал|12.00
Следующая программа разбирает CSV-файл и записывает данные в таблицу tv:
import calendar, csv import sqlite as db from sqlite.main import Time, Date ## Только для db.Date, db.Time = Date, Time ## sqlite
c = db.connect(database="tvprogram") cu = c.cursor()
input_file = open("tv.csv", "rb") rdr = csv.DictReader(input_file, fieldnames=['begt', 'channel', 'prname', 'prgenre', 'endt']) for rec in rdr: bd, bt = rec['begt'].split() bdd, bdm, bdy = map(int, bd.split('.')) bth, btm = map(int, bt.split('.')) eth, etm = map(int, rec['endt'].split('.')) rec['wd'] = calendar.weekday(bdy, bdm, bdd) rec['begd'] = db.Date(bdy, bdm, bdd) rec['begt'] = db.Time(bth, btm, 0) rec['endt'] = db.Time(eth, etm, 0)
cu.execute("""INSERT INTO tv (tvdate, tvweekday, tvchannel, tvtime1, tvtime2, prname, prgenre) VALUES ( %(begd)s, %(wd)s, %(channel)s, %(begt)s, %(endt)s, %(prname)s, %(prgenre)s);""", rec) input_file.close() c.commit()
Большая часть преобразований связана с получением дат и времен ( приходится разбивать строки на части в соответствии с форматом даты и времени). День недели получен с помощью функции из модуля calendar.
Примечание:
Из-за небольшой ошибки в пакете sqlite конструкторы Date, Time и т.д. не попадают из модуля sqlite.main при импорте из sqlite, поэтому пришлось добавить две строки, специфичные для sqlite, в универсальный "модуль" с именем db.
В этом же примере было продемонстрировано использование словаря для вставки значений в таблицу базы данных. Следует заметить, что подстановка выполняется внутри вызова execute() в соответствии с типами переданных значений. SQL-инструкция INSERT была бы некорректной при попытке выполнить подстановку самостоятельно, например, операцией форматирования %.
Объект-курсор
Курсор (от англ. cursor - CURrrent Set Of Records, текущий набор записей) служит для работы с результатом запроса. Результатом запроса обычно является одна или несколько прямоугольных таблиц со столбцами-полями и строками-записями. Приложение может читать и обрабатывать полученные таблицы и записи в таблице по одной, поэтому в курсоре хранится информация о текущей таблице и записи. Конкретный курсор в любой момент времени связан с выполнением одной SQL-инструкции.
Атрибуты объекта-курсора тоже определены DB-API:
arraysize Атрибут, равный количеству записей, возвращаемых методом fetchmany(). По умолчанию равен 1.callproc(procname[, params]) Вызывает хранимую процедуру procname с параметрами из изменчивой последовательности params. Хранимая процедура может изменить значения некоторых параметров последовательности. Метод может возвратить результат, доступ к которому осуществляется через fetch-методы.close() Закрывает объект-курсор.description Этот доступный только для чтения атрибут является последовательностью из семиэлементных последовательностей. Каждая из этих последовательностей содержит информацию, описывающую один столбец результата:(name, type_code, display_size, internal_size, precision, scale, null_ok) Первые два элемента (имя и тип) обязательны, а вместо остальных (размер для вывода, внутренний размер, точность, масштаб, возможность задания пустого значения) может быть значение None. Этот атрибут может быть равным None для операций, не возвращающих значения.execute(operation[, parameters]) Исполняет запрос к базе данных или команду СУБД. Параметры (parameters) могут быть представлены в принятой в базе данных нотации в соответствии с атрибутом paramstyle, описанным выше.executemany(operation, seq_of_parameters) Выполняет серию запросов или команд, подставляя параметры в заданный шаблон. Параметр seq_of_parameters задает последовательность наборов параметров.fetchall() Возвращает все (или все оставшиеся) записи результата запроса.fetchmany([size]) Возвращает следующие несколько записей из результатов запроса в виде последовательности последовательностей.
Пустая последовательность означает отсутствие данных. Необязательный параметр size указывает количество возвращаемых записей (реально возвращаемых записей может быть меньше). По умолчанию size равен атрибуту arraysize объекта-курсора.fetchone() Возвращает следующую запись (в виде последовательности) из результата запроса или None при отсутствии данных.nextset() Переводит курсор к началу следующего набора данных, полученного в результате запроса (при этом часть записей в предыдущем наборе может остаться непрочитанной). Если наборов больше нет, возвращает None. Не все базы данных поддерживают возврат нескольких наборов результатов за одну операцию.rowcount Количество записей, полученных или затронутых в результате выполнения последнего запроса. В случае отсутствия execute-запросов или невозможности указать количество записей равен -1.setinputsizes(sizes) Предопределяет области памяти для параметров, используемых в операциях. Аргумент sizes задает последовательность, где каждый элемент соответствует одному входному параметру. Элемент может быть объектом-типом соответствующего параметра или целым числом, задающим длину строки. Он также может иметь значение None, если о размере входного параметра ничего нельзя сказать заранее или он предполагается очень большим. Метод должен быть вызван до execute-методов.setoutputsize(size[, column]) Устанавливает размер буфера для выходного параметра из столбца с номером column. Если column не задан, метод устанавливает размер для всех больших выходных параметров. Может использоваться, например, для получения больших бинарных объектов (Binary Large Object, BLOB).
Объект-соединение
Объект-соединение, получаемый в результате успешного вызова функции connect(), должен иметь следующие методы:
close() Закрывает соединение с базой данных.commit() Завершает транзакцию.rollback() Откатывает начатую транзакцию (восстанавливает исходное состояние). Закрытие соединения при незавершенной транзакции автоматически производит откат транзакции.cursor() Возвращает объект-курсор, использующий данное соединение. Если база данных не поддерживает курсоры, модуль сопряжения должен их имитировать.
Под транзакцией понимается группа из одной или нескольких операций, которые изменяют базу данных. Транзакция соответствует логически неделимой операции над базой данных, а частичное выполнение транзакции приводит к нарушению целостности БД. Например, при переводе денег с одного счета на другой операции по уменьшению первого счета и увеличению второго являются транзакцией. Методы commit() и rollback() обозначают начало и конец транзакции в явном виде. Кстати, не все базы данных поддерживают механизм транзакций.
Следует отметить, что в зависимости от реализации DB-API 2.0 модуля, необходимо сохранять ссылку на объект-соединение в продолжение работы курсоров этого соединения. В частности, это означает, что нельзя сразу же получать объект-курсор, не привязывая объект-соединение к некоторому имени. Также нельзя оставлять объект-соединение в локальной переменной, возвращая из функции или метода объект-курсор.
Объекты-типы
DB-API 2.0 предусматривает названия для объектов-типов, используемых для описания полей базы данных:
STRING | Строка и символ |
BINARY | Бинарный объект |
NUMBER | Число |
DATETIME | Дата и время |
ROWID | Идентификатор записи |
None | NULL-значение (отсутствующее значение) |
С каждым типом данных (в реальности это - классы) связан конструктор. Совместимый с DB-API модуль должен определять следующие конструкторы:
Date(год, месяц, день) Дата.Time(час, минута, секунда) Время.Timestamp(год, месяц, день, час, минута, секунда) Дата-время.DateFromTicks(secs) Дата в виде числа секунд secs от начала эпохи (1 января 1970 года).TimeFromTicks(secs) Время, то же.TimestampFromTicks(secs) Дата-время, то же.Binary(string) Большой бинарный объект на основании строки string.
регламентирует интерфейсы модуля расширения
DB API 2. 0 регламентирует интерфейсы модуля расширения для работы с базой данных, методы объекта-соединения с базой, объекта-курсора текущей обрабатываемой записи, объектов различных для типов данных и их конструкторов, а также содержит рекомендации для разработчиков по реализации модулей. На сегодня Python поддерживает через модули расширения многие известные базы данных (уточнить можно на web-странице по адресу http://www.python.org/topics/database/). Ниже рассматриваются почти все положения DB-API за исключением рекомендаций для разработчиков новых модулей.
Основные понятия реляционной СУБД
Реляционная база данных - это набор таблиц с данными.
Таблица - это прямоугольная матрица, состоящая из строк и столбцов. Таблица задает отношение (relation).
Строка - запись, состоящая из полей - столбцов. В каждом поле может содержаться некоторое значение, либо специальное значение NULL (пусто). В таблице может быть произвольное количество строк. Для реляционной модели порядок расположения строк не определен и не важен.
Каждый столбец в таблице имеет собственное имя и тип.
Работа с базой данных из Python-приложения
Далее в лекции на конкретных примерах будет показано, как работать с базой данных из программы на языке Python. Нужно отметить, что здесь не ставится цели постичь премудрости языка запросов (это тема отдельного курса). Простые примеры позволят понять, что при программировании на Python доступ к базе данных не сложнее доступа к другим источникам данных (файлам, сетевым объектам).
Именно поэтому для демонстрации выбрана СУБД SQLite, работающая как под Unix, так и под Windows. Кроме установки собственно SQLite (сайт http://sqlite.org) и модуля сопряжения с Python (http://pysqlite.org), каких-либо дополнительных настроек проводить не требуется, так как SQLite хранит данные базы в отдельном файле: сразу приступать к созданию таблиц, занесению в них данных и произведению запросов нельзя. Выбранная СУБД (в силу своей "легкости") имеет одну существенную особенность: за одним небольшим исключением, СУБД SQLite не обращает внимания на типы данных (она хранит все данные в виде строк), поэтому модуль расширения sqlite для Python проделывает дополнительную работу по преобразованию типов. Кроме того, СУБД SQLite поддерживает достаточно большое подмножество свойств стандарта SQL92, оставаясь при этом небольшой и быстрой, что немаловажно, например, для web-приложений. Достаточно сказать, что SQLite поддерживает даже транзакции.
Еще раз стоит повторить, что выбор учебной базы данных не влияет на синтаксис использованных средств, так как модуль sqlite, который будет использоваться, поддерживает DB-API 2.0, а значит, переход на любую другую СУБД потребует минимальных изменений в вызове функции connect() и, возможно, использования более удачных типов данных, свойственных целевой СУБД.
Схематично работа с базой данных может выглядеть примерно так:
Подключение к базе данных (вызов connect() с получением объекта-соединения).Создание одного или нескольких курсоров (вызов метода объекта-соединения cursor() с получением объекта-курсора).Исполнение команды или запроса (вызов метода execute() или его вариантов).Получение результатов запроса (вызов метода fetchone() или его вариантов).Завершение транзакции или ее откат (вызов метода объекта-соединения commit() или rollback()).Когда все необходимые транзакции произведены, подключение закрывается вызовом метода close() объекта-соединения.
Создание базы данных
Для создания базы данных нужно установить, какие таблицы (и другие объекты, например индексы) в ней будут храниться, а также определить структуры таблиц (имена и типы полей).
Задача - создание базы данных, в которой будет храниться телепрограмма. В этой базе будет таблица со следующими полями:
tvdate,tvweekday,tvchannel,tvtime1,tvtime2,prname,prgenre.
Здесь tvdate - дата, tvchannel - канал, tvtime1 и tvtime2 - время начала и конца передачи, prname - название, prgenre - жанр. Конечно, в этой таблице есть функциональная зависимость (tvweekday вычисляется на основе tvdate и tvtime1), но из практических соображений БД к нормальным формам приводиться не будет. Кроме того, таблица будет создана с названиями дней недели (устанавливает соответствие между номером дня и днем недели):
weekday,wdname.
Следующий сценарий создаст таблицу в базе данных (в случае с SQLite заботиться о создании базы данных не нужно: файл создастся автоматически. Для других баз данных необходимо перед этим создать базу данных, например, SQL-инструкцией CREATE DATABASE):
import sqlite as db
c = db.connect(database="tvprogram") cu = c.cursor()
try: cu.execute(""" CREATE TABLE tv ( tvdate DATE, tvweekday INTEGER, tvchannel VARCHAR(30), tvtime1 TIME, tvtime2 TIME, prname VARCHAR(150), prgenre VARCHAR(40) ); """) except db.DatabaseError, x: print "Ошибка: ", x c.commit()
try: cu.execute(""" CREATE TABLE wd ( weekday INTEGER, wdname VARCHAR(11) ); """) except db.DatabaseError, x: print "Ошибка: ", x c.commit() c.close()
Здесь просто исполняются SQL-инструкции, и обрабатывается ошибка базы данных, если таковая случится (например, при попытке создать таблицу с уже существующим именем). Для того чтобы таблицы создавались независимо, используется commit().
Кстати, удалить таблицы из базы данных можно следующим образом:
import sqlite as db
c = db.connect(database="tvprogram") cu = c.cursor()
try: cu.execute("""DROP TABLE tv;""") except db.DatabaseError, x: print "Ошибка: ", x c.commit()
try: cu.execute("""DROP TABLE wd;""") except db.DatabaseError, x: print "Ошибка: ", x c.commit() c.close()
Выборки из базы данных
Базы данных создаются для удобства хранения и извлечения больших объемов. Следующий нехитрый пример позволяет проверить, правильно ли были введены в таблицу дни недели:
import sqlite as db
c = db.connect(database="tvprogram") cu = c.cursor() cu.execute("SELECT weekday, wdname FROM wd ORDER BY weekday;") for i, n in cu.fetchall(): print i, n
Если все было сделано правильно, получится:
0 Воскресенье 1 Понедельник 2 Вторник 3 Среда 4 Четверг 5 Пятница 6 Суббота 7 Воскресенье
Несложно догадаться, как сделать выборку телепрограммы:
import sqlite as db
c = db.connect(database="tvprogram") cu = c.cursor() cu.execute(""" SELECT tvdate, tvtime1, wd.wdname, tvchannel, prname, prgenre FROM tv, wd WHERE wd.weekday = tvweekday ORDER BY tvdate, tvtime1;""") for rec in cu.fetchall(): dt = rec[0] + rec[1] weekday = rec[2] channel = rec[3] name = rec[4] genre = rec[5] print "%s, %02i.%02i.%04i %s %02i:%02i %s (%s)" % ( weekday, dt.day, dt.month, dt.year, channel, dt.hour, dt.minute, name, genre)
В этом примере в качестве типа для даты и времени используется тип из mx.DateTime. Именно поэтому стало возможным получить год, месяц, день, час и минуту обращением к атрибуту. Кстати, datetime-объект стандартного модуля datetime имеет те же атрибуты. В общем случае для даты и времени может использоваться другой тип, поэтому если получаемые из базы даты будут проходить более глубокую обработку, их следует переводить во внутреннее представление сразу после получения по запросу. Тем самым тип даты из модуля DB-API не будет влиять на другие части программы.
В рамках данной лекции были
В рамках данной лекции были рассмотрены возможности связи Python с системами управления реляционными базами данных. Для Python разработан стандарт, называемый DB-API (версия 2.0), которого должны придерживаться все разработчики модулей сопряжения с реляционными базами данных. Благодаря этому API код прикладной программы становится менее зависимым от марки используемой базы данных, его могут понять разработчики, использующие другие базы данных. Фактически DB-API 2.0 описывает имена функций и классов, которые должен содержать модуль сопряжения с базой данных, и их семантику. Модуль сопряжения должен содержать класс объектов-соединений с базой данных и класс для курсоров - специальных объектов, через которые происходит коммуникация с СУБД на прикладном уровне.
Здесь была использована СУБД SQLite и соответствующий модуль расширения Python для сопряжения с этой СУБД - sqlite, так как он поддерживает DB-API 2.0 и достаточно прост в установке. С его помощью были продемонстрированы основные приемы работы с базой данных: создание и наполнение таблиц, выполнение выборок и анализ полученных данных.
В конце лекции дан список других пакетов и модулей, которые позволяют Python-программе работать со многими современными СУБД.
Знакомство с СУБД
Допустим, программное обеспечение установлено правильно, и можно работать с модулем sqlite. Стоит посмотреть, чему будут равны константы:
>>> import sqlite >>> sqlite.apilevel '2.0' >>> sqlite.paramstyle 'pyformat' >>> sqlite.threadsafety 1
Отсюда следует, что sqlite поддерживает DB-API 2.0, подстановка параметров выполняется в стиле строки форматирования языка Python, а соединения нельзя совместно использовать из различных потоков управления (без блокировок).