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

LXF123:Python

Материал из Linuxformat
Перейти к: навигация, поиск
Python Заставим Web доставлять нужное содержимое вам на блюдечке

Содержание

Python: Роемся в XML

Часть 4: Мы вскроем API Сети посредством XML. Ваш отважный проводник по глубинам Python, Ник Вейч, не успокоился, пока не откопал Digg.

Ранее в этом цикле статей в качестве интерфейса для наших web-объектов и работы с ними мы использовали существующий API-код. Это экономит кучу времени, но иногда и ограничивает, делая нас зависимыми от других. И вообще, зачем беспокоиться о импорте целого модуля API, если требуется всего несколько методов?

Итак, на сей раз, невзирая на то, что есть весьма удачные API для Digg – хотя и слегка несвежие – мы создадим вместо них свой собственный. Ну, типа того. Мы не собираемся охватывать всю функциональность Digg, но освоим достаточно, чтобы при желании вы смогли создать полноценный API.

Многие сайты используют конкретный способ взаимодействия со своими API, а некоторые – даже несколько способов. Самые популярные – JSON и XML. JSON проще и легко интегрируется с JavaScript, поэтому встречается часто. XML больше, массивнее и приятнее на вид. К тому же его чуть легче сопровождать; правда, в итоге вы получаете копну ответов даже на простой запрос. Но все равно, его-то мы здесь и используем.

Если честно, большой разницы между ними нет, но XML – практически универсальный язык для web, и если вы научитесь работать с вызовами API через XML, это сослужит вам хорошую службу.

Python хорошо справляется с XML и имеет готовые модули для работы с ним... но мы забегаем вперед. Первым делом определим, как мы будем взаимодействовать с API Digg. И, подобно многим запасливым социальным web-сайтам, Digg располагает массой документации для программистов о том как использовать API. Ура!

Большая кнопка «Digg it»

Зайдем на сайт Digg API (http://apidoc.digg.com) и посмотрим, что там предлагается. Доступ к API осуществляется посредством запросов к главному web-сайту. Возможно, вы уже знакомы с этим – имеется набор значений, передаваемых на web-сервер для обработки; начинаются они с ? и разделяются &. Чтобы это опробовать, не нужно даже писать ни строчки кода самостоятельно: достаточно набрать URL и ввести его в браузере. Поскольку мы собираемся использовать стандартный XML-отклик, то браузер типа Firefox отобразит полученный XML-код прямо в своем окне, не требуя предварительно сохранить его в файл, что есть великое благо. В конце концов, нуднее, чем ввод сложного запроса и получение ошибки в ответ, может быть только необходимость загрузки файла в текстовый редактор ради выяснения, что он не работает.

Для активации конкретных функций API Digg использует специальные конечные точки (endpoints), или URL-пути. Например, если необходимо найти список последний горячих новостей [hot list], перейдите по адресу http://services.digg.com/stories/hot.

Опробуйте это в вашем браузере – и, однако, получите сообщение об ошибке. Единственной оговоркой Digg при предоставлении данного сервиса является то, что приложение, делающее запрос, должно иметь свой ключ (или API-ключ). Это распространено среди web-служб – они хотят иметь способ определить конкретного клиента, и вовсе не из подлости: если что-то пойдет не так и некто будет опрашивать сервер каждые 10 миллисекунд, должна быть возможность блокировать его, не останавливая API-сервис как целое.


В этом отношении Digg немного необычен – он не требует полной регистрации ключа API: просто введите URL вашего web-проекта или сайта с исходным текстом клиентского приложения. И если мы вновь обратимся к Digg, уже с ключом http://services.digg.com/stories/hot/?appkey=http://www.linuxformat.ru, экран заполнится структурированным текстом. Символ ? в URL означает запрос, а нам надо передать несколько значений. Стандартный формат для этого – список пар «ключ–значение», разделенных амперсандом, &. Если мы теперь поменяем наш URL на http://services.digg.com/stories/hot/?appkey=http://www.linuxformat.ru&count=1, то увидим только первую статью. Естественно, можно указать другое число или любые другие переменные, принимаемые этим запросом. Как узнать, что это за переменные? Ну, придется попросить Digg опубликовать их. В данном случае вы найдете аргументы для конечных точек внизу страницы http://apidoc.digg.com/ListStories.

Digg и Python

Итак, мы ознакомились с основами Digg API. Можно вводить URL и получатьв замен XML-код. Здорово; но как бы провернуть это программно? Что же, для начала необходим способ открытия URL. Стандартный модуль Python, urllib, может сделать это для нас, поэтому запустим оболочку Python (откройте окно терминала и просто введите python) и посмотрим, что можно сделать.

>>> url=’http://services.digg.com/stories/hot/?’
>>> appkey=’http://www.linuxformat.ru’
>>> import urllib
>>> diggargs ={ ‘count’: 1, ‘appkey’: appkey}
>>> foo=urllib.urlencode(diggargs)
>>> request = url+foo
>>> request
‘http://services.digg.com/stories/hot/?count=1&appkey=http%3A%2F%2Fwww.linuxformat.ru’
>>>

Тут требуется небольшое пояснение. Сперва мы сохраняем базовый URL и ключ приложения в виде строк, потому что далее они будут часто использоваться. После импорта модуля urllib нам надо определить некоторые переменные для передачи Digg. Здесь мы воспользовались словарем Python. Это простая конструкция, заключенная в фигурные скобки – она хранит пары «ключ–значение» и ведет себя схоже со списками. Сперва указывается ключ, затем следует двоеточие, а далее значение. Тип значения может быть любым из распознаваемых Python, но чаще всего это строки или целые числа.

Зачем нужен словарь?

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

Массивный отклик

Запрос [request] строится простым объединением базового URL и строки-запроса. Любопытства ради, можете просто ввести имя этой переменной, и Python напечатает ее значение: в данном случае – с трудом читаемый URL, результат использования верной кодировки. Итак, что же мы получим, подключившись к серверу с этим запросом? Ответ должен выглядеть примерно так:

>>> response = urllib.urlopen(request)
>>> response.read()
‘<?xml version=”1.0” encoding=”utf-8” ?>\n<stories timestamp=”1251657872” total=”13711” offset=”0”
count=”1”>\n <story link=”http://myfirstfail.com/2009/08/24/funny-baby-photos-im-really-excited-to-be-here/” submit_
date=”1251584276” diggs=”125” id=”15364594” comments=”18” href=”http://digg.com/people/I_m_REALLY_Excited_to_be_
Here” status=”upcoming” media=”images”>\n <description></description>\n <title>I\'m REALLY Excited to be Here!</title>\n
<user name=”sungoddess808” registered=”1201436227” profileviews=”40965” fullname=”Sunshine ” icon=”http://digg.
com/users/sungoddess808/l.png” />\n <topic name=”People” short_name=”people” />\n <container name=”Offbeat”
short_name=”offbeat” />\n <thumbnail originalwidth=”500” originalheight=”426” contentType=”image/jpeg” 
src=”http://digg.com/people/I_m_REALLY_Excited_to_be_Here/t.jpg” width=”80”
height=”80” />\n <shorturl short_url=”http://digg.com/d312T22” view_count=”582” />\n </story>\n</stories>‘

Функция urlopen возвращает файлоподобный объект, с которым можно обращаться как с любым другим файл-объектом. Это полезно, если вы ожидаете в ответ огромный объем данных, но я сомневаюсь, что из-за экспериментов нашего урока Digg заполнит всю вашу память. Далее, response.read() просто выводит «файл», и можно совместить эти две команды:

>>> response = urllib.urlopen(request).read()

Такой трюк применим к большинству объектов Python, хотя затрудняет понимание кода.

Наша уловка сработала, и мы получили ответ – кучу XML-кода для обработки. Чтобы получить из нее XML-объект, необходимо задействовать кое-какие методы из XML-модуля Python, а именно

>>> from xml.dom import xml.minidom
>>> x = minidom.parseString(response)

Выуживание данных из объектов и построение из этой информации правильно структурированного XML-файла известно в мире XML как маршалинг [marshalling, упорядочивание]. Но нам-то нужна обратная операция – создать объект Python из данных. Нижеследующий кусок кода, вероятно, один из наиболее часто копируемых, по крайней мере для Python и XML. По-моему, он восходит к коду, написанному Марком Пилгримом [Mark Pilgrim] (автор книги В глубь языка Python, http://ru.diveintopython.org), а кто несогласен – пишите на известный адрес...

 class Bag: pass
 def unmarshal(element):
   rc = Bag()
   if isinstance(element, minidom.Element):
    for key in element.attributes.keys():
      setattr(rc, key, element.attributes[key].value)
 
 childElements = [e for e in element.childNodes \
     if isinstance(e, minidom.Element)]
 if childElements:
   for child in childElements:
     key = child.tagName
     if hasattr(rc, key):
      if type(getattr(rc, key)) <> type([]):
        setattr(rc, key, [getattr(rc, key)])
      setattr(rc, key, getattr(rc, key) + [unmarshal(child)])
     elif isinstance(child, minidom.Element) and \
        (child.tagName == ‘Details’):
      # Делаем первый элемент Details ключом
      setattr(rc,key,[unmarshal(child)])
     else:
      setattr(rc, key, unmarshal(child))
  else:
    text = “”.join([e.data for e in element.childNodes \
       if isinstance(e, minidom.Text)])
    setattr(rc, ‘text’, text)
 return rc

Опять немного поясним. Первая странность – класс Bag, который вроде и описан, но пуст. В Python такое допустимо – и правда, как узнать, что будет в классе, пока не распакованы данные? Это прекрасно демонстрирует гибкость Python; он допускает классы объектов, которые можно создавать в процессе выполнения.


Анархия кода?

Функция unmarshal – просто рекурсивная процедура, которая пошагово проходит каждый узел дерева XML DOM и собирает из него объект Python. Желая создать модуль API для Python, выполняющий интерпретацию вывода Digg, вы можете делать это более структурировано и осмысленно, потому что структура XML известна заранее. А приведенный метод – из разряда всеобъемлющих, с тем недостатком, что получающийся объект все еще неуклюж. Однако небольшая пост-обработка расставит все по местам.

Мы получили список контейнеров story, заключенных в контейнер с именем stories. Каждый из них имеет свои собственные подузлы для комментариев, ID, URL и так далее. Если мы хотим взглянуть на объекты story, следует просто в цикле пройтись по контейнеру stories, вот так:

>>> for item in bar.stories.story:
>>> print item.id, item.link, item.title.text

Ничто не мешает добавлять данные в эту структуру программно. Что если, например, полюбопытствовать, где опубликованы все эти истории? На это имеется полезная свободная библиотека с именем GeoIP, выставляющая соответствие между IP-адресами и странами.

Добавляем данные

Модуль GeoIP имеется в основных дистрибутивах, или можно загрузить его с MaxMind (http://www.maxmind.com/app/python). Все очень просто: вы создаете объект GeoIP, затем используете его методы для определения кода или названия страны по имени домена web-сервера. Вот простой пример:

>>> import GeoIP
>>> geo=GeoIP.new(GeoIP.GEOIP_STANDARD)
>>> geo.country_name_by_name(‘google.com’) ‘United States’

Проще некуда. К сожалению, ему необходим только домен, а не весь URL. Но мы можем импортировать еще один стандартный модуль Python, под названием urlparse (http://www.python.org/doc/2.5.2/lib/moduleurlparse.html), который разбивает URL на части.

>>> import urlparse
>>> for item in bar.stories.story:
>>> item.country=geo.country_name_by_name(urlparse.urlparse(item.link).netloc)
>>> print item.country

Мы переписали наш цикл и создали внутри него новое свойство объекта item с именем country. Передача вырезанного имени домена от функции urlparse к функции GeoIP выдает название страны в виде строки.

Можно зайти дальше и на основании значений country проследить частоту появления различных стран, скажем, в первой сотне самых посещаемых сайтов. Это весьма легко сделать: объявим пустой словарь, затем будем добавлять единицу к величине ключа данной страны по ходу цикла обработки. Если ключа не существует, то используем значение по умолчанию – ноль. Словарь потом можно преобразовать в упорядоченный список и построить гистограмму частоты появления стран.


 #!/usr/bin/python
 
 import urllib
 from xml.dom import minidom
 import urlparse, GeoIP, operator
 
 url=’http://services.digg.com/stories/hot/?’
 appkey=’http://linuxformat.co.uk’
 geo=GeoIP.new(GeoIP.GEOIP_MEMORY_CACHE)
 
 class Bag: pass
 def unmarshal(element):
   rc = Bag()
   if isinstance(element, minidom.Element):
     for key in element.attributes.keys():
       setattr(rc, key, element.attributes[key].value)
 
   childElements = [e for e in element.childNodes \
         if isinstance(e, minidom.Element)]
   if childElements:
     for child in childElements:
       key = child.tagName
       if hasattr(rc, key):
         if type(getattr(rc, key)) <> type([]):
           setattr(rc, key, [getattr(rc, key)])
         setattr(rc, key, getattr(rc, key) + [unmarshal(child)])
       elif isinstance(child, minidom.Element) and \
            (child.tagName == ‘Details’):
         # делаем первый элемент Details ключом
         setattr(rc,key,[unmarshal(child)])
       else:
         setattr(rc, key, unmarshal(child))
    else:
      text = “”.join([e.data for e in element.childNodes \
        if isinstance(e, minidom.Text)])
      setattr(rc, ‘text’, text)
    return rc
 
 diggargs ={ ‘count’: 100, ‘appkey’: appkey}
 foo=urllib.urlencode(diggargs)
 request = url+foo
 response = urllib.urlopen(request).read()
 
 x = minidom.parseString(response)
 bar = unmarshal(x)
 
 hist = {}
 for item in bar.stories.story:
 
   item.country=geo.country_name_by_name(urlparse.urlparse(item.link).netloc)
   hist[item.country]=hist.get(item.country, 0) +1
 
 sorted = sorted(hist.items(), key=operator.itemgetter(1),reverse=True)
 print sorted

Сегодня мы освоили солидный кусок, хотя занимались всего одной точкой входа для Digg. Для более полезного API вы, возможно, захотите создать класс и несколько объектов, чтобы описать пользователей, заметки и тому подобное, и примените описанные здесь советы, чтобы заполнить их. Digg – это, в основном, трафик в одну сторону, но в следующий раз мы рассмотрим также и запись данных, создав графический клиент Flickr.

Персональные инструменты
купить
подписаться
Яндекс.Метрика