Webes programozás Pythonban

Kategória: Python.

Webszerver létrehozása

Pythonban a http.server belső könyvtár segítségével tudunk webszervert létrehozni.

Helló, világ!

Egy egyszerű webszervert, ami generált HTML oldallal tér vissza, az alábbi módon tudunk létrehozni:

import http.server
 
class WebSzerver(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html; charset=utf-8')
        self.end_headers()
        self.wfile.write(bytes('<html><body>Helló, <b>Python</b> világ!</body></html>', 'UTF-8'))
 
http.server.HTTPServer(('localhost', 8080), WebSzerver).serve_forever()

Ha futtatjuk, akkor böngészőből nyissuk meg a http://localhost:8080/ oldalt. Kb. a következőt kell látnunk:

hellovilag.png

Paraméterek

Az alábbi programban a következőket fogjuk beletenni a generált válaszba. Tegyük fel, hogy a beírt URL a következő: http://localhost:8080/gyumolcs?fajta=alma&darab=2.

  • A szerver utáni útvonalat, ami jelen esetben a /gyumolcs.
  • A paramétereket, ami a ? utáni rész, jelen példában fajta=alma&darab=2. Ez valójában két kulcs-érték pár; lista formájában adjuk vissza.
  • Többféle lekérdezési típus lehet, pl. GET ill. POST. A fenti példa GET. POST lekérdezés esetén kiírjuk az átküldött adatokat.
import http.server
import urllib.parse
 
class WebSzerver(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        self.válasz('GET')
 
    def do_POST(self):
        self.válasz('POST')
 
    def válasz(self, lekérdezés):
        elemzett_url = urllib.parse.urlparse(self.path)
        paraméterek = urllib.parse.parse_qs(elemzett_url.query)
        self.send_response(200)
        self.send_header('Content-type', 'text/html; charset=utf-8')
        self.end_headers()
        self.wfile.write(bytes('<html><body>', 'UTF-8'))
        self.wfile.write(bytes('<p>Lekérdezés: ' + lekérdezés + '</p>', 'UTF-8'))
        self.wfile.write(bytes('<p>Útvonal: <code>' + elemzett_url.path + '</code></p>', 'UTF-8'))
        self.wfile.write(bytes('<p>Paraméterek:<ul>', 'UTF-8'))
        for kulcs in paraméterek:
            self.wfile.write(bytes('<li>' + kulcs + ': ' + paraméterek[kulcs][0], 'UTF-8'))
        self.wfile.write(bytes('</ul></p>', 'UTF-8'))
        if (lekérdezés == 'POST'):
            adat = self.rfile.read(int(self.headers['Content-Length'])).decode('utf-8')
            self.wfile.write(bytes('<p>Adat: ' + adat + '</p>', 'UTF-8'))
        self.wfile.write(bytes('</body></html>', 'UTF-8'))
 
http.server.HTTPServer(('localhost', 8080), WebSzerver).serve_forever()

Ha tehát megnyitjuk egy böngészővel a fenti linket (http://localhost:8080/gyumolcs?fajta=alma&darab=2), akkor az alábbi eredményt kapjuk:

get.png

POST üzenetet tipikusan akkor szoktunk használni, ha olyan formanyomtatvány eredményét dolgozzuk fel, amely nagyobb mennyiségű adatot tartalmaz. Közvetlenül kiváltani egy ilyen hívást nem egyszerű, ahhoz célszerű segédprogramot használni. Az egyik legegyszerűbb ilyen alkalmazás a parancssori curl, amit a https://curl.se/ oldalról tudunk letölteni.

A HTML alapértelmezésben ugyanúgy generálja a POST hívás tartalmát, mintha az URL része lenne, tehát & jellel elválasztott kulcsérték párok formájában. A következő hívás lehetne egy valós példa is:

curl -X POST -d "fajta=alma&darab=2" http://localhost:8080/gyumolcs

Ez esetben parancssorban kapjuk meg a választ:

<html><body><p>Lekérdezés: POST</p><p>Útvonal: <code>/gyumolcs</code></p><p>Paraméterek:<ul></ul></p><p>Adat: fajta=alma&darab=2</p></body></html>

Ebben a példában nincsenek paraméterek, viszont az adat megjelent.

Egy egyszerű alkalmazás

A fent bemutatott lehetőségekkel valójában fel tudunk építeni egy alkalmazást. Ebben gyümölcsöket fogunk számolni: alapból van egy memória mini-adatbázis gyümölcs-darabszám formában, amit egyrészt le tudunk kérdezni, másrészt hozzá tudunk adni. A program za alábbi:

import http.server
import urllib.parse
 
class WebSzerver(http.server.SimpleHTTPRequestHandler):
    gyümölcsök = {
        'alma': 3,
        'banán': 2,
        'narancs': 5,
    }
 
    def do_GET(self):
        elemzett_url = urllib.parse.urlparse(self.path)
        if elemzett_url.path == '/gyumolcs':
            self.hozzáad(elemzett_url.query)
            self.válasz()
 
    def hozzáad(self, adat):
        paraméterek = urllib.parse.parse_qs(adat)
        if 'fajta' in paraméterek and 'darab' in paraméterek:
            fajta = paraméterek['fajta'][0]
            darab = int(paraméterek['darab'][0])
            if fajta not in self.gyümölcsök:
                self.gyümölcsök[fajta] = 0
            self.gyümölcsök[fajta] += darab
            if self.gyümölcsök[fajta] == 0:
                self.gyümölcsök.pop(fajta)
 
    def válasz(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html; charset=utf-8')
        self.end_headers()
        self.wfile.write(bytes('<html><body>Gyümölcsök:<ul>', 'UTF-8'))
        for gyümölcs in self.gyümölcsök:
            self.wfile.write(bytes('<li>' + gyümölcs + ': ' + str(self.gyümölcsök[gyümölcs]), 'UTF-8'))
        self.wfile.write(bytes('</ul></body></html>', 'UTF-8'))
 
http.server.HTTPServer(('localhost', 8080), WebSzerver).serve_forever()

Itt már ügyelünk arra, hogy csak a /gyumolcs lekérdezésre adjunk választ. Ha megnyitjuk a http://localhost:8080/gyumolcs oldalt, akkor az alábbi eredményt kapjuk:

lekerdez.png

A következő sorral két darab almát tudunk hozzáadni: http://localhost:8080/gyumolcs?fajta=alma&darab=2. Eredmény:

hozzaad.png

Negatív darabszámmal csökkenteni tudjuk a darabszámot, ha pedig kinullázódik, akkor töröljük. Új gyümölcsöt is hozzá tudunk adni.

Ha a POST lekérdezést is támogatni szeretnénk, akkor a következőt adjuk hozzá a programhoz, praktikusan a do_GET alá, azzal egy magasságban:

    def do_POST(self):
        elemzett_url = urllib.parse.urlparse(self.path)
        if elemzett_url.path == '/gyumolcs':
            self.hozzáad(self.rfile.read(int(self.headers['Content-Length'])).decode('utf-8'))
            self.válasz()

Figyeljük meg, hogy ugyanazon a végponton két kapcsolatot is létesíthetünk: egy GET-et és egy POST-ot. Ez egyébként elég gyakori: a GET általában lekérdezésre szolgál, a POST létrehozásra, a DELETE törlésre.

Ne felejtsük újraindítani a szervert. Kipróbálása:

curl -X POST -d "fajta=alma&darab=2" http://localhost:8080/gyumolcs

Eredmény:

<html><body>Gyümölcsök:<ul><li>alma: 5<li>banán: 2<li>narancs: 5</ul></body></html>

Most, hogy kész a backend rész, valósítsuk meg hozzá egy frontendet:

<html>
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        <form action="http://localhost:8080/gyumolcs" method="get">
            Fajta: <input type="text" name="fajta"><br>
            Darab: <input type="text" name="darab"><br>
            <input type="submit" value="Elküld">
        </form>
    </body>
</html>

Ha közvetlenül megnyitjuk ezt az oldalt, akkor kitöltve az alábbit kapjuk:

form.png

Ez esetben ha az Elküld gombra kattintunk, akkor megjelenik a módosított darabszám.

AJAX

Az AJAX betűszó az Asynchronous JavaScript And XML rövidítése, ami arra utal, hogy JavaScript segítségével lehet aszinkron módon XML formátumú adatot lekérdezni. Ezzel a szűk adatcserére csökken a kliens-szerver kommunikáció: magát az oldalt csak egyszer kell betölteni, utána a kliens (azaz a böngésző) a felhasználói interakciók hatására lekérdezi az adatokat, majd abból dinamikusan megváltoztatja az oldalt. Azaz ezzel a technikával lehet elkerülni azt, hogy minden egyes lekérdezéskor a teljes oldal újra betöltődjön.

Annyi változott a kezdetektől ezzel kapcsolatban, hogy az adatformátum már tipikusan nem XML hanem JSON.

Lássunk erre is egy példát:

  • Backend
    • GET /gyumolcs: visszatér a gyümölcslistával, JSON formában.
    • POST /gyumolcs: megadjuk a fajtát és a darabszámot JSON formában, majd feldolgozza, és visszatér a gyümölcslistával. (Most az egyszerűség érdekében működik így. Normál esetben a visszatérési érték inkább a művelet sikerességét jelentené, és a kliens feladata lenne az adatfrissítés.)
  • Frontend
    • Egy külön HTML oldal tartalmazza az aktuális gyümölcslistát, valamint egy kétmezős formanyomtatványt.
    • A gyümölcslistát JavaScript generálja.
    • Betöltéskor lekérdezzük a gyümölcslistát.
    • Új gyümölcs beírásakor elküldjük azt JSON formában, a válasz megérkezése után pedig újraépítjük a gyümölcslistát.


Ha így valósítanánk meg, akkor az alábbi hibába futnánk bele: Access to fetch at 'http://localhost:8080/gyumolcs' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. Ez azt jelenti, hogy a HTML és a backend nem ugyanazon a szerveren fut (mivel a backend a localhost, a HTML fájlt pedig közvetlenül nyitottuk meg, azaz ott nincs szerver), és az ún. cross-site scripting (XSS) támadás léphetne fel. Kétféleképpen tudjuk ezt a problémát kezelni:

  • Lehetővé tesszük azt, hogy a HTML oldalt szerveren keresztül nyissuk meg, azaz http://localhost:8080/gyumolcsok.html formában.
  • Letiltjuk az XSS ellenőrzést. Ez utóbbit nyilván csak oktatási célból célszerű megtennünk.

Az kód példa mindkét esetre ad példát.

A szerver kód az alábbi:

import http.server
import urllib.parse
import json
 
class WebSzerver(http.server.SimpleHTTPRequestHandler):
    gyümölcsök = {
        'alma': 3,
        'banán': 2,
        'narancs': 5,
    }
 
    def do_GET(self):
        elemzett_url = urllib.parse.urlparse(self.path)
        if elemzett_url.path == '/gyumolcsok.html':
            return http.server.SimpleHTTPRequestHandler.do_GET(self)
        elif elemzett_url.path == '/gyumolcs':
            self.válasz()
        else:
            self.send_response(500)
            self.end_headers()
 
    def do_POST(self):
        elemzett_url = urllib.parse.urlparse(self.path)
        if elemzett_url.path == '/gyumolcs':
            kérés = json.loads(self.rfile.read(int(self.headers['Content-Length'])).decode('utf-8'))
            fajta = kérés['fajta']
            darab = int(kérés['darab'])
            if fajta not in self.gyümölcsök:
                self.gyümölcsök[fajta] = 0
            self.gyümölcsök[fajta] += darab
            if self.gyümölcsök[fajta] == 0:
                self.gyümölcsök.pop(fajta)
            self.válasz()
        else:
            self.send_response(500)
            self.end_headers()
 
    def válasz(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/json; charset=utf-8')
        self.send_header('Access-Control-Allow-Origin', '*')
        self.end_headers()
        self.wfile.write(bytes(json.dumps(self.gyümölcsök, ensure_ascii=False), 'UTF-8'))
 
http.server.HTTPServer(('localhost', 8080), WebSzerver).serve_forever()

Próbáljuk ki az alábbi parancsok segítségével:

Ha megfelelően működik, akkor valósítsuk meg a HTML kódot, gyumolcsok.html néven. Fontos, hogy a gyökérbe kerüljön:

<html>
    <head>
        <meta charset="UTF-8">
        <script>
            let url = 'http://localhost:8080/gyumolcs';
 
            function gyumolcsoketLekerdez() {
                fetch(url)
                    .then(eredmeny => eredmeny.json())
                    .then(gyumolcsok => listatLetrehoz(gyumolcsok));
            }
 
            function elkuld() {
                const fajta = document.getElementById('fajta').value;
                const darab = parseInt(document.getElementById('darab').value);
                fetch(url, {
                    method: 'POST',
                    body: JSON.stringify({
                        'fajta': fajta,
                        'darab': darab,
                    })
                })
                    .then(eredmeny => eredmeny.json())
                    .then(gyumolcsok => listatLetrehoz(gyumolcsok));
            }
 
            function listatLetrehoz(gyumolcsok) {
                const gyumolcsokUL = document.createElement('ul');
                gyumolcsokUL.setAttribute('id', 'gyumolcsokUL');
                for (const gyumolcs of Object.keys(gyumolcsok)) {
                    const gyumolcsLI = document.createElement('li');
                    gyumolcsLI.innerHTML = gyumolcs + ': ' + gyumolcsok[gyumolcs];
                    gyumolcsokUL.appendChild(gyumolcsLI);
                }
                document.getElementById('gyumolcsok').replaceChild(gyumolcsokUL, document.getElementById('gyumolcsokUL'));
            }
 
            gyumolcsoketLekerdez();
        </script>
    </head>
    <body>
        Gyümölcsök:
        <div id="gyumolcsok"><div id="gyumolcsokUL"></div></div>
        Fajta: <input type="text" id="fajta"><br>
        Darab: <input type="text" id="darab"><br>
        <button onclick="elkuld()">Elküld</button>
    </body>
</html>

Most nem megyek bele a JavaScript részletekbe; a fetch() a lekérdezést hajtja végre, a listatLetrehoz() pedig legenerálja a listát.

Nyissuk meg a http://localhost:8080/gyumolcsok.html oldalt. Kb. az alábbit kell létnunk:

ajax.png


Érdemes megfigyelnünk, hogy a kommunikációban valóban csak az adatok mennek, a HTML oldal nem.

  • Nyissuk meg a böngészőben az ún. Developer Tools-t (tipikusan F12).
  • Kattintsunk a Network fülre.
  • Töltsük be újra az oldalt, és hajtsunk végre műveleteket.
  • Kattintsunk a Name oszlopban valamelyik elemre.
  • Nézzük meg a Headers, a Payload (ha van) és a Response füleket.

Az alábbiakat láthatjuk:

  • gyumolcsok.html: ez a fenti megvalósított HTML oldal.
  • Első gyumolcs lekérdezés. A Headers alapján ez egy GET lekérdezés. A Response alapján a következő adatok érkeztek: {"alma": 3, "banán": 2, "narancs": 5} . Tehát minimális volt az adatkommunikáció.
  • Van egy favicon.ico lekérdezés, amit nem talál, HTTP 500-zal tér vissza. Ez az az ikon, ami a böngészőben megjelenik a fülön, így valós alkalmazásnál érdemes beállítani.
  • A második gyumolcs hívás a Headers alapján egy POST hívás. A tartalma a Payload alapján {fajta: "alma", darab: 2} . Az eredmény a Response alapján {"alma": 5, "banán": 2, "narancs": 5} . Ha nem AJAX technológiát használnánk, akkor egy teljes HTML oldal letöltés lenne itt, teljes újrarajzolással.

Webes lekérdezés

A fentiekben a szerver oldalról volt szó; most nézzük meg, hogy hogyan funkcionál a Python kliensként. Ez is megoldható belső könyvtár segítségével, a http.client csomagot használva, de ez már kissé nehézkes, így látni fogjuk a gyakrabban használt requests külső könyvtárat is.

Natív Python

A fenti két végpontot az alábbi módon tudjuk lekérdezni natív Python kód segítségével:

import http.client
import json
 
kapcsolat = http.client.HTTPConnection('localhost', 8080)
 
kapcsolat.request('GET', '/gyumolcs')
válasz = kapcsolat.getresponse()
if válasz.status == 200:
    print(válasz.read().decode('utf-8'))
 
kapcsolat.request('POST', '/gyumolcs', json.dumps({'fajta': 'alma', 'darab': 2}), {'Content-type': 'application/json'})
válasz = kapcsolat.getresponse()
if válasz.status == 200:
    gyümölcsök = json.loads(válasz.read().decode('utf-8'))
    print(gyümölcsök['alma'])

Az első lekérdezés eredménye:

{"alma": 3, "banán": 2, "narancs": 5}

A másodiké:

5

Tehát végső soron le tudtuk kérdezni, és a JSON-t fel is tudtuk dolgozni. Ez a fenti módszer - összehasonlítva a legtöbb programozási nyelv hasonló műveletével - kifejezetten egyszerű, ám Python mércével mérve kissé nehézkes. Meglepően sok időmbe telt, mire sikerült utánajárnom mindennek. Még mindig tartalmaz néhány feleslegesnek tűnő kódot. Lássunk egy egyszerűbb megoldást!

A requests csomag

Telepítsük fel a requests csomagot a szokásos

pip install requests

parancs segítségével. A fentivel ekvivalens program ennek segítségével az alábbi:

import requests
 
válasz = requests.get('http://localhost:8080/gyumolcs')
if válasz.status_code == 200:
    print(válasz.text)
 
válasz = requests.post('http://localhost:8080/gyumolcs', json = {'fajta': 'alma', 'darab': 2})
if válasz.status_code == 200:
    print(válasz.json()['alma'])

Azt gondolom, hogy egy jóval letisztultabb eredményt kaptunk, melyben lényegében nincsenek felesleges dolgok.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License