Nacházíte se zde: Domů Ponořme se do Pythonu 3

Úroveň obtížnosti: ♦♦♦♦♢

Webové služby nad HTTP

A ruffled mind makes a restless pillow.
(Rozbouřená mysl je nepohodlný polštář.)
— Charlotte Bronteová

 

Ponořme se

Z filozofického hlediska můžeme webové služby nad HTTP (HyperText Transfer Protocol) popsat devíti slovy: výměna dat se vzdálenými servery pouze s použitím operací protokolu HTTP. Pokud chceme ze serveru získat data, použijeme HTTP GET. Pokud chceme nová data na server zaslat, použijeme HTTP POST. Některá pokročilejší aplikační rozhraní (API) webových služeb nad HTTP umožňují také vytváření, modifikaci a rušení dat použitím HTTP PUT a HTTP DELETE. To je vše. Žádné registry, žádné obálky, žádný obalující kód, žádné tunelování. „Slovesa“, která jsou součástí HTTP protokolu (GET, POST, PUT a DELETE) přímo odpovídají operacím na aplikační úrovni pro získávání, vytváření, modifikaci a rušení dat.

Hlavní výhodou tohoto přístupu je jednoduchost a právě jednoduchost vedla k jeho oblibě. Data — obvykle XML nebo JSON — mohou být vytvořena a uložena jako statická, nebo mohou být generována dynamicky, skriptem na straně serveru. Všechny hlavní programovací jazyky (samozřejmě včetně Pythonu) umožňují stahování těchto dat prostřednictvím svých HTTP-knihoven. Jednodušší je i ladění. Každý prostředek (resource) webové služby nad HTTP má jednoznačnou adresu v podobě URL. Po zadání do webového prohlížeče dojde k načtení a hned vidíte surová data.

Příklady webových služeb nad HTTP:

Pro interakci s webovými službami nad HTTP jsou v Pythonu 3 k dispozici dvě různé knihovny:

Takže který mám použít? Z těchto dvou žádný. Místo toho byste měli použít httplib2, což je open source knihovna třetí strany, která implementuje HTTP do větších detailů než http.client. Současně používá lepší abstrakce než urllib.request.

Abyste porozuměli tomu, proč je httplib2 tou správnou volbou, musíte nejdříve porozumět HTTP.

Vlastnosti HTTP

Každý HTTP klient by měl podporovat pět důležitých vlastností.

Používání mezipaměti

Nejdůležitější věcí, které musíme v souvislosti s libovolným typem webové služby rozumět, je to, že přístup k síti je velmi drahý. Nemám na mysli cenu „v penězích“ (i když šířka přenosového pásma není zadarmo). Mám na mysli to, že hrozně dlouhou dobu zabere otevření spojení, odeslání požadavku a získání odezvy ze vzdáleného serveru. Dokonce i v případě nejrychlejšího dostupného spojení může být latence (tj. čas mezi zasláním požadavku a zahájením přijímání dat odpovědi) vyšší, než byste předpokládali. Směrovače mohou zafungovat divně, paket se ztratí, na mezilehlý server někdo zaútočil... Na veřejné internetové síti není nikdy klidná chvilka a nic s tím nenaděláte.

Při návrhu HTTP se počítalo s využíváním mezipaměti (cache). Existuje dokonce samostatná třída zařízení (zvaných „mezipaměťové proxy-servery“, anglicky „chaching proxies“), jejichž jedinou prací je ležet mezi vámi a zbytkem světa a minimalizovat zatěžování sítě. Vaše firma nebo váš poskytovatel připojení (ISP) téměř jistě mezipaměťové proxy-servery udržuje, i když si toho nemusíte být vědomi. Fungují, protože používání mezipaměti (caching) je součástí HTTP protokolu.

Následuje konkrétní příklad toho, jak to funguje. Prostřednictvím svého prohlížeče navštívíte diveintomark.org. Uvedená stránka používá pro pozadí obrázek wearehugh.com/m.jpg. Když váš prohlížeč obrázek stáhne, server k němu přiloží následující HTTP hlavičky:

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

Hlavičky Cache-Control a Expires říkají vašemu prohlížeči (a všem mezipaměťovým proxy-serverům mezi vámi a serverem), že se tento obrázek může získávat z mezipaměti až jeden rok. Celý rok! A pokud někdy v příštím roce navštívíte jinou stránku, která také obsahuje odkaz na tento obrázek, váš prohlížeč jej načte ze své mezipaměti, aniž by vyvolal jakoukoliv síťovou aktivitu.

Ale počkejte, bude to ještě lepší. Dejme tomu, že váš prohlížeč obrázek z lokální mezipaměti z nějakého důvodu odstraní. Možná mu došlo místo na disku, možná jste mezipaměť vyprázdnili ručně. Z jakéhokoliv důvodu. Ale HTTP hlavičky říkají, že tato data mohou být uchovávána veřejnými mezipaměťovými proxy-servery. (Z technického pohledu je důležité, co hlavičky neříkají. Hlavička Cache-Control neuvádí klíčové slovo private, takže data mohou být uložena v mezipaměti automaticky.) Mezipaměťové proxy-servery jsou navrženy tak, že mají k dispozici obrovské množství úložného prostoru — pravděpodobně ho mají mnohem více, než má vyhrazeno váš lokální prohlížeč.

Pokud vaše firma nebo váš poskytovatel připojení spravuje mezipaměťový proxy-server, může se v jeho mezipaměti obrázek pořád ještě nacházet. Pokud navštívíte diveintomark.org znovu, podívá se váš prohlížeč po obrázku do lokální mezipaměti, ale nenajde jej. Takže vytvoří síťový požadavek a pokusí se obrázek stáhnout ze vzdáleného serveru. Pokud ale mezipaměťový proxy-server pořád má kopii uvedeného obrázku, váš požadavek zachytí a dodá vám obrázek ze své mezipaměti. To znamená, že se váš požadavek ke vzdálenému serveru nikdy nedostane. Ve skutečnosti nemusí opustit vaši firemní síť. Získání obrázku je rychlejší (méně skoků po síti) a vaše firma ušetří peníze (z vnějšího světa se stahuje méně dat).

Použití mezipamětí v HTTP funguje, pokud všechny strany dělají, co mají. Na jedné straně musí servery v odpovědích posílat správné hlavičky. Na druhé straně musí klienti hlavičkám rozumět, respektovat je a nežádat stejná data dvakrát. Mezilehlé proxy-servery nejsou všelékem. Mohou být „chytré“ jen do té míry, do jaké jim to servery a klienti umožní.

Standardní pythonovské knihovny pro HTTP používání mezipaměti nepodporují, ale httplib2 ano.

Kontrola Last-Modified

Některá data se nemění nikdy, zatímco jiná data se mění pořád. A mezi tím je obrovské množství dat, která se mohla změnit, ale nezměnila se. Publikovaný obsah (feed) serveru CNN.com se mění každých pár minut, ale publikovaný obsah mého weblogu se nemusí změnit celé dny nebo týdny. I kdyby to byl ten druhý případ, nechci klientům říct, aby si můj publikovaný obsah brali z mezipaměti celé týdny, protože pokud bych doopravdy něco nového zveřejnil, lidé by se o tom celé týdny nedozvěděli (protože by respektovali mé hlavičky týkající se mezipaměti, které říkají „neobtěžujte se s kontrolou tohoto publikovaného obsahu po celé týdny“). Na druhou stranu zase nechci, aby klienti stahovali celý publikovaný obsah (feed) každou hodinu, pokud se vůbec nezměnil!

HTTP nabízí řešení i pro tento případ. Pokud o data žádáme poprvé, server může zpět poslat hlavičku Last-Modified (naposledy změněno). Je to přesně to, jak to vypadá: datum a čas, kdy se data naposledy změnila. Obrázek pozadí, na který vedl odkaz z diveintomark.org, doprovázela hlavička 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

Pokud požadujeme stejná data podruhé (nebo potřetí nebo počtvrté), můžeme v dotazu poslat hlavičku If-Modified-Since (pokud bylo změněno od) s hodnotou data a času, které jsme od serveru dostali minule. Pokud se data od té doby změnila, pak server vrátí nová data doplněná o stavový kód 200. Ale pokud se data od té doby nezměnila, server pošle zpět speciální stavový kód protokolu HTTP304. Ten říká „od doby, kdy ses naposledy ptal, se tato data nezměnila“. Z příkazového řádku si to můžeme ověřit nástrojem 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

A proč by to mělo být vylepšení? Protože když server pošle 304, neposílá data znovu. Dostaneme pouze stavový kód. Kontrola poslední modifikace zajistí, že se nezměněná data nebudou stahovat podruhé i v případě, kdy došlo k vypršení platnosti kopie v lokální mezipaměti. (Jako bonus navíc obsahuje odpověď 304 také hlavičky pro mezipaměť. Proxy-servery si kopii dat drží, dokonce i když oficiálně „expirovala“, v naději, že se data ve skutečnosti nezměnila a že další požadavek povede k odpovědi se stavovým kódem 304 a s aktualizovanými informacemi pro mezipaměť.)

Standardní pythonovské knihovny pro HTTP nepodporují kontrolu data poslední modifikace, ale httplib2 ano.

Kontrola ETag

ETagy (tag = značka) představují alternativní způsob dosažení stejného efektu jako v případě kontroly last-modified. Při použití ETagů posílá server spolu s požadovanými daty v hlavičce ETag s heš-kódem (hash). (Jak se přesně heš-hodnota určí, to závisí zcela na serveru. Jediný požadavek je takový, aby se změnila, pokud se změní data.) Obrázek pozadí, na který vedl odkaz z diveintomark.org, doprovázela hlavička 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

Pokud stejná data požadujeme podruhé, přiložíme heš-hodnotu v hlavičce pořadavku If-None-Match (pokud žádná data neodpovídají). Pokud se data nezměnila, server pošle zpět stavový kód 304. Server — stejně jako v případě kontroly založené na čase poslední modifikace — pošle zpět pouze stavový kód 304. Stejná data znovu neposílá. Přiložením heš-hodnoty v ETagu při druhém požadavku serveru říkáme, že při shodě heše není nutné posílat stejná data znovu, protože je pořád máme schovaná od minula.

Opět vyzkoušíme pomocí 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
  1. ETagy se běžně uzavírají do uvozovek, ale tyto uvozovky jsou součástí hodnoty. To znamená, že v hlavičce If-None-Match musíme serveru poslat zpět i uvozovky.

Standardní pythonovské knihovny pro HTTP používání ETagů nepodporují, ale httplib2 ano.

Komprese

Pokud se bavíme o webových službách nad HTTP, pak se téměř vždy bavíme o přesunování textových dat po drátech tam a zase zpět. Možná jsou ve formátu XML, možná jsou v JSON, možná je to prostý text. Text se dá dobře komprimovat nezávisle na použitém formátu. Příklad publikovaného obsahu (feed) z kapitoly XML má nekomprimovaný 3070 bajtů, ale po kompresi algoritmem gzip má 941 bajtů. To je jen 30 % původní velikosti!

HTTP podporuje několik komprimačních algoritmů. Mezi dva nejběžnější patří gzip a deflate. Pokud přes HTTP požadujeme nějaký prostředek (resource), můžeme serveru říci, aby ho poslal v komprimovaném formátu. Do požadavku vložíme hlavičku Accept-encoding, ve které vyjmenujeme námi podporované komprimační algoritmy. Pokud server některý z těchto algoritmů podporuje, pošle nám zpět komprimovaná data (s hlavičkou Content-encoding, která říká, jaký algoritmus byl použit). O dekompresi se už musíme postarat sami.

Důležitý tip pro vývojáře kódu na straně serveru: Ujistěte se, že komprimovaná podoba zdroje dostane přidělenou jinou značku Etag než nekomprimovaná verze. V opačném případě by došlo ke zmatení mezipaměťových proxy-serverů a ty by mohly klientům vracet komprimovanou verzi, se kterou by si klient nemusel poradit. Více detailů o této delikátní záležitosti si můžete přečíst v diskusi Apache bug 39727.

Standardní pythonovské knihovny pro HTTP kompresi nepodporují, ale httplib2 ano.

Přesměrování

Senzační URI se nemění, ale mnohá URI jsou opravdu… nesenzační. Webová místa se reorganizují, stránky se přesouvají na nové adresy. Dokonce i webové služby mohou být reorganizovány. Publikovaný obsah (syndicated feed) mohl být přesunut z http://example.com/index.xml do http://example.com/xml/atom.xml. Nebo se při rozšiřování a reorganizaci firmy mohla přesunout celá doména. Z http://www.example.com/index.xml se mění na http://server-farm-1.example.com/index.xml.

Pokaždé, když HTTP server požádáme o nějaký zdroj (resource), vrací v odpovědi stavový kód. Stavový kód 200 znamená „vše v pořádku, tady je požadovaná stránka“. Stavový kód 404 znamená „stránka nenalezena“. (Chybu 404 jste už asi při brouzdání po webu viděli.) Stavové kódy ve skupině 300 vyjadřují nějakou formu přesměrování.

HTTP nabízí několik způsobů, jakými se dá oznámit, že se požadované zdroje přesunuly. Dvě nejběžnější techniky používají stavové kódy 302 a 301. Stavový kód 302 označuje dočasné přesměrování. Znamená „ejhle, je to dočasně přesunuté“ (a v hlavičce Location se vrátí dočasná adresa). Stavový kód 301 označuje trvalé přesměrování. Znamená „ejhle, je to trvale přesunuté“ (a v hlavičce Location se vrací nová adresa). Pokud obdržíte stavový kód 302 a novou adresu, pak máte podle specifikace HTTP pro požadovanou věc použít novou adresu. Ale až se budete na stejný zdroj informací ptát příště, máte to znovu zkusit s původní adresou. Pokud ale obdržíte stavový kód 301 a k němu novou adresu, očekává se od vás, že od toho okamžiku začnete používat novou adresu.

Modul urllib.request při obdržení příslušného stavového kódu od HTTP serveru sice „následuje“ přesměrování, ale neřekne vám, že tato situace nastala. Dostanete data, která jste požadovali, ale nikdy se nedozvíte, že se použitá knihovna zachovala „užitečně“ a následovala přesměrování za vás. Takže pořád bušíte na staré adrese a pokaždé jste serverem přesměrováni na novou adresu a modul urllib.request pokaždé „užitečně“ následuje přesměrování. Jinými slovy, tato knihovna se k trvalému přesměrování chová stejně jako k dočasnému přesměrování. To znamená, že se místo jednoho kola provedou vždycky dvě. To je špatné jak pro server, tak pro vás.

Knihovna httplib2 trvalé přesměrování zvládá. Nejen že vám řekne, že nastalo trvalé přesměrování, ale lokálně si je poznamená a přesměrovaná URL automaticky přepíše dříve, než vznese příslušný požadavek.

Jak se nedostat k datům přes HTTP

Dejme tomu, že přes HTTP chceme stáhnout informační zdroj, jako je například Atom feed. Protože jde o publikovaný obsah (feed), nebudeme jej stahovat jen jednou. Budeme jej stahovat opakovaně, pořád dokola. (Většina čteček publikovaného obsahu (feed reader) kontroluje změny každou hodinu.) Nejdříve vyzkoušíme „rychlý a špinavý“ způsob a pak se podíváme, jak bychom to mohli provádět lépe.

>>> import urllib.request
>>> a_url = 'http://diveintopython3.org/examples/feed.xml'
>>> data = urllib.request.urlopen(a_url).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/'/>
  …
  1. Stažení čehokoliv přes HTTP je v Pythonu neuvěřitelně jednoduché. Dá se to ve skutečnosti napsat na jeden řádek. Modul urllib.request nabízí šikovnou funkci urlopen(), která přebírá adresu požadované stránky a vrací objekt typu stream, ze kterého získáme celý obsah stránky prostým zavoláním metody read(). Už to asi nemůže být jednodušší.
  2. Metoda urlopen().read() vrací vždy objekt typu bytes a ne řetězec. Vzpomeňte si — bajty jsou bajty, znaky jsou abstrakce. HTTP servery nepracují s abstrakcemi. Kdykoliv požádáme o nějaký zdroj (resource), dostaneme bajty. Pokud z toho chceme udělat řetězec, musíme zjistit znakové kódování a provést explicitní převod na řetězec.

A co na tom je špatného? Při rychlém, jednorázovém přístupu během ladění a vývoje na tom není špatného nic. Dělám to takhle pořád. Chtěl jsem publikovaný obsah (feed), dostal jsem publikovaný obsah. Stejná technika funguje pro libovolné webové stránky. Ale jakmile o tom začneme uvažovat z pohledu webové služby, která se má využívat pravidelně (tj. požadavek na získání publikovaného obsahu každou hodinu), pak by to bylo neefektivní a my bychom byli nezdvořilí.

Co že to máme na drátě?

Abychom viděli, proč je to neefektivní a nezdvořilé, obrátíme se na ladicí prostředky pythonovské knihovny pro HTTP a uvidíme, co běhá „po drátech“ (tj. co se přenáší v síti).

>>> from http.client import HTTPConnection
>>> HTTPConnection.debuglevel = 1                                       
>>> from urllib.request import urlopen
>>> response = 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'
…further debugging information omitted…
  1. Jak už jsem se zmínil na začátku této kapitoly, urllib.request spoléhá na další standardní pythonovskou knihovnu, http.client. S knihovnou http.client za normálních okolností do přímého styku nepřicházíte. (Modul urllib.request ji importuje automaticky.) Ale my si ji importujeme ručně, abychom mohli nastavit příznak ladění u třídy HTTPConnection, kterou modul urllib.request používá pro připojení k HTTP serveru.
  2. Když teď máme ladicí příznak nastaven, budou se informace o HTTP požadavku a o odpovědi na něj tisknout v reálném čase. Když si vyžádáme Atom feed, je vidět, že modul urllib.request posílá serveru pět řádků.
  3. První řádek uvádí používané HTTP sloveso (metodu; zde GET) a cestu ke zdroji (bez uvedení jména domény).
  4. Druhý řádek uvádí doménu, ze které byl požadavek na feed vznesen.
  5. Třetí řádek uvádí komprimační algoritmy, které klient podporuje. Jak bylo uvedeno výše, urllib.request standardně kompresi nepodporuje.
  6. Čtvrtý řádek uvádí jméno knihovny, jejímž prostřednictvím byl požadavek vznesen. Výchozí hodnotou je Python-urllib a číslo verze. Jak urllib.request, tak httplib2 podporují změnu identifikace zprostředkovatele tím, že se do požadavku jednoduše přidá hlavička User-Agent, která přepíše výchozí hodnotu.

Teď se podívejme na to, jakou odpověď poslal server zpět.

# pokračování předchozího příkladu
>>> 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
>>> data = response.read()                     
>>> len(data)
3070
  1. Odpověď (response) vrácená funkcí urllib.request.urlopen() obsahuje všechny HTTP hlavičky, které server poslal zpět. Obsahuje také metody pro stahování skutečných dat. K tomu se dostaneme za minutku.
  2. Server říká, kdy zpracoval náš požadavek.
  3. Odpověď obsahuje i hlavičku Last-Modified.
  4. Odpověď obsahuje také hlavičku ETag.
  5. Data mají velikost 3070 bajtů. Všimněte si, že zde není hlavička Content-encoding. V požadavku jsme uvedli, že přijímáme jen nekomprimovaná data (Accept-encoding: identity), takže jsme tím pádem dostali nekomprimovaná data.
  6. V odpovědi se nacházejí hlavičky pro mezipaměti, které říkají, že publikovaný obsah (feed) může být brán z mezipaměti po dobu 24 hodin (86 400 sekund).
  7. A nakonec stáhneme skutečná data voláním response.read(). Z výsledku funkce len() vidíme, že se stáhlo všech 3070 bajtů najednou.

Jak sami vidíte, tento kód je už teď neefektivní. Požadoval (a obdržel) nekomprimovaná data. Určitě vím, že uvedený server podporuje kompresi gzip, ale v HTTP se komprese zapíná na vyžádání. Nepožádali jsme o ni, tak jsme ji nedostali. To znamená, že jsme stahovali 3070 bajtů v situaci, kdy jsme mohli stahovat pouhých 941. Zlobivý pejsek, žádná sušenka.

Ale moment, začíná to být ještě horší! Abychom viděli, jak neefektivní ten kód je, požádáme o stejný publikovaný obsah (feed) podruhé.

# pokračování předchozího příkladu
>>> 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'
…further debugging information omitted…

Všimli jste si na tom požadavku něčeho zvláštního? Vůbec se nezměnil! Je naprosto stejný jako ten předchozí. Žádná známka použití hlavičky If-Modified-Since. Žádná známka použití hlavičky If-None-Match. Žádný respekt k hlavičkám mezipaměti. Ještě pořád žádná komprese.

A co se stane, když uděláme stejnou věc dvakrát? Dostaneme stejnou odpověď. Dvakrát.

# pokračování předchozího příkladu
>>> 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
>>> data2 = response2.read()
>>> len(data2)                               
3070
>>> data2 == data                            
True
  1. Server pořád posílá stejné pole „chytrých“ hlaviček: Cache-Control a Expires pro mezipaměť (cache), Last-Modified a ETag pro sledování „nezměněného stavu“. A dokonce hlavičku Vary: Accept-Encoding, kterou server dává najevo, že by mohl podporovat kompresi, kdybychom si o ni řekli. Ale my jsme to neudělali.
  2. A ještě jednou, při získávání dat se stáhlo všech 3070 bajtů…
  3. …stejných 3070 bajtů, které jsme stáhli už minule.

Protokol HTTP je navržen, aby pracoval lepším způsobem. Knihovna urllib umí HTTP asi tak, jak já umím španělsky — dost na to, abych se dostal z problémů, ale ne dost k vedení konverzace. A HTTP se týká konverzace. Je čas přejít ke knihovně, která protokolem HTTP mluví plynule.

Představujeme httplib2

Než začneme knihovnu httplib2 používat, musíme ji nainstalovat. Navštivte stránku code.google.com/p/httplib2/ a stáhněte poslední verzi. httplib2 je k dispozici pro Python 2.x a pro Python 3.x. Ujistěte se, že jde o verzi pro Python 3. Jmenuje se podobně jako httplib2-python3-0.5.0.zip. (V době překladu už to bylo jinak: httplib2-0.6.0.zip; uvnitř jsou obě verze.)

Rozbalte archiv, otevřete terminálové okno a přejděte do nově vytvořeného adresáře httplib2. Pod Windows otevřete menu Start, vyberte Run..., napište cmd.exe a stiskněte ENTER.

c:\Users\pilgrim\Downloads> dir
 Volume in drive C has no label.
 Volume Serial Number is DED5-B4F8

 Directory of c:\Users\pilgrim\Downloads

07/28/2009  12:36 PM    <DIR>          .
07/28/2009  12:36 PM    <DIR>          ..
07/28/2009  12:36 PM    <DIR>          httplib2-python3-0.5.0
07/28/2009  12:33 PM            18,997 httplib2-python3-0.5.0.zip
               1 File(s)         18,997 bytes
               3 Dir(s)  61,496,684,544 bytes free

c:\Users\pilgrim\Downloads> cd httplib2-python3-0.5.0
c:\Users\pilgrim\Downloads\httplib2-python3-0.5.0> c:\python31\python.exe setup.py install
running install
running build
running build_py
running install_lib
creating c:\python31\Lib\site-packages\httplib2
copying build\lib\httplib2\iri2uri.py -> c:\python31\Lib\site-packages\httplib2
copying build\lib\httplib2\__init__.py -> c:\python31\Lib\site-packages\httplib2
byte-compiling c:\python31\Lib\site-packages\httplib2\iri2uri.py to iri2uri.pyc
byte-compiling c:\python31\Lib\site-packages\httplib2\__init__.py to __init__.pyc
running install_egg_info
Writing c:\python31\Lib\site-packages\httplib2-python3_0.5.0-py3.1.egg-info

V Mac OS X spusťte aplikaci Terminal.app, kterou najdete ve složce /Applications/Utilities/. V Linuxu spusťte aplikaci Terminal, kterou obvykle najdete v menu Applications pod Accessories nebo System.

you@localhost:~/Desktop$ unzip httplib2-python3-0.5.0.zip
Archive:  httplib2-python3-0.5.0.zip
  inflating: httplib2-python3-0.5.0/README
  inflating: httplib2-python3-0.5.0/setup.py
  inflating: httplib2-python3-0.5.0/PKG-INFO
  inflating: httplib2-python3-0.5.0/httplib2/__init__.py
  inflating: httplib2-python3-0.5.0/httplib2/iri2uri.py
you@localhost:~/Desktop$ cd httplib2-python3-0.5.0/
you@localhost:~/Desktop/httplib2-python3-0.5.0$ sudo python3 setup.py install
running install
running build
running build_py
creating build
creating build/lib.linux-x86_64-3.1
creating build/lib.linux-x86_64-3.1/httplib2
copying httplib2/iri2uri.py -> build/lib.linux-x86_64-3.1/httplib2
copying httplib2/__init__.py -> build/lib.linux-x86_64-3.1/httplib2
running install_lib
creating /usr/local/lib/python3.1/dist-packages/httplib2
copying build/lib.linux-x86_64-3.1/httplib2/iri2uri.py -> /usr/local/lib/python3.1/dist-packages/httplib2
copying build/lib.linux-x86_64-3.1/httplib2/__init__.py -> /usr/local/lib/python3.1/dist-packages/httplib2
byte-compiling /usr/local/lib/python3.1/dist-packages/httplib2/iri2uri.py to iri2uri.pyc
byte-compiling /usr/local/lib/python3.1/dist-packages/httplib2/__init__.py to __init__.pyc
running install_egg_info
Writing /usr/local/lib/python3.1/dist-packages/httplib2-python3_0.5.0.egg-info

Abychom mohli httplib2 používat, vytvoříme instanci třídy httplib2.Http.

>>> import httplib2
>>> h = httplib2.Http('.cache')                                                    
>>> response, content = h.request('http://diveintopython3.org/examples/feed.xml')  
>>> response.status                                                                
200
>>> content[:52]                                                                   
b"<?xml version='1.0' encoding='utf-8'?>\r\n<feed xmlns="
>>> len(content)
3070
  1. Primárním rozhraním k httplib2 je objekt třídy Http. Z důvodů, které si ukážeme v další podkapitole, bychom při vytváření objektu třídy Http měli vždy předávat jméno adresáře. Adresář nemusí existovat. V případě potřeby si jej httplib2 vytvoří.
  2. Jakmile máme objekt třídy Http k dispozici, můžeme data získat jednoduše tím, že zavoláme metodu request() a předáme jí adresu dat. Pro dané URL se tím vytvoří požadavek HTTP GET. (Později v této kapitole si ukážeme, jak můžeme vytvořit jiné HTTP požadavky, jako například POST.)
  3. Metoda request() vrací dvě hodnoty. První hodnotou je objekt třídy httplib2.Response, který obsahuje všechny HTTP hlavičky vrácené serverem. Například hodnota stavového kódu (status) 200 indikuje, že byl dotaz proveden úspěšně.
  4. Proměnná content obsahuje data, která HTTP server vrátil. Data se vracejí jako objekt typu bytes, nikoliv jako řetězec. Pokud z toho chceme udělat řetězec, musíme zjistit znakové kódování a převést si je sami.

Pravděpodobně budete potřebovat jen jeden objekt třídy httplib2.Http. Existují rozumné důvody pro vytváření více než jednoho objektu, ale měli byste to dělat jen v případě, kdy víte, proč je potřebujete. „Potřebuji získávat data ze dvou různých URL“ takovým důvodem není. Použijte objekt třídy Http znovu — prostě zavolejte metodu request() dvakrát.

Krátká odbočka vysvětlující, proč httplib2 vrací bajty místo řetězců

Bajty. Řetězce. To je bolest. Proč httplib2 nemůže „jednoduše“ provést konverzi za nás? No, ono je to komplikované, protože pravidla pro zjištění znakového kódování jsou specifická v závislosti na tom, jaký zdroj (resource) požadujeme. Jak by mohla httplib2 vědět, jaký druh zdroje požadujeme? Obvykle bývá uveden v HTTP hlavičce Content-Type, ale tato hlavička je v HTTP nepovinná a ne všechny HTTP servery ji vkládají. Pokud tato hlavička není součástí HTTP odpovědi, ponechává se odhad na klientovi. (Říká se tomu anglicky „content sniffing“ čili „čmuchání v obsahu“. Výsledek není nikdy perfektní.)

Pokud víme, jaký druh dat očekáváme (v našem případě XML dokument), mohli bychom „jednoduše“ předat objekt typu bytes funkci xml.etree.ElementTree.parse(). To by fungovalo, kdyby XML dokument obsahoval informaci o svém vlastním kódování znaků (jako je tomu v tomto případě). Ale jde o nepovinný údaj a ne všechny XML dokumenty ho používají. Pokud XML dokument informaci o kódování neobsahuje, měl by se klient podívat na transportní obálku — tj. na HTTP hlavičku Content-Type, která by mohla parametr charset obsahovat.

[Tričko podporuji RFC 3023]

Ale ono je to ještě horší. Teď už může být informace o kódování uvedena na dvou místech: uvnitř samotného XMLdokumentu a uvnitř HTTP hlavičky Content-Type. Jenže když je tato informace uvedena na obou místech, které z nich vyhraje? Podle RFC 3023 platí (a přísahám, to jsem si nevymyslel): pokud je v HTTP hlavičce Content-Type uveden typ média application/xml, application/xml-dtd, application/xml-external-parsed-entity nebo libovolný z podtypů application/xml, jako je application/atom+xml nebo application/rss+xml nebo dokonce application/rdf+xml, pak je kódování rovno

  1. kódování zadanému parametrem charset v HTTP hlavičce Content-Type nebo
  2. kódování zadanému atributem encoding v XML deklaraci uvnitř dokumentu nebo
  3. UTF-8

Na druhou stranu, pokud je v HTTP hlavičce Content-Type uveden typ média text/xml, text/xml-external-parsed-entity nebo podtyp jako text/AnythingAtAll+xml, pak se atribut uvádějící kódování v XML deklaraci uvnitř dokumentu zcela ignoruje a kódování je rovno

  1. kódování zadanému parametrem charset v HTTP hlavičce Content-Type nebo
  2. us-ascii

A to se bavíme jen o XML dokumentech. Pro HTML dokumenty vytvořily webové prohlížeče taková byzantská pravidla pro zjišťování obsahu (content-sniffing) [PDF], že se stále ještě snažíme všechna zjistit.

Opravy jsou vítány.“

Jak httplib2 zachází s mezipamětí

Vzpomínáte si, že jsem vás v předchozí podkapitole nabádal, abyste vždy vytvářeli objekt třídy httplib2.Http se zadaným jménem adresáře? Důvod se jmenuje mezipaměť (cache).

# pokračování z předchozího příkladu
>>> 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
  1. Tohle by vás nemělo moc překvapit. Stejnou věc už jsme dělali naposledy s tou výjimkou, že výsledek ukládáme do dvou nových proměnných.
  2. HTTP opět vrací stavový kód (status) 200, jako minule.
  3. Stažený obsah je také stejný jako minule.

Takže… koho to zajímá? Ukončete pythonovský interaktivní shell a spusťte nové sezení. Hned vám to ukážu.

# toto NENÍ pokračování z předchozího příkladu!
# Ukončete, prosím, interaktivní shell
# a spusťte nový.
>>> import httplib2
>>> httplib2.debuglevel = 1                                                        
>>> h = httplib2.Http('.cache')                                                    
>>> response, content = h.request('http://diveintopython3.org/examples/feed.xml')  
>>> len(content)                                                                   
3070
>>> response.status                                                                
200
>>> response.fromcache                                                             
True
  1. Zapněme ladění a podívejme se, co nám lítá po drátech. Takto se v httplib2 zapíná ladicí režim (srovnejte se zapínáním v http.client). httplib2 vytiskne všechna data, která se posílají na server, a některé klíčové informace, které se posílají zpět.
  2. Vytvoříme objekt třídy httplib2.Http se stejným jménem adresáře jako minule.
  3. Vyžádáme si stejné URL jako minule. Zdá se, že se nic nestalo. Přesněji řečeno, nic se neposílá na server a ze serveru se nic nevrací. Na síti nepozorujeme vůbec žádnou aktivitu.
  4. Přesto jsme nějaká data „přijali“ — ve skutečnosti jsme dostali všechno.
  5. A „přijali“ jsme také stavový kód protokolu HTTP, který říká, že „požadavek“ byl úspěšný.
  6. Tady je důvod: „odpověď“ byla vygenerována z lokální mezipaměti httplib2. Adresář, jehož jméno jsme zadávali při vytváření objektu třídy httplib2.Http, slouží knihovně httplib2 jako mezipaměť (cache) pro všechny operace, které se kdy provedly.

Pokud chcete v httplib2 zapnout ladicí režim, musíte nastavit konstantu na úrovni modulu (httplib2.debuglevel) a potom vytvořit nový objekt třídy httplib2.Http. Pokud chcete ladicí režim vypnout, musíte změnit tutéž konstantu na úrovni modulu a potom vytvořit nový objekt třídy httplib2.Http.

Minule jsme požadovali data z konkrétního URL. Požadavek byl úspěšný (status: 200). Odpověď zahrnovala nejen data publikovaného obsahu, ale také množinu hlaviček pro mezipaměť (caching headers). Ty každému příjemci říkají, že si tento zdroj může pamatovat po dobu až 24 hodin (Cache-Control: max-age=86400, což je 24 hodin v sekundách). httplib2 hlavičkám pro mezipaměť rozumí a respektuje je. Předchozí odpověď byla uložena do adresáře .cache (jehož jméno jsme zadali při vytváření objektu třídy Http). Platnost obsahu mezipaměti zatím nevypršela, takže když data ze stejného URL požadujeme podruhé, httplib2 jednoduše vrátí zapamatovaný výsledek, aniž by došlo ke komunikaci po síti.

Říkám „jednoduše“, ale za touto jednoduchostí je evidentně skryto hodně složitostí. Knihovna httplib2 zvládá používání mezipaměti v HTTP automaticky a aniž se o to musíme starat. Pokud z nějakého důvodu potřebujeme vědět, zda odpověď přichází z mezipaměti, můžeme zkontrolovat response.fromcache. Z jiného pohledu… prostě to funguje.

Dejme tomu, že teď máme data v mezipaměti, ale chceme ji obejít a znovu si je vyžádat od vzdáleného serveru. Prohlížeče to někdy dělají, když si to uživatel vyžádá. Například stisk F5 obnoví aktuální stránku, ale stiskem Ctrl+F5 se obejde mezipaměť a aktuální stránka se znovu vyžádá ze vzdáleného serveru. Možná si myslíte „aha, prostě smažu data ze své lokální mezipaměti a provedu požadavek znovu“. Tohle byste udělat mohli. Ale vzpomeňte si, že se to může týkat více stran než jen vás a vzdáleného serveru. Což takhle mezilehlé proxy-servery? Ty jsou zcela mimo vaši kontrolu a pořád mohou uchovávat ona data ve své mezipaměti. A s radostí vám je vrátí, protože obsah jejich mezipaměti je (z jejich pohledu) stále platný.

Takže místo toho, abyste manipulovali s lokální mezipamětí a doufali v nejlepší, měli byste využít vlastností HTTP k zajištění toho, že se váš požadavek skutečně dostal až ke vzdálenému serveru.

# pokračování předchozího příkladu
>>> 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'
…further debugging information omitted…
>>> response2.status
200
>>> 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'}
  1. httplib2 vám umožní přidat k jakémukoliv odcházejícímu požadavku libovolné HTTP hlavičky. Abychom obešli všechny mezipaměti (nejen lokální diskovou, ale také mezipaměťové proxy-servery mezi námi a vzdáleným serverem), přidáme do slovníku headers hlavičku no-cache.
  2. Teď vidíme, že httplib2 zahajuje síťový požadavek. httplib2 rozumí hlavičkám pro mezipaměť a respektuje je v obou směrech — jako součást přicházející odpovědi i jako součást odcházejícího požadavku. Knihovna si všimla, že jsme přidali hlavičku no-cache, takže úplně obešla své lokální mezipaměti. Potom ale nemá na výběr a musí odeslat požadavek na data do sítě.
  3. Tato odpověď nebyla generovaná z naší lokální mezipaměti. To samozřejmě víme, protože jsme viděli ladicí informaci týkající se odcházejícího požadavku. Ale je dobré, že si to můžeme ověřit v programu.
  4. Požadavek byl úspěšný. Opět jsme ze vzdáleného serveru stáhli celý publikovaný obsah (feed). Server samozřejmě poslal zpět s požadovanými daty (feed) i celou sadu HTTP hlaviček. Jsou mezi nimi i hlavičky pro mezipaměť, které httplib2 použije pro aktualizaci své lokální mezipaměti v naději, že se při příštím požadavku na stejná data bude moci vyhnout přístupu na síť. Návrh používání mezipamětí v HTTP je zcela podřízen maximalizaci úspěšnosti mezipamětí (cache hit) a minimalizaci přístupu k síti. I když jsme tentokrát mezipaměti obešli, vzdálený server by opravdu ocenil, kdybychom si výsledek do mezipaměti uložili — s ohledem na příští možný dotaz.

Jak httplib2 zachází s hlavičkami Last-Modified a ETag

Hlavičky mezipaměti Cache-Control a Expires se nazývají indikátory čerstvosti (freshness indicators). Říkají mezipamětem jasným způsobem, že se do vypršení platnosti obsahu mezipaměti můžeme zcela vyhnout přístupu k síti. Přesně takové chování jsme viděli v předchozí podkapitole: pokud je indikována čerstvost, httplib2 při vrácení dat z mezipaměti negeneruje ani bajt síťové aktivity (pokud ovšem explicitně nepředepíšeme obejití mezipaměti).

Ale jak to bude vypadat v případě, kdy se data mohla změnit, ale přitom se nezměnila? Pro tento účel HTTP definuje hlavičky Last-Modified a Etag. Těmto hlavičkám se říká validátory. Pokud už lokální mezipaměť není čerstvá, může klient s dalším dotazem zaslat validátory, aby si ověřil, zda se data skutečně změnila. Pokud se data nezměnila, server pošle zpět stavový kód 304 a žádná data. Takže tu sice stále dochází ke vzájemné komunikaci po síti, ale výsledkem je stahování menšího množství bajtů.

>>> 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'
>>> 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'}
>>> len(content)                                                  
6657
  1. Místo publikovaného obsahu (feed) budeme tentokrát stahovat domácí stránku webového místa (home page), která je v HTML. Protože tuto stránku požadujeme úplně poprvé, nemůže httplib2 s požadavkem nic moc udělat a odešle s ním minimum hlaviček.
  2. Odpověď obsahuje velké množství HTTP hlaviček… ale žádné informace pro mezipaměť. Ale obsahuje jak hlavičku ETag, tak hlavičku Last-Modified.
  3. V době vytváření příkladu měla stránka 6657 bajtů. Od té doby už se pravděpodobně změnila, ale tím se nebudeme zatěžovat.
# pokračování z předchozího příkladu
>>> 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'                                
>>> response.fromcache                                            
True
>>> response.status                                               
200
>>> response.dict['status']                                       
'304'
>>> len(content)                                                  
6657
  1. O stejnou stránku jsme požádali znovu, prostřednictvím stejného objektu třídy Http (a se stejnou lokální mezipamětí).
  2. httplib2 pošle serveru zpět validátor ETag jako obsah hlavičky If-None-Match.
  3. httplib2 pošle zpět serveru také validátor Last-Modified jako hodnotu hlavičky If-Modified-Since.
  4. Server se podívá na zaslané validátory, podívá se na požadovanou stránku a zjistí, že se stránka od posledního požadavku nezměnila. Proto pošle zpět stavový kód 304 a žádná data.
  5. A zpět ke klientovi. httplib2 obdrží stavový kód 304 a načte obsah stránky ze své mezipaměti.
  6. Tohle může být trošku matoucí. Ve skutečnosti tu máme dva stavové kódy — 304 (který vrátil server teď a který způsobil, že httplib2 použije svou mezipaměť) a 200 (který vrátil server minule a který je spolu s daty uložen v mezipaměti pro httplib2). response.status vrací stavový kód odpovědi z mezipaměti.
  7. Pokud chceme zjistit surový stavový kód vrácený serverem, můžeme jej zjistit nahlédnutím do response.dict, což je slovník aktuálních hlaviček vrácených serverem.
  8. Ať je to jakkoliv, data opět získáte v proměnné content. Obecně vzato, nepotřebujeme vědět, proč byl požadavek obsloužen z mezipaměti. (Dokonce nás nemusí vůbec zajímat, že byl obsloužen z mezipaměti. To je v pořádku. Knihovna httplib2 je dost chytrá na to, abychom si mohli hrát na hlupáky.) V tomto okamžiku už metoda request() vrátila řízení volajícímu kódu. httplib2 už aktualizovala svou mezipaměť a vrátila nám data.

Jak http2lib pracuje s kompresí

HTTP podporuje několik typů komprese. Dva nejpoužívanější typy jsou gzip a deflate. httplib2 podporuje oba.

>>> 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'
>>> 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'}
  1. Pokaždé když httplib2 odešle požadavek, vloží do něj hlavičku Accept-Encoding, kterou serveru oznámí, že zvládá jak kompresi deflate, tak gzip.
  2. V tomto případě server odpověděl daty komprimovanými algoritmem gzip. V tomto okamžiku metoda request() vrací řízení, httplib2 dekomprimovala (rozbalila) tělo odpovědi a umístila je do proměnné content. Pokud jste zvědaví, jestli odpověď přišla komprimovaná, můžete zkontrolovat response['-content-encoding']. Ale jinak si s tím nemusíte dělat starosti.

Jak httplib2 řeší přesměrování

HTTP definuje dva druhy přesměrování: dočasné a trvalé. U dočasných přesměrování se nedělá nic zvláštního až na to, že se mají následovat (follow), což httplib2 provede automaticky.

>>> 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'
  1. Na tomto URL není žádný publikovaný obsah. Nastavil jsem svůj server, aby signalizoval dočasné přesměrování na správnou adresu.
  2. Tady je náš požadavek.
  3. A tady je odpověď: 302 Found. I když se to zde nezobrazuje, odpověď obsahuje také hlavičku Location, která ukazuje na skutečné URL.
  4. httplib2 se ihned otočí a „následuje“ přesměrování vydáním dalšího požadavku na URL, které je uvedeno v hlavičce Location: http://diveintopython3.org/examples/feed.xml

„Následování“ přesměrování není nic jiného, než co ukazuje tento příklad. httplib2 pošle požadavek pro URL, které jsme požadovali. Server odvětí odpovědí, která říká: „Ne ne. Místo toho se podívejte támhle.“ httplib2 odešle další požadavek pro nové URL.

# pokračování z předchozího příkladu
>>> 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'}
  1. Odpověď (response), kterou jste obdrželi z jediného volání metody request(), je odpovědí z konečného URL.
  2. httplib2 přidá konečné URL do slovníku response jako content-location. Nejde o hlavičku, která by přišla ze serveru. Je to záležitost specifická pro httplib2.
  3. Jen abych nezapomněl, tento feed je komprimovaný.
  4. A uchovatelný v mezipaměti (cacheable). (To je důležité — jak uvidíme za minutku.)

Slovník response, který se nám vrátí, poskytuje informace o konečném URL. A co když chceme informace o přechodných URL, tedy o těch, která byla přesměrována na konečné URL? httplib2 nám umožní i to.

# pokračování z předchozího příkladu
>>> 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'}
>>> type(response)                                                        
<class 'httplib2.Response'>
>>> type(response.previous)
<class 'httplib2.Response'>
>>> response.previous.previous                                            
>>>
  1. Atribut response.previous uchovává referenci na předchozí objekt odpovědi, který httplib2 následovala, aby získala současný objekt odpovědi.
  2. Jak response, tak response.previous jsou objekty třídy httplib2.Response.
  3. To znamená, že můžeme řetězec přesměrování sledovat zpětně ještě dál kontrolou response.previous.previous. (Scénář: jedno URL je přesměrováno na druhé URL, které je přesměrováno na třetí URL. To se opravdu může stát!) V tomto případě už jsme dosáhli začátku řetězce přesměrování, takže atribut má hodnotu None.

Co se stane, když si vyžádáme stejné URL znovu?

# pokračování z předchozího příkladu
>>> 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
  1. Stejné URL, stejný objekt třídy httplib2.Http (a tím pádem stejná mezipaměť).
  2. Odpověď 302 nebyla v mezipaměti, takže httplib2 pošle pro stejné URL další požadavek.
  3. A ještě jednou, server odpovídá kódem 302. Ale všimněte si, co se nestalo: chybí druhý dotaz na konečné URL, http://diveintopython3.org/examples/feed.xml. Tato odpověď byla v mezipaměti (vzpomeňte si na hlavičku Cache-Control, kterou jsme viděli v předchozím příkladu). Jakmile httplib2 obdržela kód 302 Found, zkontrolovala si před vydáním dalšího požadavku obsah mezipaměti. Mezipaměť obsahovala čerstvou kopii http://diveintopython3.org/examples/feed.xml, takže nebylo nutné žádat o data znovu.
  4. V tomto okamžiku dochází k návratu z metody request(). Přečetla data publikovaného obsahu (feed) z mezipaměti a vrátila je. Jde samozřejmě o stejná data, která jsme obdrželi minule.

Jinými slovy, při dočasném přesměrování nemusíme dělat nic zvláštního. httplib2 je bude následovat automaticky. Skutečnost, že je jedno URL přesměrováno na jiné, nemá na httplib2 žádné dopady z hlediska podpory komprese, použití mezipaměti, ETagů nebo jakýchkoliv jiných rysů HTTP.

Trvalá přesměrování jsou stejně jednoduchá.

# pokračování z předchozího příkladu
>>> 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
  1. Ještě jednou. Toto URL ve skutečnosti neexistuje. Nastavil jsem svůj server, aby produkoval trvalé přesměrování na http://diveintopython3.org/examples/feed.xml.
  2. A tady to máme: stavový kód 301. Ale znovu si všimněte, co se nestalo: neobjevil se žádný požadavek na přesměrované URL. Proč ne? Protože už se nachází v lokální mezipaměti.
  3. httplib2 „následovala“ přesměrování přímo do své mezipaměti.

Ale počkejte! Ono je toho ještě víc!

# pokračování z předchozího příkladu
>>> response2, content2 = h.request('http://diveintopython3.org/examples/feed-301.xml')  
>>> response2.fromcache                                                                  
True
>>> content2 == content                                                                  
True
  1. Tady je ten rozdíl mezi dočasným a trvalým přesměrováním: jakmile jednou httplib2 následuje trvalé přesměrování, všechny další požadavky se stejným URL budou transparentně přepsány na cílové URL aniž se kvůli originálnímu URL komunikuje po síti. Připomeňme si, že ladicí režim je pořád zapnutý. Přesto nevidíme vůbec žádný výstup síťové aktivity.
  2. Ano, tato odpověď byla vytažena z lokální mezipaměti.
  3. Ano, dostali jsme celý publikovaný obsah (z mezipaměti).

HTTP. Funguje.

Za hranicemi HTTP GET

Webové služby nad HTTP se neomezují jen na požadavky typu GET. Co kdybychom chtěli vytvořit něco nového? Kdykoliv přidáte komentář do diskusního fóra, aktualizujete weblog, upravujete svůj stav na mikroblogové službě, jakou je Twitter nebo Identi.ca, používáte pravděpodobně HTTP POST.

Jak Twitter, tak Identi.ca nabízejí pro zveřejňování a aktualizaci vašeho stavu, popsaného 140 nebo méně znaky, jednoduché rozhraní založené na HTTP. Podívejme se na dokumentaci aplikačního rozhraní pro aktualizaci vašeho stavu v systému Identi.ca.

Identi.ca REST API Metoda: statuses/update
Aktualizuje stav autentizovaného uživatele. Vyžaduje parametr status, popsaný níže. Požadavek musí být typu POST.

URL
https://identi.ca/api/statuses/update.format
Formáty
xml, json, rss, atom
HTTP metod(y)
POST
Vyžaduje autentizaci
ano
Parametry
status. Povinný. Text aktualizace vašeho stavu. Kódované URL podle potřeby.

Jak to funguje? Když chceme na Identi.ca zveřejnit novou zprávu, musíme zaslat požadavek typu HTTP POST na http://identi.ca/api/statuses/update.format. (Část format nepatří k URL. Nahrazuje se datovým formátem, v jakém nám má server vrátit odpověď na náš požadavek. Takže pokud požadujeme odpověď v XML, musíme zaslat požadavek na https://identi.ca/api/statuses/update.xml.) Požadavek musí obsahovat parametr nazvaný status, který obsahuje text pro aktualizaci našeho stavu. A požadavek musí být autentizován.

Autentizován? Jistě. Když chceme na Identi.ca aktualizovat svůj stav, musíme prokázat svou totožnost. Identi.ca není jako wiki. Svůj vlastní stav můžeme aktualizovat jen my. Pro účel bezpečné a snadno použitelné autentizace používá Identi.ca HTTP Basic Authentication (základní autentizaci; známou také jako RFC 2617) přes SSL. httplib2 podporuje jak SSL, tak HTTP Basic Authentication, takže tahle část bude snadná.

Požadavek POST se od požadavku GET liší, protože nese náklad. Nákladem jsou data, která chceme poslat na server. Částí dat, kterou toto aplikační rozhraní metody vyžaduje, je status (stav) a měl by mít podobu kódovaného URL. Je to velmi jednoduchý serializační formát. Vstupem je množina dvojic klíč-hodnota (tj. slovník) a výsledkem je řetězec.

>>> from urllib.parse import urlencode              
>>> data = {'status': 'Test update from Python 3'}  
>>> urlencode(data)                                 
'status=Test+update+from+Python+3'
  1. V Pythonu pro zakódování slovníku do podoby URL najdeme pomocnou funkci: urllib.parse.urlencode().
  2. Aplikační rozhraní systému Identi.ca očekává zhruba takovýto slovník. Obsahuje jeden klíč, status, jehož hodnotou je text jedné aktualizace stavu.
  3. A takto vypadá řetězec kódovaného URL. To je náklad, který bude požadavkem HTTP POST odeslán „po drátě“ na server s aplikačním rozhraním Identi.ca.

>>> from urllib.parse import urlencode
>>> import httplib2
>>> httplib2.debuglevel = 1
>>> h = httplib2.Http('.cache')
>>> data = {'status': 'Test update from Python 3'}
>>> h.add_credentials('diveintomark', 'MY_SECRET_PASSWORD', 'identi.ca')    
>>> resp, content = h.request('https://identi.ca/api/statuses/update.xml',
...     'POST',                                                             
...     urlencode(data),                                                    
...     headers={'Content-Type': 'application/x-www-form-urlencoded'})      
  1. Tímto způsobem httplib2 pracuje s autentizací. Jméno a heslo uložíme metodou add_credentials(). Když se httplib2 pokusí o vydání požadavku, server odpoví stavovým kódem 401 Unauthorized (neautorizováno) a připojí seznam autentizačních metod, které podporuje (v hlavičce WWW-Authenticate). httplib2 automaticky vytvoří hlavičku Authorization a pošle požadavek s URL znovu.
  2. Druhý parametr uvádí typ HTTP požadavku. V tomto případě je to POST.
  3. Třetím parametrem je náklad, který se serveru posílá. Posíláme slovník se stavovou zprávou zakódovaný do podoby URL.
  4. Nakonec musíme serveru říct, že náklad má podobu dat zakódovaných do podoby URL.

Třetím parametrem metody add_credentials() je doména, ve které osobní údaje platí. Měli byste ji vždy uvádět! Pokud doménu vynecháte a později znovu použijete objekt třídy httplib2.Http pro jiné autentizované místo, mohla by httplib2 způsobit únik jména a hesla z jednoho místa na druhé místo (site).

A o čem zpívají dráty:

# pokračování z předchozího příkladu
send: b'POST /api/statuses/update.xml HTTP/1.1
Host: identi.ca
Accept-Encoding: identity
Content-Length: 32
content-type: application/x-www-form-urlencoded
user-agent: Python-httplib2/$Rev: 259 $

status=Test+update+from+Python+3'
reply: 'HTTP/1.1 401 Unauthorized'                        
send: b'POST /api/statuses/update.xml HTTP/1.1            
Host: identi.ca
Accept-Encoding: identity
Content-Length: 32
content-type: application/x-www-form-urlencoded
authorization: Basic SECRET_HASH_CONSTRUCTED_BY_HTTPLIB2  
user-agent: Python-httplib2/$Rev: 259 $

status=Test+update+from+Python+3'
reply: 'HTTP/1.1 200 OK'                                  
  1. Po prvním požadavku odpoví server stavovým kódem 401 Unauthorized. httplib2 nikdy neposílá autentizační hlavičky, pokud si o ně server explicitně neřekne. Server si o ně říká tímto způsobem.
  2. httplib2 okamžitě zareaguje opakovaným odesláním požadavku se stejným URL.
  3. Tentokrát obsahuje jméno a heslo, která jsme přidali metodou add_credentials().
  4. Funguje to!

A co vlastně server posílá po úspěšném požadavku zpět? To zcela závisí na aplikačním rozhraní příslušné webové služby. V některých protokolech (jako například Atom Publishing Protocol) posílá server zpět stavový kód 201 Created spolu s umístěním nově vytvořeného zdroje (resource) v hlavičce Location. Identi.ca posílá zpět 200 OK a XML dokument, který obsahuje informace o nově vytvořeném zdroji.

# pokračování z předchozího příkladu
>>> print(content.decode('utf-8'))                             
<?xml version="1.0" encoding="UTF-8"?>
<status>
 <text>Test update from Python 3</text>                        
 <truncated>false</truncated>
 <created_at>Wed Jun 10 03:53:46 +0000 2009</created_at>
 <in_reply_to_status_id></in_reply_to_status_id>
 <source>api</source>
 <id>5131472</id>                                              
 <in_reply_to_user_id></in_reply_to_user_id>
 <in_reply_to_screen_name></in_reply_to_screen_name>
 <favorited>false</favorited>
 <user>
  <id>3212</id>
  <name>Mark Pilgrim</name>
  <screen_name>diveintomark</screen_name>
  <location>27502, US</location>
  <description>tech writer, husband, father</description>
  <profile_image_url>http://avatar.identi.ca/3212-48-20081216000626.png</profile_image_url>
  <url>http://diveintomark.org/</url>
  <protected>false</protected>
  <followers_count>329</followers_count>
  <profile_background_color></profile_background_color>
  <profile_text_color></profile_text_color>
  <profile_link_color></profile_link_color>
  <profile_sidebar_fill_color></profile_sidebar_fill_color>
  <profile_sidebar_border_color></profile_sidebar_border_color>
  <friends_count>2</friends_count>
  <created_at>Wed Jul 02 22:03:58 +0000 2008</created_at>
  <favourites_count>30768</favourites_count>
  <utc_offset>0</utc_offset>
  <time_zone>UTC</time_zone>
  <profile_background_image_url></profile_background_image_url>
  <profile_background_tile>false</profile_background_tile>
  <statuses_count>122</statuses_count>
  <following>false</following>
  <notifications>false</notifications>
</user>
</status>
  1. Připomeňme si, že data vracená httplib2 jsou vždy bajty a ne řetězce. Abychom je mohli převést na řetězec, musíme je dekódovat s použitím příslušného znakového kódování. Aplikační rozhraní systému Identi.ca vždy vrací výsledky v UTF-8. Takže tato část je snadná.
  2. Zde je text stavové zprávy, kterou jsme právě zveřejnili.
  3. Toto je unikátní identifikátor nové stavové zprávy. Identi.ca jej používá pro konstrukci URL, které se dá použít pro zobrazení zprávy na webu.

A tady ji máme:

snímek obrazovky, který ukazuje zveřejněnou stavovou zprávu na Identi.ca

Za hranicemi HTTP POST

HTTP se neomezuje jen na GET a POST. Nepochybně jde o nejběžnější typy dotazů, obzvlášť ze strany webových prohlížečů. Ale rozhraní webových služeb může jít za hranice GET a POST — a knihovna httplib2 je na to připravená.

# pokračování z předchozího příkladu
>>> from xml.etree import ElementTree as etree
>>> tree = etree.fromstring(content)                                          
>>> status_id = tree.findtext('id')                                           
>>> status_id
'5131472'
>>> url = 'https://identi.ca/api/statuses/destroy/{0}.xml'.format(status_id)  
>>> resp, deleted_content = h.request(url, 'DELETE')                          
  1. Sever vrátil XML, že ano? A my už víme, jak XML zpracovat.
  2. Metoda findtext() najde první objekt odpovídající zadanému výrazu a extrahuje jeho textový obsah. V tomto případě hledáme element <id>.
  3. Z textového obsahu elementu <id> můžeme zkonstruovat URL pro vymazání stavové zprávy, kterou jsme zrovna zveřejnili.
  4. Zprávu vymažeme tím, že pro zmíněné URL vytvoříme požadavek HTTP DELETE.

Po drátech běhá následující:

send: b'DELETE /api/statuses/destroy/5131472.xml HTTP/1.1      
Host: identi.ca
Accept-Encoding: identity
user-agent: Python-httplib2/$Rev: 259 $

'
reply: 'HTTP/1.1 401 Unauthorized'                             
send: b'DELETE /api/statuses/destroy/5131472.xml HTTP/1.1      
Host: identi.ca
Accept-Encoding: identity
authorization: Basic SECRET_HASH_CONSTRUCTED_BY_HTTPLIB2       
user-agent: Python-httplib2/$Rev: 259 $

'
reply: 'HTTP/1.1 200 OK'                                       
>>> resp.status
200
  1. „Odstraň tuto stavovou zprávu.“
  2. „Je mi líto, Dave [dejve]. Obávám se, že to nemohu udělat.“
  3. „Neautorizováno Hmmm. Odstraň tu stavovou zprávu, prosím
  4. …a tady je mé jméno a heslo.“
  5. „Považuj to za hotovou věc!“

Puf a je to pryč.

snímek obrazovky, který ukazuje odstraněnou zprávu na Identi.ca

Přečtěte si

httplib2:

Práce HTTP s mezipamětí:

RFC:

© 2001–11 Mark Pilgrim