Перейти до вмісту

Пориньте у Python 3/Веб-сервіси HTTP

Матеріал з Вікіпідручника

Збуджений розум робить подушку незручною. (A ruffled mind makes a restless pillow.)
Шарлотта Бронте

Пірнаймо!

[ред.]

Філософськи, веб-сервіси HTTP можна описати як обмін даними з віддаленим сервером використовуючи лише засоби протоколу HTTP. Якщо ви хочете отримати дані з сервера - використовуйте HTTP GET. Якщо хочете відправити дані на сервер - HTTP POST. API деяких передових веб-сервісів також дозволяють створення, модифікацію та видалення даних за допомогою HTTP PUT, та HTTP DELETE. Це все. Ніяких регістрів, конвертів, обгорток, тунелювання. "Дієслова" вбудовані в протокол HTTP (GET, POST, PUT та DELETE) напряму пов’язані з операціями прикладного рівня для отримання, створення, модифікації та видалення даних.

Основна перевага такого підходу - простота, а простота зарекомендувала себе дуже популярною. Дані - зазвичай XML або JSON можуть зберігатись статично, чи генеруватись динамічно скриптом на стороні сервера, і всі основні мови програмування (включаючи Python звісно!) включать бібліотеку для доступу до них через HTTP. Зневадження також набагато простіше, бо так як кожен ресурс веб-сервісу має унікальну адресу в формі URL, ви можете просто відкрити його в веб-браузері щоб одразу побачити чисті дані.

Приклади веб-сервісів:

Python 3 постачається з двома бібліотеками для взаємодії з веб-сервісами HTTP:

  • http.client - низькорівнева бібліотека яка реалізує RFC2616 (протокол HTTP)
  • urllib.request - шар абстракції над http.client. Він надає стандартне API для взаємодії як з HTTP, так і з FTP серверами, автоматично переходить за перенаправленнями, і справляється з деякими типовими видами HTTP аутентифікації.

Так яку з них варто використовувати? Жодну! Натомість, ви повинні використовувати httplib2, сторонню бібліотеку з відкритим кодом, яка реалізує HTTP повніше ніж http.client і надає кращий рівень абстракції ніж urllib.request.

Щоб зрозуміти чому httplib2 - правильний вибір, вам спершу потрібно зрозуміти HTTP.

Насправді httplib2 був таким під час написання цієї книжки. Але в 2011-тому році з’явилась нова, краща бібліотека, requests. Та все одно читання цього розділу може бути корисним, тому що ви дізнаєтесь багато про особливості HTTP.


* * *


Особливості HTTP

[ред.]

Існує п’ять важливих особливостей, які повинні підтримуватись всіма клієнтами.

Кешування

[ред.]

Найважливіше що потрібно розуміти про будь-який веб-сервіс це те, що доступ до мережі неймовірно дорогий. І я не маю на увазі долари й центи, хоча, звісно, трафік теж не безкоштовний. Я маю на увазі, що, щоб відкрити з’єднання, послати запит, отримати відповідь від сервера, потрібно неймовірно багато часу. Навіть на найшвидших з’єднаннях латентність (час який проходить від відправлення запиту до отримання відповіді) може перевищити ваші очікування. Маршрутизатор працює погано, пакети губляться, проміжне проксі перебуває під атакою - завжди знайдеться причина, і ви нічого не зможете з цим зробити.

Cache-Control: max-age означає "не чіпайте мене аж до наступного тижня"

HTTP був створений з думкою про кешування. Існує цілий клас пристроїв (які називаються "кешуючими проксі"), єдине завдання яких - знаходитись між вами і рештою світу та мінімізувати навантаження на мережу. Ваша компанія чи провайдер інтернету майже напевне підтримує кешуючі проксі, хоча ви можете про них і не здогадуватись. Вони працюють, тому що кешування вбудоване в протокол HTTP.

Ви відкриваєте в своєму браузері сторінку яка може містити якісь зображення. Коли браузер просить віддати це зображення сервер додає у відповідь такі заголовки HTTP:

HTTP/1.1 200 OK
Date: Sun, 31 May 2009 17:14:04 GMT
Server: Apache
Last-Modified: Fri, 22 Aug 2008 04:28:16 GMT
ETag: "3075-ddc8d800"
Accept-Ranges: bytes
Content-Length: 12405
Cache-Control: max-age=31536000, public
Expires: Mon, 31 May 2010 17:14:04 GMT
Connection: close
Content-Type: image/jpeg

Такі заголовки Cahce-Control та Expires повідомляють вашому браузеру (і кожному кешуючому проксі перед ним), що зображення може зберігатись в кеші ще рік. Рік! І якщо через рік ви відвідаєте іншу сторінку, яка теж включає це зображення, ваш браузер візьме це зображення з кешу не створюючи ніякого навантаження на мережу взагалі.

І навіть краще. Скажімо ваш браузер видалив це зображення з якоїсь причини. Можливо закінчився вільний простір на диску, можливо ви вручну очистили кеш. Без різниці. Але заголовки HTTP кажуть, що це зображення може кешуватись публічними кешуючими проксі. (Якщо говорити технічно точно, то важливо не те що кажуть заголовки, а те чого вони не кажуть. Заголовок Cache-Control не містить ключового слова private, тому ці дані кешуються за замовчуванням.) В кешуючих проксі передбачені гектари місця для зберігання даних, ймовірно, на порядок більше ніж може собі дозволити ваш локальний браузер.

Якщо ваш інтернет-провайдер підтримує кешуючий проксі-сервер, він може все ще зберігати закешоване зображення. Коли ви відвідуєте сторінку вдруге, браузер подивиться в локальний кеш, не знайде там потрібної картинки і зробить запит до мережі намагаючись завантажити її з віддаленого сервера. Але якщо кешуючий проксі досі зберігає копію картинки, він перехопить запит і віддасть картинку зі свого кешу. Це означає, що ваш запит ніколи не досягне віддаленого сервера і, можливо, навіть не покине мережу вашої компанії. Це пришвидшить завантаження (менша кількість передач пакетів між маршрутизаторами) і зекономить гроші компанії (менше даних буде завантажено з зовнішнього світу).

Кешування HTTP працює лише, коли кожен робить свою справу. З одного боку, сервери повинні відправляти з відповіддю коректні заголовки. З іншого боку, клієнти повинні розуміти і зважати на ті заголовки, перш ніж запитувати ті ж дані вдруге. Проксі посередині не є панацеєю, вони розумні рівно настільки, наскільки їм дозволяють клієнти й сервери.

Стандартні бібліотеки мови Python не підтримують кешування, зате httplib2 підтримує.

Перевірка Last-Modified

[ред.]

Деякі дані не змінюються ніколи, а деякі постійно. Між ними ще є широкий діапазон даних, які можуть змінитись, але не змінились. Фід CNN.com оновлююється щохвилини, а фід мого блогу може не змінюватись днями а то й тижнями. В другому випадку я не хочу казати клієнтам кешувати мій фід цілий тиждень, бо тоді, коли я нарешті щось опублікую, люди не дізнаються про це ще тиждень (тому що вони зважають на заголовки, які кажуть "не перевіряти цей фід ще тиждень"). З іншого боку я не хочу щоб клієнти завантажували ввесь мій фід щогодини аби перевірити чи не з’явилось нічого нового.

304: Not Modified означає "новий день, та сама х*ня"

Протокол HTTP має рішення і для цього. Коли ви запитуєте дані вперше, сервер може повернути заголовок Last-Modified. Він містить інформацію про час останньої зміни даних.

HTTP/1.1 200 OK
Date: Sun, 31 May 2009 17:14:04 GMT
Server: Apache
Last-Modified: Fri, 22 Aug 2008 04:28:16 GMT
ETag: "3075-ddc8d800"
Accept-Ranges: bytes
Content-Length: 12405
Cache-Control: max-age=31536000, public
Expires: Mon, 31 May 2010 17:14:04 GMT
Connection: close
Content-Type: image/jpeg

Коли ви запитуєте ті ж дані другий (чи третій, чи четвертий) раз, ви можете з запитом відправити заголовок If-Modified-Since в якому вказати час, який повернув вам сервер минулого разу. Якщо з того часу дані змінились, сервер поверне вам нові дані, з кодом статусу 200. Але, якщо дані з того часу не змінювались, сервер поверне спеціальний код статусу HTTP 304, який означає "дані не змінились з того моменту коли ви запитували їх востаннє". Можете перевірити це з командного рядка, використовуючи curl:

you@localhost:~$ curl -I -H "If-Modified-Since: Fri, 22 Aug 2008 04:28:16 GMT" http://wearehugh.com/m.jpg
HTTP/1.1 304 Not Modified
Date: Sun, 31 May 2009 18:04:39 GMT
Server: Apache
Connection: close
ETag: "3075-ddc8d800"
Expires: Mon, 31 May 2010 18:04:39 GMT
Cache-Control: max-age=31536000, public

Це дуже добре, бо коли сервер повертає статус 304, він не відсилає заново всі дані. Від відсилає лише код статусу. Навіть якщо термін зберігання копії в кеші закінчився, last-modified гарантує, що ви не будете завантажувати ті самі дані двічі, якщо вони не змінились. (Як додатковий бонус, з відповіддю 304 також приходять заголовки кешування. Проксі зберігатимуть копію даних навіть якщо в них офіційно закінчиться термін зберігання, сподіваючись що якщо дані не змінились, то з наступним запитом повернеться відповідь з кодом 304 і оновленими термінами кешування.)

Стандартні бібліотеки мови Python не підтримують перевірку часу останньої модифікації, зате httplib2 підтримує.

Перевірка ETag

[ред.]

ETag-и це альтернативний спосіб досягти того ж самого, що й за допомогою перевірки last-modified. З Etags, сервер відправляє разом з даними хеш-код в заголовку ETag. (Як точно визначається цей хеш цілком залежить від сервера. Єдина вимога - він повинен змінюватись, коли змінюються дані.) Фонове зображення, що отримується з diveintomark.org має заголовок ETag.

HTTP/1.1 200 OK
Date: Sun, 31 May 2009 17:14:04 GMT
Server: Apache
Last-Modified: Fri, 22 Aug 2008 04:28:16 GMT
ETag: "3075-ddc8d800"
Accept-Ranges: bytes
Content-Length: 12405
Cache-Control: max-age=31536000, public
Expires: Mon, 31 May 2010 17:14:04 GMT
Connection: close
Content-Type: image/jpeg
ETag означає "немає нічого нового під сонцем".

Коли наступного разу ви зробите запит тих самих даних, то включите хеш ETag в заголовок If-None-Match свого запиту. Якщо дані не змінились, сервер відішле нам назад код статусу 304. Так само як і при перевірці last-modified сервер відправить назад лише код 304, він не буде відправляти ті ж дані повторно. Включаючи хеш ETag в свій другий запит, ви повідомляєте сервер, що немає потреби пересилати ті ж самі дані повторно, якщо вони все ще відповідають хешу, так як ми зберегли дані з останнього запиту.

Знову за допомогою curl:

you@localhost:~$ curl -I -H "If-None-Match: \"3075-ddc8d800\"" http://wearehugh.com/m.jpg
HTTP/1.1 304 Not Modified
Date: Sun, 31 May 2009 18:04:39 GMT
Server: Apache
Connection: close
ETag: "3075-ddc8d800"
Expires: Mon, 31 May 2010 18:04:39 GMT
Cache-Control: max-age=31536000, public

ETag-и зазвичай поміщуються в лапки, але лапки є частиною значення. Це означає, що потрібно відправляти й лапки назад на сервер в заголовку If-None-Match.

Стандартні бібліотеки мови Python не підтримують перевірку хешу ETags, зате httplib2 підтримує.

Стиснення

[ред.]

Коли ми говоримо про веб-сервіси HTTP, ми майже завжди говоримо про передачу текстових даних туди-сюди по мережі. Можливо це XML, можливо JSON, можливо це всього лиш простий текст. Незалежно від формату текст чудово стискається. Наприклад фід в розділі про XML має розмір 3070 байт, але займатиме лише 941 байт після gzip стиснення. А це всього лише 30% від початкового розміру!

HTTP підтримує кілька алгоритмів стиснення. Двома найбільш вживаними є gzip та deflate. Коли ви запитуєте ресурс через HTTP, ви можете попросити сервер прислати його в стиснутому форматі. Для цього потрібно включити в запит заголовок Accept-encoding, в якому перелічити підтримувані алгоритми стиснення. Якщо сервер теж підтримує один з тих алгоритмів, він відправить вам стиснені дані (з заголовком Content-encoding який визначає використаний алгоритм). Після цього розкодування даних в ваших руках.

Важлива підказка для тих, хто розробляє на стороні сервера: переконайтесь, що стиснена версія ресурсу має відмінний від нестисненої версії Etag. Інакше кешуючі проксі можуть заплутатись і передати стиснену версію клієнтам, які її не підтримують. Прочитайте обговорення помилки Apache №39727, щоб дізнатись додаткові деталі цієї тонкої проблеми.

Стандартні бібліотеки мови Python не підтримують стиснення, зате httplib2 підтримує.

Перенаправлення

[ред.]

Круті URI не змінюються, але багато URI дуже не круті. Веб-сайти реорганізовуються, сторінки переміщуються на нові адреси. Навіть веб-сервіси переорганізовуються. Синдикований фід за адресою http://example.com/index.xml може переміститись в http://example.com/xml/atom.xml. Чи ввесь домен може переміститись, в процесі росту і перебудови організації. http://www.example.com/index.xml може стати http://server-farm-1.example.com/index.xml.

Location означає “дивись сюди!”

Щоразу як ви запитуєте будь-який ресурс з HTTP сервера, сервер додає код статусу в свою відповідь. Код статусу 200 означає "все нормально, ось сторінка яку ви просили". Код статусу 404 означає "сторінка не знайдена". (Ви напевне вже бачили помилки 404 переглядаючи веб). Коди статусу від 300 і до 400 вказують на якусь форму перенаправлення.

В HTTP є кілька різних способів позначення того що ресурс переміщено. Двома найбільш типовими є коди статусу 302 та 301. Код статусу 302 означає тимчасове перенаправлення, "уупс, ми тимчасово переїхали сюди" (в такому разі заголовок Location містить тимчасову адресу). Код статусу 301 - постійне перенаправлення: "уупс, ми назавжди переїхали" (і теж передає нову адресу в заголовку Location). Якщо ви отримали у відповідь код 302 і нову адресу, специфікація HTTP каже що потрібно використати нову адресу для того щоб отримати те що потрібно, але коли вам знадобиться цей ресурс наступного разу, ви повинні спершу спробувати стару адресу. Але якщо ви отримали код 301 і нову адресу, ви повинні використовувати цю нову адресу й надалі.

Модуль urllib.request автоматично "переходить" за перенаправленнями коли отримує відповідний код статусу з сервера, але не каже вам що він так зробив. В кінцевому результаті ви отримаєте ті дані про які просили, але не будете знати що бібліотека прослідувала по перенаправленню за вас. Тому ви продовжите використовувати стару адресу, і кожного разу urllib.request буде переходити за тим самим перенаправленням. Іншими словами він працює з постійними перенаправленнями так само як з тимчасовими. А це означає два запити замість одного, що погано як для сервера так і для вас.

httplib2 опрацьовує постійні перенаправлення для нас. Він не тільки може повідомити про те що відбулось перенаправлення, він самостійно слідкує за ними, і може автоматично змінити URL перед тим як їх запитувати.


* * *


Як не варто отримувати дані через HTTP

[ред.]

Скажімо нам потрібно завантажити через HTTP наприклад фід Atom. А так як це фід, його зазвичай не завантажують один раз, а часто і регулярно. (Більшість програм що читають RSS перевірятимуть зміни щогодини.) Давайте спочатку зробимо косо-криво аби живо, а потім подивимось як можна зробити краще.

>>> import urllib.request
>>> a_url = 'http://diveintopython3.org/examples/feed.xml'
>>> data = urllib.request.urlopen(a_url).read()

Завантаження будь-чого через HTTP в Python страшенно легко, і робиться одним рядком коду. Модуль urllib.request має гарну функцію urlopen() яка бере адресу потрібної сторінки, і повертає файлоподібний об'єкт з якого можна просто взяти ввесь вміст сторінки за допомогою методу read(). Простіше вже ніяк.

>>> type(data)
<class 'bytes'>
>>> print(data)
<?xml version='1.0' encoding='utf-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
  <title>dive into mark</title>
  <subtitle>currently between addictions</subtitle>
  <id>tag:diveintomark.org,2001-07-29:/</id>
  <updated>2009-03-27T21:56:07Z</updated>
  <link rel='alternate' type='text/html' href='http://diveintomark.org/'/>
  

Метод urlopen().read() завжди повертає байтовий об'єкт а не рядок. Пам'ятаєте, байти це байти, а символи це абстракція. HTTP сервери не мають справи з абстракціями. Якщо ви запитуєте ресурс, ви отримуєте байти. Якщо ви хочете текст, вам потрібно визначити кодування його символів, і явно зробити розкодування.

То що тут такого поганого? Для швидкого одноразового тесту під час розробки - нічого. Я так постійно роблю. Мені потрібен був вміст фіду, я його отримав. Такий самий підхід працює з будь-якою веб сторінкою. Але як тільки ви починаєте думати в термінах веб-сервісу, ви захочете отримати регулярний доступ (наприклад оновлювати фід щогодини), тоді такий метод буде неефективним, а також невихованим.


* * *


Що на дроті?

[ред.]

Щоб побачити чому це неефективно та невиховано, давайте ввімкнемо зневаджувальні можливості бібліотеки HTTP, і подивимось що відправляється "дротами" (тобто через мережу).

>>> from http.client import HTTPConnection 
>>> HTTPConnection.debuglevel = 1

Як я вже згадував на початку цього розділу, urllib.request працює на основі іншої стандартної бібліотеки мови Python, http.client. Зазвичай вам взагалі не потрібно буде чіпати http.client напряму. (Модуль urllib.request імпортує її автоматично). Але ми імпортуємо її тут, щоб можна було змінити зневаджувальний прапорець класу HTTPConnection, який urllib.request використовує для з'єднання з HTTP сервером.

>>> from urllib.request import urlopen 
>>> response = urlopen('http://diveintopython3.org/examples/feed.xml')

Тепер, коли встановлено зневаджувальний прапорець, інформація запитів та відповідей HTTP буде роздруковуватись в режимі реального часу. Як ви можете бачити, коли ви запитуєте фід Atom, модуль urllib.request відправляє п'ять рядків на сервер.

send: b'GET /examples/feed.xml HTTP/1.1

Перший рядок задає версію HTTP яку ви використовуєте та шлях до ресурсу (без доменного імені).

Host: diveintopython3.org

Другий рядок задає доменне ім'я в якого ви запитуєте цей фід.

Accept-Encoding: identity

Третій рядок задає алгоритми стиснення які підтримує клієнт. Як я згадав раніше, urllib.request за замовчуванням не підтримує стиснення.

User-Agent: Python-urllib/3.1'

Четвертий рядок задає назву бібліотеки що здійснює запит. За замовчуванням там написано Python-urllib та номер версії. urllib.request та httplib2 підтримують зміну значення заголовку User-Agent додаванням його до запиту (що перевстановить значення за замовчуванням).

Connection: close
reply: 'HTTP/1.1 200 OK'
... подальші дані зневадження опущено ...
Ми завантажуємо 3070 байт в той час як могли завантажувати всього лиш 941.

Тепер давайте поглянемо що сервер прислав у відповідь.


# продовжуємо з попереднього прикладу
>>> print(response.headers.as_string())  
Date: Sun, 31 May 2009 19:23:06 GMT  
Server: Apache
Last-Modified: Sun, 31 May 2009 06:39:55 GMT  
ETag: "bfe-93d9c4c0"  
Accept-Ranges: bytes
Content-Length: 3070  
Cache-Control: max-age=86400  
Expires: Mon, 01 Jun 2009 19:23:06 GMT
Vary: Accept-Encoding
Connection: close
Content-Type: application/xml

① Значення response повернене функцією urllib.request.urlopen() містить всі заголовки HTTP які сервер надіслав у відповідь. Також response має методи для завантаження самих даних, але ми перейдемо до цього за хвилину.

② Сервер каже нам коли він обробив запит.

③ Ця відповідь включає заголовок Last-Modified.

④ Також ця відповідь включає заголовок ETag.

⑤ Дані мають довжину 3070 байтів. Зауважте що тут відсутнє - заголовок Content-encoding. Ваш запит стверджував що ви можете приймати лише нестиснені дані (Accept-encoding: identity), тому можна бути достатньо впевненим що ця відповідь теж містить нестиснені дані.

⑥ Відповідь включає заголовки що вказують на те що результат запиту можна зберігати в кеші протягом 24 годин (86400 секунд).

>>> data = response.read()
>>> len(data)
3070

Ну й нарешті завантажуємо дані викликаючи response.read(). Як можна бачити з результату функції len() отримано загалом 3070 байт.

Як ви можете бачити, цей код вже неефективний: він запитує (і отримує) нестиснені дані. Я знаю той факт що сервер підтримує стиснення gzip, але в HTTP стиснення лише опціональне. Ми не просили про це, тому ми його й не отримуємо. Це означає що ми пересилаємо мережею 3070 байт, в той час як могли пересилати лише 941. Поганий пес, ніякої кістки.

Але все ще може стати гіршим! Щоб побачити яким неефективним є цей код, давайте попросимо той самий фід вдруге.


# продовжуємо з попереднього прикладу
>>> response2 = urlopen('http://diveintopython3.org/examples/feed.xml')
send: b'GET /examples/feed.xml HTTP/1.1
Host: diveintopython3.org
Accept-Encoding: identity
User-Agent: Python-urllib/3.1'
Connection: close
reply: 'HTTP/1.1 200 OK'
... подальші дані зневадження опущено ...

Зауважили щось особливе в цьому запиті? Він не змінився! Він точно такий самий як попередній. Ніяких слідів заголовків Last-Modified. Ніяких слідів If-None-Match. Ніякої поваги до заголовків кешування. І знову ніякого стиснення.

І що відбувається якщо ви зробите такий самий запит двічі? Ви отримаєте таку саму відповідь. Двічі.

# продовжуємо з попереднього прикладу
>>> print(response2.headers.as_string())
Date: Mon, 01 Jun 2009 03:58:00 GMT
Server: Apache
Last-Modified: Sun, 31 May 2009 22:51:11 GMT
ETag: "bfe-255ef5c0"
Accept-Ranges: bytes
Content-Length: 3070
Cache-Control: max-age=86400
Expires: Tue, 02 Jun 2009 03:58:00 GMT
Vary: Accept-Encoding
Connection: close
Content-Type: application/xml

Сервер досі присилає той самий набір "розумних" заголовків: Cache-Control та Expires для того щоб уможливити кешування, Last-Modified та ETag для того щоб дозволити перевірку того що дані не змінились. Заголовок Vary: Accept-Encoding навіть підказує що сервер підтримує стиснення, якщо б ви тільки попросили про це. Але ви не попросили.

>>> data2 = response2.read() 
>>> len(data2)
3070

І знову ми завантажили всі 3070 байтів...

>>> data2 == data
True

... точно ті самі 3070 байтів що й минулого разу.

HTTP створено щоб працювати краще ніж тут. urllib говорить на HTTP так як я говорю іспанською - достатньо щоб вибратись з пробки, але недостатньо для того щоб підтримувати розмову. Час оновитись до бібліотеки що спілкується на HTTP вільно.


* * *


Представляємо httplib2

[ред.]

Перш ніж ви зможете використовувати httplib2, ви повинні встановити його. Відвідайте code.google.com/p/httplib2/ і завантажте останню версію. httplib2 доступний як для Python 2.x так і Python 3.x; пересвідчіться що ви отримаєте правильну версію, файл якої має назву подібну до httplib2-python3-0.5.0.zip.

Розпакуйте архів, відкрийте термінал, перейдіть в директорію яку щойно створили, і виконайте команду:

c:\Users\you\Downloads\httplib2-python3-0.5.0> c:\python31\python.exe setup.py install
running install
running build
running build_py
...

В Unix-системах аналогічно:

you@localhost:~/Desktop/httplib2-python3-0.5.0$ sudo python3 setup.py install
running install
running build
running build_py

Щоб використовувати httplib2 створіть екземпляр класу httplib2.Http.

>>> import httplib2
>>> h = httplib2.Http('.cache')

Первинним інтерфейсом до httplib2 є об’єкт Http. З причин, які ви зрозумієте в наступній секції, ви повинні при створенні об’єкт завжди передавати назву директорії. Директорія не обов'язково повинна бути існюючою, httplib2 за потреби створить її самостійно.

>>> response, content = h.request('http://diveintopython3.org/examples/feed.xml')

Як тільки в нас є об'єкт класу Http, щоб отримати дані достатньо викликати метод request() з адресою даних що нам потрібні. Це створить для переданого URL запит HTTP GET. (Далі в цьому розділі ви побачите як створювати інші типи запитів HTTP, наприклад POST.)

>>> response.status
200

Метод request() повертає два значення. Перше - це об'єкт httplib2.Response, який містить всі заголовки HTTP що повернув сервер. Наприклад код статусу 200, який вказує на те що запит був успішним.

>>> content[:52] 
b"<?xml version='1.0' encoding='utf-8'?>\r\n<feed xmlns=" 
>>> len(content) 
3070

Змінна content яка прийняла друге значення, містить дані що повернув сервер. Дані повертаються як об'єкт bytes, а не як рядок. Якщо ви хочете отримати їх як рядок, вам потрібно буде з'ясувати кодування і розкодувати байти самостійно.

Вам скоріш за все достатньо одного об'єкта httplib2.Http. Існують причини створювати більш ніж один, але ви повинні робити так лише в тому випадку коли знаєте нащо це потрібно. "Мені потрібно отримати дані з двох різних URL" не є достатньою причиною. Можна двічі викликати метод request() одного об'єкта Http.

Короткий відступ для того щоб пояснити чому httplib2 повертає байти замість рядків

[ред.]

Байти. Рядки. Який біль. Чому httplib2 "просто" не зробить перетворення за вас? Ну, це складно, тому що правила для визначення кодування символів залежать від того який ресурс ми запитуємо. Як httplib2 може знати який ресурс ми запитуємо? Він зазвичай описується в заголовку HTTP Content-Type, але це необов’язковий заголовок HTTP і не всі сервери HTTP його відправляють. Якщо цей заголовок не включається в відповідь HTTP, вгадати кодування повинен клієнт. (Таке вгадування називають "винюхуванням контенту" ("content sniffing"), і воно ніколи не буває ідеальним.)

Якщо ви знаєте який ресурс ви очікуєте (XML документ в нашому випадку), можливо ви можете "просто" передати отриманий об’єкт bytes функції xml.etree.ElementTree.parse(). Це працює лише поки XML документ містить в собі інформацію про своє кодування (як в цьому випадку і є), але це необов’язково, і не всі документи XML включають кодування. Якщо XML документ не описує своє кодування, клієнт повинен подивитись на те в чому його транспортували (на заголовок Content-Type HTTP), в якому може міститись параметр charset.

Але все ще гірше. Тепер інформація про кодування символів може знаходитись в двох місцях: всередині самого XML документа, та всередині заголовку HTTP Content-Type. Якщо ця інформація в двох місцях, то якому надати перевагу? Згідно RFC 3023 (Клянусь що я нічого не вигадую), якщо тип медіа який передається в заголовку HTTP Content-Type є application/xml, application/xml-dtd, application/xml-external-parsed-entity, чи будь-який інший з підтипів application/xml таких як application/atom+xml чи application/rss+xml чи навіть application/rdf+xml, тоді кодування буде:

  1. кодування що задане в параметрі charset заголовку HTTP Content-Type, чи
  2. кодування що задане в атрибуті encoding декларації XML всередині документа, чи
  3. UTF-8

З іншого боку, якщо тип медіа що передається в заголовку HTTP Content-Type є text/xml, text/xml-external-parsed-entity, чи підтип форми text/ЩоЗавгодно+xml, тоді кодування задане всередині XML документа повністю ігнорується, і визначається як

  1. кодування що задане в параметрі charset заголовку HTTP Content-Type, чи
  2. us-ascii

І це тільки для XML документів. Для HTML документів браузери створили такі заплутані правила з’ясування формату документа що ми й досі намагаємось їх всіх з’ясувати.

"Патчі вітаються"

Як httplib2 працює з кешуванням

[ред.]

Пам’ятаєте, в попередньому розділі я сказав що ви завжди повинні створювати об’єкт httplib2.Http з назвою директорії?Кешування є мотивом цього.

# продовжуючи попередній приклад
>>> response2, content2 = h.request('http://diveintopython3.org/examples/feed.xml')
>>> response2.status
200
>>> content2[:52]
b"<?xml version='1.0' encoding='utf-8'?>\r\n<feed xmlns="
>>> len(content2)
3070

Це не повинно бути страшенним сюрпризом. Все так само як ми робили минулого разу, якщо не врахоувати що ми кладемо результат в дві нові змінні. Як і минулого разу статус HTTP 200. Та й розмір завантаженого контенту такий самий як і минулого разу.

Отож, чому нам не все одно? Вийдіть з інтерактивної оболонки Python, зайдіть знову і я вам покажу.


# НЕ продовжуючи попередній приклад
# Вийдіть з інтерактивної оболонки і запустіть нову.
>>> import httplib2
>>> httplib2.debuglevel = 1

Давайте ввімкнемо режим зневадження і подивимось що на дроті.

>>> h = httplib2.Http('.cache')

Створіть об’єкт httplib2.Http з такою самою назвою директорії як і раніше.

>>> response, content = h.request('http://diveintopython3.org/examples/feed.xml')

Запитайте той самий URL що й раніше. Схоже на те, що нічого не відбувається. Якщо точніше, то нічого не відсилається на сервер, і нічого не повертається з сервера. Взагалі відсутня будь-яка мережева активність.

>>> len(content)
3070

Щоправда ми отримали деякі дані. Навіть всі дані.

>>> response.status
200

Ми також "отримали" статус код який вказує що "запит" був успішним.

>>> response.fromcache
True

Ах ось де собака зарита: ця "відповідь" була згенерована з локального кешу httplib2. Ця директорія, ім’я якої ви передали коли створювали об’єкт httplib2.Http, містить локальний кеш всіх будь-коли виконаних операцій httplib2.

Що на дроті? Зовсім нічого.

Якщо ви хочете ввімкнути режим зневадження httplib2, ви повинні встановити константу на рівні модуля (httplib2.debuglevel, а потім створити новий об’єкт httplib2.Http. Якщо ви хочете вимкнути зневадження, ви повинні змінити ту саму константу рівня модуля, і знову створити новий об’єкт httplib2.Http.

Раніше ви запитали дані з цього URL. Запит був успішним (status: 200). Відповідь включала не тільки дані фіду, але також набір заголовків кешування які вказують будь-кому хто слухає, що вони можуть кешувати цей ресурс протягом 24-х годин (Cache-Control: max-age=86400, це 24 години в секундах). httplib2 розуміє і поважає ці заголовки кешування, та зберігає попередню відповідь в директорії .cache (ім’я якої ми задали при створенні об’єкта Http). Цей кеш ще не застарів, тому коли ми вдруге запитали дані за цим URL, httplib2 просто повернув той самий результат, навіть не чіпаючи мережу.

Я кажу "просто", але очевидно за простотою ховається багато складності. httplib2 працює з кешування HTTP автоматично та за замовчуванням. Якщо, з певної причини, вам потрібно знати чи відповідь прийшла з кешу, ви можете перевірити response.fromcache. А в інших випадках, воно Просто Працює.

Тепер, припустимо, ви маєте закешовані дані, але хочете пропустити кеш і перезапитати їх з віддаленого сервера. Браузери іноді роблять це якщо користувач спеціально про таке просить. Ви можете подумати "о, ну я просто видалю дані з локального кешу, а потім зроблю ще один запит." Ви можете так зробити, але пам’ятайте, що може бути задіяно ще багато сторін окрім вас та віддаленого сервера. Як щодо проміжних проксі-серверів? Вони цілком поза вашим контролем, і вони можуть все ще мати дані закешованими, та щасливо повернуть їх вам, тому що (настільки, наскільки вони зацікавлені) їхній кеш все ще дійсний.

Замість маніпуляцій локальним кешем, сподіваючись на краще, ви повинні використовувати можливості HTTP, щоб бути певними що ваш запит справді досягає віддаленого сервера.

# продовжуючи з попереднього прикладу 
>>> response2, content2 = h.request('http://diveintopython3.org/examples/feed.xml',
...     headers={'cache-control':'no-cache'})
connect: (diveintopython3.org, 80)
send: b'GET /examples/feed.xml HTTP/1.1
Host: diveintopython3.org
user-agent: Python-httplib2/$Rev: 259 $
accept-encoding: deflate, gzip
cache-control: no-cache'
reply: 'HTTP/1.1 200 OK'
 подальший вивід опущено 
>>> response2.status
200

httplib2 дозволяє вам додавати довільні заголовки HTTP до будь-якого вихідного запиту. Щоб пропустити всі кеші (не тільки ваш локальний дисковий кеш, а й кешуючі проксі між вами та віддаленим сервером), додайте заголовок no-cache в словник headers.

Після цього ви зможете побачити що httplib2 ініціює мережевий запит. httplib2 розуміє та поважає заголовки кешування в обох напрямках - як частину вхідної відповіді, так і як частину вихідного запиту. Вона помічає, що ви додаєте заголовок no-cache, тому пропускає свій локальний кеш і тоді не має іншого вибору, окрім як звернутись до мережі запитуючи дані.

>>> response2.fromcache
False

Ця відповідь не була згенерованою з вашого локального кешу. Ви, звісно, вже про це знаєте, тому що бачили лог вихідного запиту. Але це добре, що ми можемо перевірити цю інформацію програмно.

>>> print(dict(response2.items()))
{'status': '200',
 'content-length': '3070',
 'content-location': 'http://diveintopython3.org/examples/feed.xml',
 'accept-ranges': 'bytes',
 'expires': 'Wed, 03 Jun 2009 00:40:26 GMT',
 'vary': 'Accept-Encoding',
 'server': 'Apache',
 'last-modified': 'Sun, 31 May 2009 22:51:11 GMT',
 'connection': 'close',
 '-content-encoding': 'gzip',
 'etag': '"bfe-255ef5c0"',
 'cache-control': 'max-age=86400',
 'date': 'Tue, 02 Jun 2009 00:40:26 GMT',
 'content-type': 'application/xml'}

Запит був успішним, ви завантажили увесь фід знову з віддаленого сервера. Звісно, сервер також відправив назад повний набір заголовків HTTP разом з даними фіду. Це включає також заголовки кешування, які httplib2 використовує для оновлення локального кешу, з надією уникнути необхідності доступатись до мережі наступного разу коли ви запитаєте цей фід. Все в кешуванні HTTP продумано для того аби максимізувати звертання до кешу і мінімізувати звертання до мережі. Незважаючи на те що цього разу ви пропустили кеш, віддалений сервер справді оцінить, якщо ви наступного разу кешуватимете результат.

Як httplib2 працює з заголовками Last-Modified та ETag

[ред.]

Заголовки кешування Cache-Control та Expires називаються ``індикаторами свіжості``. Вони вказують кешам в точних термінах що вони можуть повністю уникати доступу до мережі поки кеш не стане недійсним. І це саме та поведінка яку ви бачили в попередньому розділі: маючи індикатор свіжості, httplib2 не генерує жодного байта мережевої активності щоб подати нам закешовані дані (якщо ви, звісно, явно не пропустите кеш).

Але як щодо випадку коли дані могли змінитись, але не змінились? HTTP для цього описує заголовки Last-Modified та Etag. Ці заголовки називаються валідаторами. Якщо локальний кеш більше не свіжий, клієнт може послати валідатори з наступним запитом щоб дізнатись чи дані змінювались. Якщо дані не змінювались, сервер відповідає кодом статусу 304 і не відправляє дані. Тому, хоча використання мережі присутнє, в кінцевому результаті ви завантажуєте менше байтів.

>>> import httplib2
>>> httplib2.debuglevel = 1
>>> h = httplib2.Http('.cache')
>>> response, content = h.request('http://diveintopython3.org/')
connect: (diveintopython3.org, 80)
send: b'GET / HTTP/1.1
Host: diveintopython3.org
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 200 OK'

Замість фіду, цього разу ми будемо завантажувати домашню сторінку сайту, яка написана на HTML. Так як це перший раз коли ми запитуємо цю сторінку, httplib2 має мало з чим працювати, тому відправляє мінімальний набір заголовків разом з запитом.

>>> print(dict(response.items()))
{'-content-encoding': 'gzip',
 'accept-ranges': 'bytes',
 'connection': 'close',
 'content-length': '6657',
 'content-location': 'http://diveintopython3.org/',
 'content-type': 'text/html',
 'date': 'Tue, 02 Jun 2009 03:26:54 GMT',
 'etag': '"7f806d-1a01-9fb97900"',
 'last-modified': 'Tue, 02 Jun 2009 02:51:48 GMT',
 'server': 'Apache',
 'status': '200',
 'vary': 'Accept-Encoding,User-Agent'}

Відповідь містить багато заголовків HTTP..., але жодної інформації про кешування. Щоправда, вона включає як заголовок ETag, так і Last-Modified.

>>> len(content)
6657

Коли я створював цей приклад, сторінка мала розмір 6657 байт. З того часу її розмір міг змінитись, але про це не турбуйтесь.

# продовжуючи попередній приклад
>>> response, content = h.request('http://diveintopython3.org/')  
connect: (diveintopython3.org, 80)
send: b'GET / HTTP/1.1
Host: diveintopython3.org
if-none-match: "7f806d-1a01-9fb97900"                             
if-modified-since: Tue, 02 Jun 2009 02:51:48 GMT                  
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 304 Not Modified'                                

① Ми знову запитуємо ту саму сторінку, з тим самим об’єктом Http (та тим самим локальним кешем). ② httplib2 відправляє валідатор ETag назад на сервер в заголовку If-None-Match. ③ httplib2 також відправляє валідатор Last-Modified назад на сервер в заголовку If-Modified-Since. ④ Сервер подивився на ці валідатори, подивився на сторінку яку ви запитуєте і визначив що сторінка не змінювалась з того часу як ви її востаннє запитували, тому відправляє назад статус код 304 та жодних даних.

>>> response.fromcache
True

На клієнті httplib2 помічає статус код 304 і завантажує дані з кешу.

>>> response.status
200

А це може трішки заплутувати. Насправді є два коди статусу - 304 (повернутий сервером цього разу, який змусив httplib2 дивитись в кеш), та 200 (повернений з сервера останнього разу, і збережений в кеші httplib2 поряд з даними сторінки). response.status повертає статус з кешу.

>>> response.dict['status']
'304'

Якщо ви хочете знати чистий код статусу отриманий від сервера, ви можете отримати його дивлячись в response.dict, який є словником заголовків справді повернутих сервером.

>>> len(content)
6657

Щоправда, ви все ще можете отримати дані в змінній content. Зазвичай, ви не маєте потреби знати чому відповідь була віддана з кешу. (Ви можете навіть не турбуватись чи дані взагалі отримуються з кешу, і це теж нормально. httplib2 достатньо розумний, аби дозволити вам поводитись як дурень.) На час, коли метод request() повертає дані, httplib2 вже оновила кеш.

Як httplib2 працює зі стисненням

[ред.]

HTTP підтримує кілька видів стиснення. Двома найбільш поширеними є gzip та deflate. httplib2 підтримує обидва.

Ми маємо обидва жанри музики, кантрі ТА вестерн.
>>> response, content = h.request('http://diveintopython3.org/')
connect: (diveintopython3.org, 80)
send: b'GET / HTTP/1.1
Host: diveintopython3.org
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 200 OK'

Щоразу, коли httplib2 відправляє запит, він включає заголовок Accept-Encoding щоб сказати серверу що він може прочитати дані стиснуті як методом gzip, так і deflate.

>>> print(dict(response.items()))
{'-content-encoding': 'gzip',                          
 'accept-ranges': 'bytes',
 'connection': 'close',
 'content-length': '6657',
 'content-location': 'http://diveintopython3.org/',
 'content-type': 'text/html',
 'date': 'Tue, 02 Jun 2009 03:26:54 GMT',
 'etag': '"7f806d-1a01-9fb97900"',
 'last-modified': 'Tue, 02 Jun 2009 02:51:48 GMT',
 'server': 'Apache',
 'status': '304',
 'vary': 'Accept-Encoding,User-Agent'}

В цьому випадку сервер відповідає даними стисненими за допомогою gzip. До того часу як метод request() поверне дані, httplib2 вже розпакувала тіло відповіді і помістила її в змінну content. Якщо вам цікаво чи відповідь була стиснена, ви можете перевірити response['-content-encoding'], а якщо ні, можете про це взагалі не хвилюватись.

Як httplib2 працює із перенаправленнями

[ред.]

HTTP описує два види перенаправлень: тимчасові та постійні. З тимчасовими перенаправленнями не треба робити нічого особливого, окрім того що йти за ними, що httplib2 робить автоматично.

>>> import httplib2
>>> httplib2.debuglevel = 1
>>> h = httplib2.Http('.cache')
>>> response, content = h.request('http://diveintopython3.org/examples/feed-302.xml') 
connect: (diveintopython3.org, 80)
send: b'GET /examples/feed-302.xml HTTP/1.1 ②
Host: diveintopython3.org
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 302 Found' 
send: b'GET /examples/feed.xml HTTP/1.1 ④
Host: diveintopython3.org
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 200 OK'

① За цим URL немає фіда. Я налаштував мій сервер віддавати тимчасове перенаправлення на правильну адресу.

② Ось запит.

③ А ось відповідь: 302 Found. Тут не показано, але ця відповідь також містить заголовок Location, який вказує на справжній URL.

httplib2 негайно розвертається і "йде за" перенаправленням, створючи інший запит до URL переданого в загловку Location: http://diveintopython3.org/examples/feed.xml

"Слідування" за перенаправленням не є чимось більшим ніж показує цей приклад. httplib2 посилає запит про URL який ви попросили. Сервер дає відповідь яка каже "Ні ні, краще подивіться сюди". httplib посилає інший запит за новим URL.

# продовжуючи попередній приклад
>>> response                                                          
{'status': '200',
 'content-length': '3070',
 'content-location': 'http://diveintopython3.org/examples/feed.xml',  
 'accept-ranges': 'bytes',
 'expires': 'Thu, 04 Jun 2009 02:21:41 GMT',
 'vary': 'Accept-Encoding',
 'server': 'Apache',
 'last-modified': 'Wed, 03 Jun 2009 02:20:15 GMT',
 'connection': 'close',
 '-content-encoding': 'gzip',                                         
 'etag': '"bfe-4cbbf5c0"',
 'cache-control': 'max-age=86400',                                    
 'date': 'Wed, 03 Jun 2009 02:21:41 GMT',
 'content-type': 'application/xml'}

① Відповідь яку ми отримаємо від цього єдиного виклику методу request() - це відповідь від кінцевого URL.

httplib2 додає кінцевий URL до словника response, як content-location. Це не заголовок що прийшов з сервера, це поле специфічне для httplib2.

③ Без певної на те причини, цей фід стиснений.

④ І може бути закешованим. (Це важливо, як ви зможете побачити за хвилину.)

Словник response який ми отримуємо, дає вам інформацію про кінцевий URL. А що якщо ви захочете отримати більше інформації про проміжні URL-и, ті які в кінцевому результаті перенаправили нас до фінального URL? httplib2 дозволяє вам дізнатись і про це.

# продовжуючи попередній приклад
>>> response.previous
{'status': '302',
 'content-length': '228',
 'content-location': 'http://diveintopython3.org/examples/feed-302.xml',
 'expires': 'Thu, 04 Jun 2009 02:21:41 GMT',
 'server': 'Apache',
 'connection': 'close',
 'location': 'http://diveintopython3.org/examples/feed.xml',
 'cache-control': 'max-age=86400',
 'date': 'Wed, 03 Jun 2009 02:21:41 GMT',
 'content-type': 'text/html; charset=iso-8859-1'}

Атрибут response.previous містить посилання на попередній об’єкт відповіді, який httplib2 використала щоб дістатись до поточної відповіді.

>>> type(response)
<class 'httplib2.Response'>
>>> type(response.previous)
<class 'httplib2.Response'>

Як response так і response.previous - це об’єкти класу httplib2.Response.

>>> response.previous.previous
>>>

Це означає що ви можете перевірити response.previous.previous аби прослідкувати за ланцюжком перенаправлень ще далі. (Сценарій: один URL перенаправляє на інший, який перенаправляє на третій. Таке може трапитись!). В даному випадку, ми вже досягли початку ланцюга перенаправлень, тому атрибут має значення None.

Що трапиться якщо ви запитаєте той самий URL знову?

# продовжуючи попередній приклад
>>> response2, content2 = h.request('http://diveintopython3.org/examples/feed-302.xml')  
connect: (diveintopython3.org, 80)
send: b'GET /examples/feed-302.xml HTTP/1.1                                              ②
Host: diveintopython3.org
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 302 Found'                                                              
>>> content2 == content                                                                  
True

① Той самий URL, той самий об’єкт httplib2.Http (відповідно той самий кеш).

② Відповідь 302 не була закешованою, тому httplib2 посилає інший запит до того ж URL.

③ І знову, сервер відповідає з 302. Але зауважте, що не сталося: не було наступного запиту до кінцевого URL, http://diveintopython3.org/examples/feed.xml. Ця відповідь була закешованою (пам’ятаєте заголовок Cache-Control, який ми бачили в попередньому прикладі?) Як тільки httplib2 отримала код 302 Found, вона перевірила його кеш перед створенням наступного запиту. Цей кеш містив свіжу копію http://diveintopython3.org/examples/feed.xml, тому не було потреби її перезапитувати.

④ Коли метод request() повертає дані, він бере їх з кешу. І звичайно це ті самі дані що ви отримали минулого разу.


Іншими словами, ви не повинні робити нічого особливого для обробки тимчасових перенаправлень. httplib2 буде слідувати за ними автоматично, і факт того що один URL посилається на інший, не має ніякого відношення до підтримки httplib2 стиснення, кешування, чи будь-яких інших особливостей HTTP.

Постійні перенаправлення так само прості.

# продовжуючи попередній приклад
>>> response, content = h.request('http://diveintopython3.org/examples/feed-301.xml')  
connect: (diveintopython3.org, 80)
send: b'GET /examples/feed-301.xml HTTP/1.1
Host: diveintopython3.org
accept-encoding: deflate, gzip
user-agent: Python-httplib2/$Rev: 259 $'
reply: 'HTTP/1.1 301 Moved Permanently'                                                
>>> response.fromcache                                                                 
True

① Знову ж таки, цього URL насправді не існує. Я додав його на свій сервер щоб створювати постійне перенаправлення на http://diveintopython3.org/examples/feed.xml.

② І ось він тут: код статусу 301. Але знову, зауважте чого не відбулось: не було запиту до URL на який нас перенаправляли. Чому? Тому що він вже закешований локально.

httplib2 "пішла" за перенаправленням прямо в свій кеш.


Але зачекайте, є ще!

# продовжуючи попередній приклад
>>> response2, content2 = h.request('http://diveintopython3.org/examples/feed-301.xml')  
>>> response2.fromcache                                                                  
True
>>> content2 == content                                                                  
True

① Ось різниця між тимчасовим та постійними перенаправленнями: як тільки httplib2 переходить за постійним перенаправленням, всі подальші запити за тим URL будуть очевидно перекидатись на цільовий URL без звертань по мережі до оригінального URL. Пам’ятайте, режим зневадження все ще ввімкнений, і при цьому нема жодного виводу про якусь мережеву активність.

② Ага, ця відповідь була отримана з локального кешу.

③ Ага, ви отримали ввесь фід (з кешу).


HTTP. Воно працює!


* * *


Поза HTTP GET

[ред.]

Веб-сервіси HTTP не обмежуються GET запитами. Що якщо ви захочете створити щось нове? Щоразу коли ви публікуєте коментар на форумі, оновлюєте свій блог, публікуєте статус на сервісі мікроблогів на зразок Twitter чи Google+, чи публікуєте gist на GitHub[1], ви скоріш за все вже використовуєте HTTP POST.

GitHub та Twitter надають просте API на основі HTTP для публікації статусів та коду відповідно. Давайте подивимось на документацію API GitHub щодо публікації нового gist.

Як це працює? Щоб створити новий gist, нам потрібно здійснити POST запит до https://api.github.com/gists. В запиті потрібно передати JSON об’єкт що містить дані нашого gist-та. І запит повинен бути автентифікованим.

Автентифікованим? Звісно. Щоб розмістити щось від свого імені на gist, потрібно довести що це саме ви. httplib2 підтримує як автентифікацію по SSL, так і базову HTTP автентифікацію, але в даному випадку ми використовуватимемо токен. Токен для авторизації в GitHub можна отримати на сторінці Applications налаштувань профілю, на панелі Personal Access Tokens:


Цей токен варто зберегти в якийсь, не доступний стороннім файл на зразок gist_token.txt і передавати його в заголовку наступним чином:

token = open('gist_token.txt').read().strip()
headers = {'Authorization': 'token ' + token}

POST запит відрізняється від GET запиту тим, що він містить корисне навантаження. Корисне навантаження - це дані які ви хочете відправити на сервер. GitHub приймає дані закодовані як JSON:

>>> import json
>>> data = json.dumps({
...     'description': 'test gist created from Python',
...     'public': True,
...     'files': {
...         'file1.txt': { 'content': 'hello world!'},
...     }
... })
>>> data
'{"files": {"file1.txt": {"content": "hello world!"}}, "description": "test gist created from Python", "public": true}'

Деякі сервіси приймають дані закодовані в форматі URL. Закодувати їх так можна за допомогою функції urlencode() з модуля urllib.parse, яка приймає словник пар ключ-значення, та перетворює його в рядок:

>>> from urllib.parse import urlencode
>>> data = {'greeting': 'Hello from Python 3'}
>>> urlencode(data)
'greeting=Hello+from+Python+3'

І нарешті, відправляємо запит разом з вмістом та заголовками авторизації серверу:

>>> http.request('https://api.github.com/gists', method='POST', body=data, headers=headers)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/httplib2/__init__.py", line 1570, in request
    (response, content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey)
  File "/usr/local/lib/python2.7/dist-packages/httplib2/__init__.py", line 1317, in _request
    (response, content) = self._conn_request(conn, request_uri, method, body, headers)
  File "/usr/local/lib/python2.7/dist-packages/httplib2/__init__.py", line 1252, in _conn_request
    conn.connect()
  File "/usr/local/lib/python2.7/dist-packages/httplib2/__init__.py", line 1044, in connect
    raise SSLHandshakeError(e)
SSLHandshakeError: [Errno 1] _ssl.c:504: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed

Ого, це було неочікувано, правда?

Про SSL

[ред.]

httplib2 сказав нам що при встановленні захищеного з’єднання (https), сервер надав сертифікат, який підтверджує що це справді сервер GitHub, а не якогось хакера, але ми не можемо його перевірити, тому що у нас немає відповідного публічного сертифікату. З цієї ситуації є два виходи.

Найпростіший - вимкнути перевірку сертифікатів взагалі:

http = httplib2.Http(disable_ssl_certificate_validation=True)
headers, responce = http.request('https://api.github.com/gists', method='POST', body=data, headers=headers)

Але є ще й правильний вихід. Переглянемо для GitHub в браузері інформацію про сторінку.

Ми бачимо що факт того що ми на сайті github.com засвідчує якась DigiCert Inc. Натискаємо Переглянути сертифікат, далі відкриваємо вкладку подробиці і шукаємо внизу кнопку Експорт.... Серед типів файлу вибираємо "Сертифікат X.509 з ланцюжком (PEM)" і зберігаємо його кудись на диск, наприклад у файл digicert.pem.

Тепер, ми можемо передати цей файл об’єкту Http:

http = httplib2.Http(ca_certs='digicert.pem')

Якщо сертифікат правильний, вищеописана помилка більше з’являтись не повинна, і ми повинні отримати успішний результат в responce:

{'comments': 0,
 'comments_url': 'https://api.github.com/gists/6666887/comments',
 'commits_url': 'https://api.github.com/gists/6666887/commits',
 'created_at': '2013-09-23T05:56:19Z',
 'description': 'test gist created from Python',
 'files': {'file1.txt': {'content': 'hello world!',
                           'filename': 'file1.txt',
                           'language': None,
                           'raw_url': 'https://gist.github.com/raw/6666887/bc7774a7b18deb1d7bd0212d34246a9b1260ae17/file1.txt',
                           'size': 12,
                           'type': 'text/plain'}},
 'forks': [],
 'forks_url': 'https://api.github.com/gists/6666887/forks',
 'git_pull_url': 'https://gist.github.com/6666887.git',
 'git_push_url': 'https://gist.github.com/6666887.git',
 'history': [{'change_status': {'additions': 1,
                                  'deletions': 0,
                                  'total': 1},
               'committed_at': '2013-09-23T05:56:19Z',
               'url': 'https://api.github.com/gists/6666887/60f8dbda1d08edd9c466f0cf359690d534e5348c',
               'user': {... багато даних про користувача ... },
               'version': '60f8dbda1d08edd9c466f0cf359690d534e5348c'}],
 'html_url': 'https://gist.github.com/6666887',
 'id': '6666887',
 'public': True,
 'updated_at': '2013-09-23T05:56:19Z',
 'url': 'https://api.github.com/gists/6666887',
 'user': {... знову багато даних про користувача ...}}
  1. В оригіналі книжки не йшлося про ніякий GitHub, чи Google+, але сервіс identi.ca закрив реєстрацію, тому перекладач вирішив цю книжку трохи осучаснити

А якщо ми відкриємо Gits з id, який нам передали з відповіддю (6666887) у браузері, то побачимо щось таке:


* * *


Поза HTTP POST

[ред.]

HTTP не обмежується лише методами GET та POST. Це, звісно, найбільш поширені види запитів, особливо в браузерах. Але API веб-сервісів може поширюватись далі за GET та POST, і httplib2 готовий до цього.

В responce ми прийняли текст з даними закодованими як JSON. Ми можемо розкодувати їх і витягнути звідки id нашого Gist-та:

# продовжуючи попередній приклад 
>>> gist_id = json.loads(responce)['id']
>>> gist_id
'6666887'

Тепер давайте спробуємо видалити наш Gist, пославши запит DELETE:

>>> http.request('https://api.github.com/gists/%s' %id, method='DELETE', headers=headers)
({'status': '204', 'x-ratelimit-remaining': '4999', 'x-github-media-type': 'github.beta; format=json', 'x-content-type-options': 'nosn
iff', 'access-control-expose-headers': 'ETag, Link, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Acc
epted-OAuth-Scopes', 'x-github-request-id': '5F85A6C1:5248:1DB517D:5247DA46', 'vary': 'Accept-Encoding', 'server': 'GitHub.com', 'acce
ss-control-allow-origin': '*', 'x-ratelimit-limit': '5000', 'access-control-allow-credentials': 'true', 'date': 'Sun, 29 Sep 2013 07:4
4:06 GMT', 'x-oauth-scopes': 'user, public_repo, repo, gist', 'x-accepted-oauth-scopes': 'gist', 'x-ratelimit-reset': '1380444246'}, b
'')

У відповідь ми отримуємо статус-код HTTP 204. 204 означає "No content", тобто те що запит виконаний успішно, але сервер не вважає необхідним повертати у відповідь ще якісь дані. Що й логічно, для запиту видалення. Тому що якщо ми знову захочемо подивитись на сторінку в Github, вона вже буде відсутньою:


* * *


Для подальшого читання

[ред.]

httplib2:

Кешування HTTP:

RFC:

Серіалізація об'єктів · Приклад: Перенесення chardet на Python 3