Kapcsolódás külső rendszerekhez Pythonban

Kategória: Python.

Fájlkezelés

Fájlt megnyitni az f = open(filename, mode) hívással tudunk Első paraméterként a fájlnevet kell megadnunk, az opcionális második paraméterrel pedig azt, hogy mit szeretnénk vele kezdeni: 'r' - olvasás (alapértelmezett), 'a' - hozzáfűzés, 'w' - írás, 'x' - létrehozás. Olvasni az s = f.read(), míg írni a f.write(s) függvénnyel tudunk. Lezárni fájlt az f.close() hívással lehet.

Fájl létrehozása:

fw = open('fruits.txt', 'w')
fw.write('apple orange banana')
fw.close()

Fájl olvasása:

fr = open('fruits.txt', 'r')
fruits = fr.read()
fr.close()
print(fruits) # apple orange banana

Az előzővel azonos működést eredményez a következő szintaxis:

with open('fruits.txt', 'r') as fr:
    fruits = fr.read()
    print(fruits) # apple orange banana

Itt tehát nem kell lezárni a streamet, az automatikusan megtörténik.

Egy sort a readline() függvénnyel tudunk beolvasni.

Azt gondolom, hogy elmondhatjuk erről azt, hogy egyszerű. A könnyűt semmiképpen sem használom szívesen, de azt, hogy egyszerű, szerintem teljesen indokolt jelen esetben. Érdemes összehasonlítani pl. a Java Fájlkezeléssel, hogy lássuk, valóban helytálló az "egyszerű" szó.

CSV fájlok kezelése

A CSV fájl felfogható speciális felépítésű szövegfájlként, a Python viszont külön könyvtárral segít ennek kezelésében. CSV fájl létrehozása:

import csv
 
with open('test.csv', 'w', newline='') as my_csv_file:
    my_writer = csv.writer(my_csv_file)
    my_writer.writerow(['apple', 3])
    my_writer.writerow(['banana', 2])
    my_writer.writerow(['orange', 5])

CSV fájl olvasása:

import csv
 
with open('test.csv', newline='') as my_csv_file:
    my_reader = csv.reader(my_csv_file)
    for row in my_reader:
        print(row)

A reader() és writer() függvényeknél számos paraméter beállítható, mindenekelőtt az elválasztó karakter (delimiter; alapértelmezésben a vessző, innen is ered a CSV elnevezés mint comma-separated file, aza vesszővel elválasztott fájl, de mivel sok esetben előfordul magában a tartalomban a vessző, többnyire meg szoktuk változtatni), az idézőjel (quotechar, "), az escape karakter (escape) stb., melyekről a specifikációban (https://docs.python.org/3/library/csv.html) egy kiváló összefoglalót találunk.

Szerializáció

A szerializáció önmagában nem jelent kapcsolatot semmilyen külső rendszerrel, ám szorosan együtt jár azzal. A probléma a következő: egy osztályt szeretnénk valami módon elmenteni, majd visszaállítani. A problémát a következő okozza:

  • Az osztályban lehetnek eltérő típusú elemek. Pl. egyszerre kellene kódolni egész számot és szöveget is, megadva azt, hogy a memóriában melyik adat mettől meddig terjed.
  • A hivatkozások más osztályokra több problémát is felvet. Egyrészt az önmagában megnehezíti a feladatot, ha nemcsak primitív típusok vannak, ugyanakkor a körkörös hivatkozás problémáját is meg kell oldani.
  • Külön problémát jelentenek az olyan struktúrák lementése, mint pl. a lista, a halmaz vagy a szótár.
  • Vannak olyan változók, amelyeket nem lehet (vagy legalábbis nem érdemes) szerializálni. Ilyen pl. egy hivatkozás adatbázis kapcsolatra.
  • És lehetnek olyan adatok is, amit nem szeretnénk szerializálni, pl. jelszavak.

A feladat tehát jóval összetettebb, mint amilyennek elsőre tűnik, és tipikusan erre kész megoldásokat használunk. A szerializáció és deszerializáció műveletét egyes progamozási nyelvekben marshallingnak és unmarshallingnak hívjuk. A Pythonban a pickle csomag hajtja végre ezt a műveletet.

Először lássuk a fájlba írásos módszert!

import pickle
 
my_dict = {'fruit': 'apple', 'color': 'red', 'number': 5}
pickle.dump(my_dict, open('my_dict.p', 'wb'))

Ennek az eredménye egy my_dict.p fájl, melynek a tartalma bináris. Visszafejtése:

import pickle
 
my_dict = pickle.load(open('my_dict.p', 'rb'))
print(my_dict)
# {'fruit': 'apple', 'color': 'red', 'number': 5}

A fenti fájl nemcsak alfanumerikus karaktereket tartalmaz, viszont a speciális karakterek számos helyen problémát okozhatnak. ÍCsak alfanumerikus karaktereket tartalmazó stringget a következőképpen használhatunk:

import pickle
import codecs
 
my_dict = {'fruit': 'apple', 'color': 'red', 'number': 5}
serialized = codecs.encode(pickle.dumps(my_dict), "base64").decode()
print(serialized)
# gASVLgAAAAAAAAB9lCiMBWZydWl0lIwFYXBwbGWUjAVjb2xvcpSMA3JlZJSMBm51bWJlcpRLBXUu
 
deserialized = pickle.loads(codecs.decode(serialized.encode(), "base64"))
print(deserialized)
# {'fruit': 'apple', 'color': 'red', 'number': 5}

Ez utóbbit elmenthetjük adatbázisban, vagy akár hálózaton keresztül is átküldhetjük.

Adatbázis elérés

Az adatbázis kapcsolat létrehozásához szükség van az adott adatbázisra jellemző ún. connector komponensre. Pl. MySQL adatbázis esetén a következőt kell telepíteni:

pip install mysql-connector-python

Tegyük fel, hogy a saját gépen fut egy MySQL szerver, csaba azonosítóval és farago jelszóval (innen letölthető: https://dev.mysql.com/downloads/installer/). Az alábbi példában láthatjuk a legfontosabb műveleteket: kapcsolódás a szerverhez, adatbázis törlése és létrehozása, kapcsolódás az adatbázishoz, tábla törlése és létrehozása, beszúrás, lekérdezés.

import mysql.connector
 
myconnection = mysql.connector.connect(
  host='localhost',
  user='csaba',
  password='farago'
)
mycursor = myconnection.cursor()
mycursor.execute('DROP DATABASE IF EXISTS mydatabase')
mycursor.execute('CREATE DATABASE mydatabase')
 
mydb = mysql.connector.connect(
  host='localhost',
  user='csaba',
  password='farago',
  database='mydatabase'
)
 
mycursor = mydb.cursor()
mycursor.execute('DROP TABLE IF EXISTS persons')
mycursor.execute('CREATE TABLE persons(id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)')
 
insert_person = 'INSERT INTO persons(name, age) VALUES (%s, %s)'
pista = ('Pista', 28)
joska = ('Joska', 54)
mycursor.execute(insert_person, pista)
mycursor.execute(insert_person, joska)
 
mycursor.execute('SELECT * FROM persons')
myrecords = mycursor.fetchall()
for myrecord in myrecords:
    print(myrecord)

Nagy mennyiségű adatformátum

A HDF5 nagy mennyiségű adatok bináris tárolására szolgáló adatformátum. Ez a Pythontól független szabvány. Vannak benne csoportok (group) és adathalmazok (dataset). A csoportok rekurzívan egymásba ágyazhatóak, amelyek tehát más csoportokat és adathalmazokat tartalmazhatnak. A gyökér is tartalmazhat adathalmazt. A csoport és az adathalmaz is egy kulcs-érték pár: a kulcs az adott elem neve, az érték pedig csoport esetén maga a csoport, adathalmaz esetén pedig maga az adat. Ez utóbbi általában valamiféle tömb.

Pythonban a h5py könyvtár segítségével tudunk HDF5 fájlokat kezelni. Telepítése a szokásos:

pip install h5py

Az alábbi példa létrehoz egy olyan HDF5 fájlt, ami a gyökérben tartalmaz két adathalmazt, van benne egy csoport, és azon belül is van két adathalmaz:

import h5py
 
with h5py.File('mytestfile.hdf5', 'w') as my_hdf5_file:
    my_hdf5_file.create_dataset('my_numbers', data=(2, 3, 5))
    my_hdf5_file.create_dataset('my_fruits', data=('apple', 'banana', 'orange'))
    my_group = my_hdf5_file.create_group('my_group')
    my_group.create_dataset('my_bools', data=(True, False, False))
    my_group.create_dataset('my_floats', data=(3.14, 2.71, 10.0))

Az eredmény mytestfile.hdf5, ami egy bináris fájl. Beolvasása:

import h5py
import numpy as np
 
with h5py.File('mytestfile.hdf5', 'r') as my_hdf5_file:
    print(list(my_hdf5_file.keys())) # ['my_fruits', 'my_group', 'my_numbers']
    my_numbers = my_hdf5_file['my_numbers']
    print(my_numbers.shape) # (3,)
    print(my_numbers.dtype) # int32
    print(np.array(my_numbers)) # [2 3 5]
    print(np.array(my_hdf5_file['my_fruits'])) # [b'apple' b'banana' b'orange']
    my_group = my_hdf5_file['my_group']
    print(np.array(my_group['my_bools'])) # [ True False False]
    print(np.array(my_group['my_floats'])) # [ 3.14  2.71 10.  ]

Érdemes megfigyelni azt, hogy a NumPy segítségével lett megvalósítva az adathalmaz.

Távoli metódushívás

A távoli metódushívás azt jelenti, hogy van két külön rendszer, tipikusan két külön számítógépen, és az egyik az egyik meghívja egy másik függvényét.

Egyszerűbb az eset akkor, ha a szerver és a kliens is ugyanabban a programozási nyelvben íródott, esetünkben Pythonban. Az rpyc csomag segítségével tudunk távolról is hívható metódusokat létrehozni ill. meghívni. Mivel nem része az alaprendszernek, először telepíteni kell:

pip install rpyc

A szerver kód:

from rpyc import Service
from rpyc.utils.server import ThreadedServer
 
class MyService(Service):
    def exposed_sum(self, a: int, b: int) -> int:
        return a + b
 
my_server = ThreadedServer(MyService, port=9999)
my_server.start()

A kiajánlott, tehát távolról is hívható függvények elé az exposed_ előtagot kell tenni. Majd indítsuk el a programot szokásos módon.

A kliens kód:

import rpyc
 
connection = rpyc.connect('localhost', 9999)
result = connection.root.sum(3, 2)
connection.close()
print(result) # 5

Itt az exposed_ előtag nélkül kell a függvényt meghívni.

Összetettebb esetekben a programozási nyelvek eltérőek. Több szabványt is létrehoztak az idők folyamán, pl. CORBA vagy WSDL, és a jelentősebb programozási nyelvekre el is készültek a könyvtárak. Pl. a pyws segítségével volt lehetőség WSDL szervert létrehozni Pythonban. A múlt idő amiatt indokolt, mert ez régebbi Pythonhoz készült, az írás pillanatában viszont már szintaxis hibás, és nincs karbantartva. Az írás pillanatában ugyanis már egyre kevesebbet használnak tulajdonképpeni távoli metódushívást, a helyét átvette a REST alapú adatkapcsolat: a szerver általában egy webszerver, melyet HTTP hívásokon (GET, POST stb.) keresztül tudunk elérni. Az adatcsere formátuma többnyire JSON.

Webes lekérdezés

Webes lekérdezést többféle módon lehet programozni. Dicséretes, hogy belső könyvtárral is megoldható, pl. a httplib segítségével:

import http.client
 
connection = http.client.HTTPConnection("faragocsaba.hu")
connection.request("GET", "/")
response = connection.getresponse()
print(response.status)
print(response.read())

Ám ez kissé nehézkes. Elég sok weboldalt kellett megnyitnom és az ott leírtakat kipróbálnom, mire sikerült a fenti működő programot létrehoznom. Az eredményt ráadásul egyetlen hosszú sorban írja ki. Sokkal elegánsabb a lekérdezés a requests könyvtár segítségével. Telepítése a szokásos:

pip install h5py

Használata:

import requests
 
my_page = requests.get('http://faragocsaba.hu')
print(my_page.status_code)
print(my_page.text)

Ennek is számos egyéb funkciója van: lekérdező paraméteres, más metódusfajták (pl. POST), tanúsítványok kezelése stb. melyekre itt nem térünk ki.

Webszerver létrehozása

Web szervert a gyakorlatban ritkán hozunk létre keretrendszer nélkül. Az alábbi módon komolyabb produktív rendszert nem érdemes létrehozni, inkább csak kipróbálásra ajánlott.

A http.server és a urllib.parse csomagokat használhatjuk egy egyszerű webszerver elkészítéséhez, amelyek részei az alap Python rendszernek. Láttuk az alábbi példát!

from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs
 
class MyServer(BaseHTTPRequestHandler):
    def do_GET(self):
        parsed_url = urlparse(self.path)
        path = parsed_url.path
        query_params = parse_qs(parsed_url.query)
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.end_headers()
        self.wfile.write(bytes("<html><head><title>Python HTTP server</title></head>", "utf-8"))
        self.wfile.write(bytes("<body>", "utf-8"))
        if path == "/":
            self.wfile.write(bytes("<p>Python web server is running!</p>", "utf-8"))
        elif path == "/hello":
            if "name" in query_params:
                name = query_params["name"][0]
            else:
                name = "world"
            self.wfile.write(bytes("<p>Hello " + name  + "!</p>", "utf-8"))
        else:
            self.wfile.write(bytes("<p>Path: " + path + "</p>", "utf-8"))
        self.wfile.write(bytes("</body></html>", "utf-8"))
 
def start_server():
    webServer = HTTPServer(("localhost", 8080), MyServer)
    print("Server started at http://localhost:8080.")
    webServer.serve_forever()
 
start_server()

Magyarázat:

  • A web szervert a HTTPServer példányosításával hozunk létre, amit a serve_forever() hívással indítjuk el.
  • Az első paraméter a szerver név és a port.
  • A második paraméter az az osztály, ami kiszolgálja a kéréseket.
  • Ez utóbbi a BaseHTTPRequestHandler osztályból származik.
  • A példában a do_GET() metódust valósítottuk meg. Hasonlóan létezik do_POST() stb.
  • Egy osztálynak csak egy do_GET() metódusa lehet. Ha több elérési útvonalra is reagál, akkor azt ki kell nyerni, és attól függően kell megvalósítani a választ (ahogyan a példában láthatjuk is).
  • Az urlparse() és a parse_qs() függvényekkel nyerjük ki az URL-ből az adatokat; a példában magát az elérési utat és a lekérés paramétereit.
  • A self.wfile.write() hívásokkal hozzuk létre a visszaadandó HTML-t.

A példa használata:

Üzenetkezelés

Számos üzenetkezelő rendszer létezik (pl. ActiveMQ, HornetQ, RabbitMQ stb.), az alapelveik viszont azonosak: az üzenetek kézbestése. A két alapvető üzenetkezelő rendszer:

  • Cső (queue): a küldő által küldött üzenetek egy "csőbe" kerülnek, a fogadók pedig a legkorábban küldött üzenetet tudják feldolgozni. Egy üzenetet csak egyszer dolgoznak fel, így ha több kliens is figyeli a csövet, akkor csak az egyik fogja megkapni.
  • Téma (topic): akárhány kliens megkaphatja az üzenetet. Ebben az esetben az üzenetekre fel kell iratkozni, emiatt ezt a modellt publish-subscribe-nak is hívják.

Univerzális üzenetkezelő rendszer sajnos nem létezik; mindegyik programozási nyelvnek külön-külön kell támogatnia mindegyik rendszert. Itt most a RabbitMQ-t vizsgáljuk meg Python segítségével. A telepítése kínszenvedés, a hivatalos leírás pedig iskolapéldája annak, hogy hogyan nem lenne szabad ezt csinálni; tele van a leírás felesleges marhasággal. A telepítés lépései egyszerűen:

Érdemes mindkettőt rendszergazdai jogokkal telepíteni. Ez utóbbi engedélyeket kér, amit adjunk meg. Ellenőrzés: a Szolgáltatások (Services) programban nézzük meg, hogy a RabbitMQ fut-e.

Pythonban a pika könyvtár segítségével tudunk csatlakozni a RabbitMQ-hoz. Telepítése a szokásos:

pip install pika

Queue fogadó:

import pika
 
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='hello')
 
def callback(ch, method, properties, body):
    print(f'Received: {body}')
 
channel.basic_consume(queue='hello', on_message_callback=callback, auto_ack=True)
print('Waiting for messages.')
channel.start_consuming()

Queue-ba küldő:

import pika
 
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='hello')
message = 'Hello World!'
channel.basic_publish(exchange='', routing_key='hello', body=message)
print('Sent:', message)
connection.close()

Topic fogadó:

import pika
 
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='topic_logs', exchange_type='topic')
result = channel.queue_declare('', exclusive=True)
queue_name = result.method.queue
channel.queue_bind(exchange='topic_logs', queue=queue_name, routing_key='my_topic')
print('Waiting for logs.')
 
def callback(ch, method, properties, body):
    print(f'Received from {method.routing_key}: {body}')
 
channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True)
channel.start_consuming()

Topic-ba küldő:

import pika
 
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='topic_logs', exchange_type='topic')
routing_key = 'my_topic'
message = 'Hello World!'
channel.basic_publish(exchange='topic_logs', routing_key=routing_key, body=message)
print(f'Sent to {routing_key}: {message}')
connection.close()

Titkosítás

Ha külső rendszerekhez kapcsolódunk, akkor mindenképpen fontos a biztonságra is gondolnunk. Ez a terület is óriási, önmagában lehetne csak erről külön weboldalt létrehozni, így itt sem célunk a terület részletes bemutatása. A biztonság egyik fontos összetevője a titkosítás: az adat lehetőleg ne jusson avatatlan szemek elé. Pythonban számos könyvtár támogatja ezt a területet, az egyik közülük a cryptography. Nálam a kipróbálás pillanatában már fel volt telepítve (minden bizonnyal egy másik könyvtár függőségeként), de ha nem lenne, a szokásos módon telepíthetjük:

pip install cryptography

A titkosítás két fő művelete a bekódolás és a kikódolás. Mindkettőre példa:

from cryptography.fernet import Fernet
 
message = 'Hello, world!'
print(message) # Hello, world!
 
key = Fernet.generate_key()
print(key) # b'-ESLqM5fHp8ti7gPbES9P-CxCVvJN3q5oDgSHBImHGk='
fernet = Fernet(key)
encrypted = fernet.encrypt(message.encode())
print(encrypted) # b'gAAAAABg_TixnDYfVxrpXQQy_V9IXMAD19SRtqRQXbPHugSMbyfSJ8zo1nrdUXEyD_WbS_qT960mknE5cYHiaxHCtZ5g0Wf0aA=='
 
decrypted = fernet.decrypt(encrypted).decode()
print(decrypted) # Hello, world!

A valóságban a becsomagolás és a kicsomagolás többnyire fizikailag két külön helyen történik. A gyakorlatban a fentiek az alábbi részekre oszlanak:

  • Kód generálása: fernet = Fernet(key).
  • A kód megosztása a felek között.
  • A kód használata a kliens és a szerver oldalon is (fernet = Fernet(key)).

A kód megosztása és tárolása újabb kérdéseket vet fel, ami ezen a pontot már túlmutat ennek a szakasznak a keretein.

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