|
| 1 | +# Requests + Click |
| 2 | + |
| 3 | +## Co je cílem tohoto cvičení? |
| 4 | + |
| 5 | +Po projití této lekce byste měli být obeznámeni se základním použitím knihoven |
| 6 | +`requests` a `click`. Skončíme s programem, který bude umět převádět peníze z |
| 7 | +českých korun do jiných měn podle aktuálního kurzu. |
| 8 | + |
| 9 | +## Předpoklady |
| 10 | + |
| 11 | +Předpokládáme základní znalost Pythonu. Měli byste mít počítač s nainstalovaným |
| 12 | +interpretem jazyka Python ve verzi aspoň 3.6. Pro začátek si také vytvořte nové |
| 13 | +virtuální prostředí. |
| 14 | + |
| 15 | +Dále se vám bude hodit základní přehled o tom, jak funguje internet, co je to |
| 16 | +URL a podobné drobnosti. Pokud si nejste jistí, začněte [tímto shrnutím pro |
| 17 | +začátečníky]({{ lesson_url('fast-track/http') }}). |
| 18 | + |
| 19 | + |
| 20 | +## Requests |
| 21 | + |
| 22 | +Začneme seznámením s knihovnou [requests]. Je to knihovna určená pro HTTP |
| 23 | +požadavky na straně klienta. Poskytuje mnohem pohodlnější rozhraní než |
| 24 | +standardní knihovna Pythonu. |
| 25 | + |
| 26 | +[requests]: http://docs.python-requests.org/en/master/ |
| 27 | + |
| 28 | +Prvním krokem by měla být instalace ve virtuálním prostředí: |
| 29 | + |
| 30 | +```console |
| 31 | +(venv) $ python -m pip install requests |
| 32 | +``` |
| 33 | + |
| 34 | +První pokus je ideální provádět v interaktivní konzoli Pythonu. Začneme tím, že |
| 35 | +si naimportujeme modul `requests`. Komunikace přes protokol HTTP používá model |
| 36 | +požadavek/odpověď (*request*/*response*). Klient tedy nejprve pošle požadavek, |
| 37 | +a server potom odpovídá. Takto se střídají, dokud klient nemá vše, co |
| 38 | +potřebuje, nebo nedojde k chybě. |
| 39 | + |
| 40 | +Pro začátek se podíváme na stránku `https://example.com`. |
| 41 | + |
| 42 | +```pycon |
| 43 | +>>> import requests |
| 44 | +>>> response = requests.get("https://example.com/") |
| 45 | +>>> response |
| 46 | +<Response [200]> |
| 47 | +``` |
| 48 | + |
| 49 | +Takto vypsaná odpověď není příliš užitečná. To naštěstí není zase takový |
| 50 | +problém. V proměnné `response` teď máme object, který má potřebná data uložená |
| 51 | +v různých atributech. |
| 52 | + |
| 53 | +Zkuste si vypsat, co obsahují atributy `response.text`, `response.status_code`, |
| 54 | +`response.encoding` a `response.history`. Taky vyzkoušejte zavolat metodu |
| 55 | +`response.json()`. Existuje jich mnohem více, ale tyto jsou docela zajímavé a |
| 56 | +relativně často užívané. |
| 57 | + |
| 58 | +Na tyto experimenty použijte dvě jiné adresy (protože `example.com` není příliž |
| 59 | +zajímavý web). |
| 60 | + |
| 61 | +* `https://httpbin.org/get` |
| 62 | +* `https://httpbin.org/redirect-to?url=http://example.com&status_code=301` |
| 63 | + |
| 64 | +> [note] |
| 65 | +> <https://httpbin.org/> je velice užitečná služba, pokud si potřebujete |
| 66 | +> vyzkoušet komunikaci přes HTTP. Bude vám odpovídat na všemožné požadavky |
| 67 | +> podle toho, jak si řeknete. Podívejte se v prohlížeči a uvidíte docela pěkný |
| 68 | +> seznam všech možností (akorát v angličtině) |
| 69 | +
|
| 70 | +Pojďme se tedy podívat, co dělají zmíněné jednotlivé atributy: |
| 71 | + |
| 72 | +Atribut `text` obsahuje tělo odpovědi, tak jak nám oze serveru přišla. Pro |
| 73 | +většinu stránek to bude kód v jazyku HTML, nebo v data v různých formátech. |
| 74 | + |
| 75 | +Každá odpověď od serveru obsahuje číselný kód, který popisuje výsledek akce. |
| 76 | +Tento kód si můžete přečíst z atributu `status_code`. `1xx` jsou informační |
| 77 | +zprávy, na které moc často nenarazíte. `2xx` jsou úspěšné odpovědi. Někdy se |
| 78 | +může stát, že server místo odpovědi, kterou chcete, odešle *přesměrování*. To |
| 79 | +má podobu odpovědi s kódem `3xx`. Přímo tuto odpověď neuvidíte, protože |
| 80 | +knihovna `requests` ví, že je to přesměrování a proto automaticky půjde na |
| 81 | +adresu, kam vás server poslal. |
| 82 | + |
| 83 | +Ke každému číselnému kódu existuje i texotvý popis. Ty najdete třeba na |
| 84 | +[Wikipedii](), nebo můžete použít <https://http.cat>. |
| 85 | + |
| 86 | +Pokud dojde k přesměrování (a může jich být i několik), můžete se podívat na |
| 87 | +jednotlivé odpovědi v atributu `history`. Je to seznam, který bude pro každé |
| 88 | +přesměrování obsahovat jeden objekt. |
| 89 | + |
| 90 | +Atribut `encoding` je užitečný v případě, že vám správně nefungují české znaky |
| 91 | +v odpovědi. Můžete se v něm podívat, co vám server tvrdí o datech, která vám |
| 92 | +posílá. |
| 93 | + |
| 94 | +Nakonec nám zůstává metoda `json()`. JSON je datový formát, který používá mnoho |
| 95 | +různých webových služeb. Proto `requests` nabízí tuto zkratku, jak se k datům |
| 96 | +dostat. Ale pozor! Pokud v odpovědit nejsou data v tomto formátu, dostanete |
| 97 | +chybu! (A toto je očekávané chování u druhé testovací URL.) |
| 98 | + |
| 99 | + |
| 100 | +### Parametry pro GET |
| 101 | + |
| 102 | +Ve druhé testovací URL si můžete všimnout, že obsahuje otazník a za ním nějaké |
| 103 | +další informace. Toto jsou parametry pro server, které mu říkají, co přesně od |
| 104 | +něj chceme. Typický příklad ze života je vyhledávací políčko na libovolném |
| 105 | +webu. Vyhledávaná fráze se na server stejným způsobem jako parametr. |
| 106 | + |
| 107 | +Ruční zpracování a přilepení k samotné URL ale není úplně jednoduché. Musíte |
| 108 | +myslet na to, že některé znaky je potřeba zakódovat. Proto `requests` poskytují |
| 109 | +lepší možnost, jak s parametry pracovat. |
| 110 | + |
| 111 | +Můžeme si nadefinovat slovník, kde klíče budou názvy parametrů (které obvykle |
| 112 | +závisí na tom, co server očekává), a hodnoty budou samotná data, která chceme |
| 113 | +posílat. |
| 114 | + |
| 115 | + |
| 116 | +```pycon |
| 117 | +>>> parametry = {"status_code": 301, "url": "https://example.com"} |
| 118 | +>>> r = requests.get("https://httpbin.org/redirect-to", params=parametry) |
| 119 | +``` |
| 120 | + |
| 121 | +V tomto případě *httpbin* potřebuje informaci o tom, kam a jak nás má |
| 122 | +přesměrovat. |
| 123 | + |
| 124 | +### Posílání dat |
| 125 | + |
| 126 | +Knihovna `requests` umí data nejenom přijímat, ale i posílat. K tomu slouží |
| 127 | +metoda `post()`. |
| 128 | + |
| 129 | +Jendoduchý příklad je: |
| 130 | + |
| 131 | +```pycon |
| 132 | +>>> r = requests.post("https://httpbin.org/post", {"ahoj": "svete"}) |
| 133 | +``` |
| 134 | + |
| 135 | +V praxi bývá často potřeba řešit situaci, že server vyžaduje přihlášení. A tam |
| 136 | +je potřeba pracovat případ od případu. Každopádně knihovna `requests` vám |
| 137 | +umožní použít všechny obvyklé přihlašovací metody. |
| 138 | + |
| 139 | +### Stažení velkého souboru |
| 140 | + |
| 141 | +Jeden detail, který je poměrně snadné přehlédnout, je to, že všechny příklady |
| 142 | +výše provedou požadavek, a potom stáhnou celou odpoveď a uloží ji v paměti |
| 143 | +počítače. To je v pohodě, pokud to je něco relativně malého. Pokud budete |
| 144 | +stahovat třeba video, úplně fajn to není. Proto můžete použít tento recept, |
| 145 | +který vytvoří spojení se serverem, potom čte kousky dat po 8 kilobajtech a |
| 146 | +rovnou je zapisuje do souboru. |
| 147 | + |
| 148 | +```python |
| 149 | +import requests |
| 150 | +with requests.get("https://placekitten.com/400/600") as r: |
| 151 | + r.raise_for_status() |
| 152 | + with open("kitten.jpg", "w") as f: |
| 153 | + for chunk in r.iter_content(8196): |
| 154 | + if chunk: |
| 155 | + f.write(chunk) |
| 156 | +``` |
| 157 | + |
| 158 | +Za vypíchnutí tady stojí jedna nová metoda: `raise_for_status()`. Po provedení |
| 159 | +požadavku je potřeba zkontrolovat, jestli se nám to podařilo. Klasicky se to |
| 160 | +dělá kontrolou hodnoty atributu `status_code`. Metoda `raise_for_status()` je |
| 161 | +zkratka: pokud nám server vrátil nějakou chybu, tato metoda vyhodí výjimku, |
| 162 | +kterou můžeme zpracovat. Pro úspěšnou odpověď tato metoda neědělá nic. |
| 163 | + |
| 164 | + |
| 165 | +### Cvičení |
| 166 | + |
| 167 | +Česká národní banka zveřejňuje denní kurzy, které je možné si stáhnout. Navíc |
| 168 | +jsou v pěkném textovém formátu, se kterým se nám bude pěkně pracovat. |
| 169 | + |
| 170 | +Adresa je |
| 171 | +<http://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.txt?date=01.04.2019>. |
| 172 | +Datum je ve formátu den.měsíc.rok. Pokud datum nezadáte nebo je špatně, |
| 173 | +dostanete poslední kurzy. |
| 174 | + |
| 175 | +Napište si funkci, která dostane jeden argument: datum ve správném formátu |
| 176 | +(jako řetezec). Tato funkce stáhne kurzovní lístek a vrátí data v libovolné |
| 177 | +podobě, se kterou se nám bude dále pracovat. |
| 178 | + |
| 179 | +Mohla by se vám hodit tato funkce, která přečte textovou odpoveď, rozseká ji na |
| 180 | +kousky a vrátí slovník. Klíče jsou zkratky měn, hodnoty jsou kurzy. |
| 181 | + |
| 182 | +```python |
| 183 | +def parse_rates(text): |
| 184 | + hlavicka, jmena, *radky = text.splitlines() |
| 185 | + kurzy = {} |
| 186 | + for radek in radky: |
| 187 | + _, _, castka, mena, hodnota = radek.replace(",", ".").split("|") |
| 188 | + kurzy[mena] = float(castka) / float(hodnota) |
| 189 | + return kurzy |
| 190 | +``` |
| 191 | + |
| 192 | +Řešení najdete na konci této stránky. |
| 193 | + |
| 194 | + |
| 195 | +## Click |
| 196 | + |
| 197 | +Když instalujete knihovnu, zadáváte příkaz `python -m pip install foo`. V tomto |
| 198 | +případě `python` je název příkazu, který chcete spustit, a ostatní slova na |
| 199 | +tomto řádku (oddělená mezerami), jsou argumenty tohoto příkazu. |
| 200 | + |
| 201 | +Dříve nebo později narazíte na to, že vaše programy budou potřebovat nějaký |
| 202 | +vstup od uživatele. Číst je vždy přes funkci `input()` není úplně pohodlné ani |
| 203 | +pro uživatele, ani pro programátora. Proto je dobré vědět, jak definovat a |
| 204 | +používat argumenty. |
| 205 | + |
| 206 | +Existuje hodně knihoven, které umožňují zpracovávat argumenty na příkazové |
| 207 | +řádce. Jenom samotná standardní knihovna Pythonu má `getopt`, `optparse` a |
| 208 | +`argparse`. Ty ale nejsou úplně příjemné na používání. |
| 209 | + |
| 210 | +Oproti tomu knihovna [click] poskytuje rozhraní, ve kterém můžete jednoduché |
| 211 | +programy sekat jako Baťa cvičky. Cenou je lehce magický způsob, jak argumenty |
| 212 | +definovat, a taky ztráta možnosti ovlivnit do nejjemnějších detailů, jak se |
| 213 | +program má chovat. To ale obvykle není problém. |
| 214 | + |
| 215 | +[click]: https://click.palletsprojects.com/en/7.x/ |
| 216 | + |
| 217 | +### Trocha teorie |
| 218 | + |
| 219 | +Různé systémy používají různé konvence, jak by měly argumenty vypadat a |
| 220 | +fungovat. Tady si popíšeme, jak se slušně vychované programy chovají na Linuxu |
| 221 | +(nebo na Macu). |
| 222 | + |
| 223 | +Existují dvě základní kategorie: argumenty a přepínače. Argumenty jsou většinou |
| 224 | +(ale ne vždy) vyžadované, přepínače obvykle potřeba nejsou. Argumenty jsou dané |
| 225 | +pořadím (pokud jich je víc), přepínače mají jména. |
| 226 | + |
| 227 | +Jména přepínačů obvykle začínají dvěmi pomlčkami, pokud mají hezké čitelné |
| 228 | +jméno, nebo jednou pomlčkou, pokud je to jenom jedno písmeno. Dost často jeden |
| 229 | +přepínač může mít jak jednopísmenné jméno, tak i delší a čitelnější. |
| 230 | + |
| 231 | + |
| 232 | +### Instalace |
| 233 | + |
| 234 | +Nic překvapivého: |
| 235 | + |
| 236 | +```console |
| 237 | +(venv) $ python -m pip install click |
| 238 | +``` |
| 239 | + |
| 240 | +### Hello world |
| 241 | + |
| 242 | +Na tomto jednoduchém programu si ukážeme, jak se dá funkce změnit v něco, co |
| 243 | +bude pěkně použitelné na příkazové řádce. |
| 244 | + |
| 245 | +```python |
| 246 | +import click |
| 247 | + |
| 248 | +@click.command() |
| 249 | +@click.option("--kolikrat", default=1, help="Kolikrát budeme zdravit") |
| 250 | +@click.option("--jmeno", prompt="Tvoje jméno", |
| 251 | + help="Koho budeme zdravit") |
| 252 | +def hello(kolikrat, jmeno): |
| 253 | + for x in range(kolikrat): |
| 254 | + click.echo(f"Ahoj {jmeno}!") |
| 255 | + |
| 256 | + |
| 257 | +if __name__ == "__main__": |
| 258 | + hello() |
| 259 | +``` |
| 260 | + |
| 261 | +Funguje to takto: |
| 262 | + |
| 263 | +```console |
| 264 | +(venv) $ python hello.py --kolikrat 3 --jmeno Adame |
| 265 | +Ahoj Adame! |
| 266 | +Ahoj Adame! |
| 267 | +Ahoj Adame! |
| 268 | +``` |
| 269 | + |
| 270 | +Příkazům začínajícím zavináčem před definicí funkce říkáme dekorátory. Je to |
| 271 | +možnost, jak v Pythonu můžeme ovlivnit chování funkce (a pravděpodobně se jim |
| 272 | +budeme věnovat trochu více v některé následující lekci). |
| 273 | + |
| 274 | +První řádek `@click.command()` říká, že následující funkce by se měla chovat |
| 275 | +jako příkaz. |
| 276 | + |
| 277 | +Další dva řádky definují přepínače tohoto příkazu. |
| 278 | + |
| 279 | +První z nich se jmenuje `--kolikrat`, a pokud ho nezadáme, dostane výchozí |
| 280 | +hodnotu 1. *Click* z této výchozí hodnoty pozná, že hodnotou toho přepínače |
| 281 | +bude vždy číslo. Takže když zkusíme zadat jiný text, dostaneme chybu. Argument |
| 282 | +předaný do funkce `hello()` bude už typu `int`. |
| 283 | + |
| 284 | +Druhý argument bude jméno. Typ nijak nespecifikuje, takže to bude řetězec. |
| 285 | +`prompt` říká, že pokud přepínač nezadáme, program se nás zeptá. |
| 286 | + |
| 287 | +Zkuste si s tímto programem chvilku hrát. Nezapomeňte, že *click* vypíše pěknou |
| 288 | +nápovědu, pokud program spustíte s přepínačem `--help`. |
| 289 | + |
| 290 | + |
| 291 | +### Další možnosti |
| 292 | + |
| 293 | + |
| 294 | +Možné typy přepínačů (použití: `@click.option(…, type=click.X, …`): |
| 295 | + |
| 296 | + * `click.INT` – celé číslo |
| 297 | + * `click.FLOAT` – číslo s desetinnou tečkou |
| 298 | + * `click.FILE` – název souboru na příkazové řádce, ale funkce už dostane |
| 299 | + otevřený soubor a *click* se sám postará i o zavření |
| 300 | + |
| 301 | +Další možnosti jsou třeba `multiple=True`. Tím přepínač změníme tak, že ho bude |
| 302 | +možné zadávat několikrát. Funkce potom dostane n-tici hodnot. |
| 303 | + |
| 304 | +Argumenty se definují velmi podobně jako přepínače. Jediný rozdíl je v použitém |
| 305 | +dekorátoru `@click.argument()`. Jména argumentů se zadávají bez úvodních |
| 306 | +pomlček. |
| 307 | + |
| 308 | +*Click* taky umožňuje vypisování na výstup. `click.echo` se chová velmi podobně |
| 309 | +jako `print`, akorát se snaží lépe fungovat, pokud máte rozbitý terminál. |
| 310 | + |
| 311 | + |
| 312 | +### Cvičení |
| 313 | + |
| 314 | +Napište program, který bude vypisovat tuto nápovědu: |
| 315 | + |
| 316 | +```console |
| 317 | +(venv) $ python cnb.py --help |
| 318 | +Usage: cnb.py [OPTIONS] CASTKA |
| 319 | + |
| 320 | +Options: |
| 321 | + --datum TEXT |
| 322 | + --mena TEXT může být zadaný vícekrát |
| 323 | + --help Show this message and exit. |
| 324 | +``` |
| 325 | + |
| 326 | + |
| 327 | +## Dokončení programu |
| 328 | + |
| 329 | +Zkombinujte výsledky obou cvičení do jednoho programu. Tento program bude |
| 330 | +vyžadovat jedno číslo. To bude částka v korunách. Program načte buď kurzy podle |
| 331 | +zadaného data, nebo poslední zveřejněné. |
| 332 | + |
| 333 | +Pokud nebude zadaná žádná měna, program převede částku do všech dostupných měn |
| 334 | +a vypíše je v nějakém pěkném formátu. Pokud budou nějaké měny zadané, bude |
| 335 | +převádět jen do nich. |
| 336 | + |
| 337 | + |
| 338 | +## Řešení |
| 339 | + |
| 340 | +Zkus si ale cvičení nejdřív vyřešit bez pomoci :) |
| 341 | + |
| 342 | +```python |
| 343 | +import click |
| 344 | +import requests |
| 345 | + |
| 346 | + |
| 347 | +def parse_rates(text): |
| 348 | + hlavicka, jmena, *radky = text.splitlines() |
| 349 | + kurzy = {} |
| 350 | + for radek in radky: |
| 351 | + _, _, castka, mena, hodnota = radek.replace(",", ".").split("|") |
| 352 | + kurzy[mena] = float(castka) / float(hodnota) |
| 353 | + return kurzy |
| 354 | + |
| 355 | + |
| 356 | +def get_exchange_rates(datum=None): |
| 357 | + parametry = {} |
| 358 | + if datum: |
| 359 | + # Pokud máme datum, použijeme ho. Prázdný slovník parametrů nemá na |
| 360 | + # výsledek žádný vliv. |
| 361 | + parametry["date"] = datum |
| 362 | + response = requests.get( |
| 363 | + "http://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.txt", |
| 364 | + params=parametry, |
| 365 | + ) |
| 366 | + return parse_rates(response.text) |
| 367 | + |
| 368 | + |
| 369 | +@click.command() |
| 370 | +@click.option("--datum") |
| 371 | +@click.option("--mena", multiple=True) |
| 372 | +@click.argument("castka", type=click.FLOAT) |
| 373 | +def cnb(castka, datum, mena): |
| 374 | + kurzy = get_exchange_rates(datum) |
| 375 | + for zkratka_meny in sorted(kurzy): |
| 376 | + # Pokud nemáme žádné měny, nebo tato měna byla zadaná … |
| 377 | + if not mena or zkratka_meny in mena: |
| 378 | + # … tak převedeme částku a vypíšeme ji. |
| 379 | + prevedeno = castka * kurzy[zkratka_meny] |
| 380 | + click.echo(f"{castka} CZK = {prevedeno} {zkratka_meny}") |
| 381 | + |
| 382 | + |
| 383 | +if __name__ == "__main__": |
| 384 | + cnb() |
| 385 | +``` |
0 commit comments