Uno degli strumenti che utilizzo per creare pipeline di dati (ad esempio processi E.T.L., ovvero i processi che acquisiscono i dati da una fonte, li lavorano e li storicizzano) è python.
Python ha una ricca collezione di librerie particolarmente utili per la manipolazione dei dati, ed è un linguaggio che si presta bene allo sviluppo sia di grandi e complicati sistemi che di prototipi.
Ma perchè dovrebbe interessarti? Perchè in alcuni casi può essere necessario dare evidenza al business di quanto una soluzione sarebbe utile per l’azienda. In altri casi si vogliono vedere in breve tempo i primi risultati seppur grezzi. O ancora si vuole testare la bontà della strada intrapresa per chiarirsi le idee.
Se ti sei trovato in una di queste situazioni, l’esperienza che ho avuto di recente potrà sicuramente esserti utile.
Il caso d’uso
Un mio cliente ha sviluppato una applicazione mobile per navigare un catalogo online e rendere disponibili informazioni tecniche e di installazione in modo interattivo.
Dopo la prima release il cliente voleva dare risposta ad alcune domande sull’utilizzo dell’applicazione.
Queste erano necessarie per decidere come avrebbe sviluppato ulteriormente la sua applicazione mobile che era il centro di una strategia commerciale.
Ma voleva farsi guidare da informazioni concrete e non da congetture.
Ad esempio: quali articoli gli utenti ricercano più spesso? Eseguono delle ricerche oppure navigano direttamente sul codice articolo? Fanno spesso riferimento alle istruzioni di montaggio? Utilizzano il QR code sul packaging del prodotto per arrivare direttamente al dettaglio dell’articolo?
La necessità iniziale era però quella di avere qualcosa di funzionante da presentare al business riducendo l’investimento iniziale.
Questo perché l’azienda aveva intrapreso da poco tempo un processo di rivoluzione tecnologica e c’era da abbattere lo scetticismo iniziale di chi non vedeva vantaggi nell’utilizzare metodi ad esempio di business intelligence e analisi dei dati.
Così l’idea fu quella di realizzare un prototipo che analizzasse il log del server web usato dall’applicazione mobile per ottenere i dati richiesti.
L’approccio seguito: realizzare un prototipo
Andando sul tecnico, inizialmente lo script realizzato legge il file di log e lo trasforma in un oggetto simile ad una tabella (detto “dataframe”), tramite l’utilizzo della libreria di analisi chiamata “pandas”.
Una volta ottenuto questa “tabella”, è possibile aggiungere altre colonne e calcolarne il contenuto con delle funzioni personalizzate eseguite per ogni riga.
Ad esempio ho utilizzato questo approccio per risolvere tre problemi importanti: convertire la data da formato apache in un tipo comprensibile dal database, estrarre la nazione di connessione dell’utente a partire dall’indirizzo ip, ed ottenere alcuni dati necessari dall’url della web api esposta dal server web.
Per ottenere questi dati ho utilizzato altre librerie come “urllib” (per analizzare l’indirizzo della web api richiamato) e “geoip2” per ottenere la nazione da cui si connetteva l’utente.
Infine il file originale viene copiato in un’altra cartella per mantenere uno storico, e tutti i dati vengono inseriti in una tabella di stage su un database PostgreSQL dove saranno ulteriormente analizzati.
Di seguito ti riporto il codice che ho creato. Nella prima parte ho raccolto il flusso generale dei dati. Nella seconda, le funzioni specifiche usate per analizzare e convertire i dati del log.
# file log_analyzer.py import psycopg2 import settings import pandas as pd import os import shutil import datetime import uuid from nginx_parser import parse_apache_time, get_codice_articolo, get_ricerca_articolo, get_country_by_ip, get_token from sqlalchemy import create_engine class LogAnalyzer: def analyze_log(self, log_folder, log_file): start_time = datetime.datetime.now() is_success = True message = None batch_id = uuid.uuid4() try: log_file_path = os.path.join(log_folder, log_file) if os.path.exists(log_file_path): today = datetime.datetime.now().strftime("%Y%m%d") new_log_file_name = settings.log_file.replace(".log.1", "") + "-" + today + ".log" new_log_file_path = os.path.join(settings.log_backup_folder, new_log_file_name) shutil.copy(log_file_path, new_log_file_path) log_file = open(new_log_file_path) if log_file: result_df = self.generate_base_data_frame(log_file) result_df['parsed_time'] = result_df['time'].apply(parse_apache_time) result_df['articolo_dettaglio'] = result_df['request'].apply(get_codice_articolo) result_df['articolo_ricerca'] = result_df['request'].apply(get_ricerca_articolo) result_df['token'] = result_df['request'].apply(get_token) result_df['country'] = result_df['ip'].apply(get_country_by_ip) result_df['batch_id'] = batch_id result_df['file_name'] = new_log_file_name print(result_df) engine = create_engine(settings.db_connection_string) result_df.to_sql('stage_nginx_log', engine, if_exists='append') else: message = "file not found: " + new_log_file_path print(message) is_success = False else: message = "file not found: " + log_file_path print(message) is_success = False except Exception as e: message = str(e) print(message) is_success = False finally: end_time = datetime.datetime.now() sql_cmd = "insert into public.application_log(batch_type, batch_id, start_time, end_time, is_success, message) values (%s, %s, %s, %s, %s, %s)" connection = psycopg2.connect( host=settings.db_hostname, user=settings.db_user, password=settings.db_password, dbname=settings.db_dbname) cur = connection.cursor() cur.execute(sql_cmd, (settings.batch_type, str(batch_id), start_time, end_time, is_success, message)) connection.commit() cur.close() connection.close() @staticmethod def generate_base_data_frame(file_path): df = pd.read_csv(file_path, sep=r'\s(?=(?:[^"]*"[^"]*")*[^"]*$)(?![^\[]*\])', engine='python', usecols=[0, 3, 4, 5, 6, 7, 8], names=['ip', 'time', 'request', 'status', 'size', 'referer', 'user_agent'], na_values='-', header=None ) startswith = '"GET /api/v1/articolo' newdf = df[(df['request'].str.startswith(startswith))] print(newdf) return newdf
# file nginx_parser.py from datetime import datetime from pytz import timezone import time import pytz import re import urllib.parse as urlparse import geoip2.database import settings def parse_apache_time(apache_time): utc = pytz.utc pacific = timezone("Europe/Rome") datestr_raw = apache_time datestr = datestr_raw[1:][:-1] datestr = re.sub("\s+", " ", datestr) dt = time.strptime(datestr[:-6], "%d/%b/%Y:%H:%M:%S") utc_dt = datetime(dt[0], dt[1], dt[2], dt[3], dt[4], dt[5], tzinfo=utc) loc_dt = utc_dt.astimezone(pacific) return loc_dt def get_codice_articolo(request): if not request.startswith('"GET /api/v1/articolo/') \ or request.startswith('"GET /api/v1/articolo/search/'): return None start = request.index('/articolo/') end = request.index('?') result = request[start:end].replace('/articolo/', '') return result def get_ricerca_articolo(request): query = None if not request.startswith('"GET /api/v1/articolo/search/'): return query reuqest_chunks = request.split(' ') request_url = reuqest_chunks[1] parsed_url = urlparse.urlparse(request_url) parsed_query = urlparse.parse_qs(parsed_url.query) if 'q' in parsed_query: query = parsed_query['q'][0] return query def get_token(request): token = None reuqest_chunks = request.split(' ') request_url = reuqest_chunks[1] parsed_url = urlparse.urlparse(request_url) parsed_query = urlparse.parse_qs(parsed_url.query) if 'token' in parsed_query: token = parsed_query['token'][0] return token def get_country_by_ip(ip): reader = None if reader is None: reader = geoip2.database.Reader(settings.geo_lite_db_path) response = reader.city(ip) iso_code = response.country.iso_code return iso_code
Perchè un approccio simile può essere utile per te e la tua azienda?
Nel corso del tempo mi sono ritrovato in situazioni dove alcuni colleghi, armati delle più buone intenzioni, avevano realizzato fin dall’inizio strati di software molto pesanti e ingegnerizzati.
Ma solo alla fine si erano accorti che le necessità del cliente non corrispondevano a quanto realizzato.
Per quella che è la mia esperienza, invece, in alcuni casi è più utile per il cliente realizzare un prototipo funzionante e passare all’ingegnerizzazione in un secondo momento.
Infatti come puoi vedere dagli script riportati sopra, sono servite poche righe di codice per avere un primo risultato funzionante, insomma una sorta di “minimum viable product”.
Questo non significa fare un cattivo lavoro, ma bilanciare inizialmente l’effort di sviluppo con quelle che sono le necessità e le aspettative del cliente.
Mi accade spesso di utilizzare questo metodo nelle fasi iniziali di un progetto, dove il cliente vuole avere una percezione del valore aggiunto di quanto si realizzerà e lo vuole mostrare al business (che non sempre ha un background tecnico).
Potrebbe essere il tuo caso?
Sono sicuro che un metodo simile potrebbe darti la possibilità di controllare al meglio lo sviluppo dei tuoi sistemi e gestire al meglio le risorse a disposizione, in particolare negli stadi iniziali di un progetto.
Però come puoi immaginare, è necessario affidarsi a persone specializzate e competenti che sappiano equilibrare al meglio i tanti fattori coinvolti (effort, livello di complessità, ecc..) e che sappiano essere davvero ricettivi verso le tue necessità e le tue aspettative.
Nel mio percorso ho raccolto molte informazioni interessanti in merito agli argomenti di cui ti ho parlato in questo articolo, ed ho scritto un libro “Why Your Data Matter”.
Essendo il frutto della mia passione ed esperienza diretta, ho scelto di mettere questo libro gratuitamente a disposizione di tutti gli IT Manager ed i CIO delle aziende che come te vogliono ottenere grandi risultati dalle loro scelte e dal loro lavoro (evitando di trovarsi in situazioni scomode e da risolvere con urgenza).
Ti invito a leggere le prime pagine scaricandole!
Se poi ti piacerà sarò felice di inviartene una copia GRATUITA direttamente nel tuo ufficio.
Clicca qui per scaricare l’estratto del mio libro (se ti piacerà te lo invierò in formato cartaceo!) ==> https://www.danieleperugini.it/il-mio-libro/