Analizzando le statistiche del sito, otteniamo un'idea di ciò che sta accadendo. Confrontiamo i risultati con altre conoscenze sul prodotto o servizio, migliorando così la nostra esperienza.
Una volta completata l'analisi dei primi risultati e riflettuta sull'informazione raccolta, inizia la fase successiva. Nascono idee: e se guardassimo i dati da un'altra prospettiva?
In questa fase ci sono limitazioni degli strumenti di analisi. Questa è una delle ragioni per cui Google Analytics non era sufficiente per me, a causa della sua capacità limitata di visualizzare e manipolare i propri dati.
Ho sempre desiderato caricare rapidamente i dati di base (master data), aggiungere un altro livello di aggregazione o interpretare diversamente i valori esistenti.
È facile farlo in basato sul file access.log, e per questo è sufficiente il linguaggio SQL.
Quindi, quali domande desidero rispondere?
Cosa e quando è stato modificato sul sito
La storia delle modifiche ai dati di base (master data) è sempre di interesse.

Richiesta SQL del report
SELECT
1 as 'SideStackedBar: Aggiornamenti dei contenuti per mese',
strftime('%m/%Y', datetime(UPDATE_DT, 'unixepoch')) AS 'Mese',
COUNT(CASE WHEN PAGE_TITLE != 'n.a.' THEN DIM_REQUEST_ID END) AS 'Aggiornamenti delle pagine web',
COUNT(CASE WHEN PAGE_DESCR = 'IMAGES' THEN DIM_REQUEST_ID END) AS 'Caricamenti di immagini',
COUNT(CASE WHEN PAGE_DESCR = 'VIDEO' THEN DIM_REQUEST_ID END) AS 'Caricamenti video',
COUNT(CASE WHEN PAGE_DESCR = 'AUDIO' THEN DIM_REQUEST_ID END) AS 'Caricamenti audio'
FROM DIM_REQUEST
WHERE PAGE_TITLE != 'n.a.' OR PAGE_DESCR != 'n.a.'
GROUP BY strftime('%m/%Y', datetime(UPDATE_DT, 'unixepoch'))
ORDER BY UPDATE_DTAd esempio, in un certo momento sono state effettuate ottimizzazioni SEO o è stato aggiunto nuovo contenuto al sito, e di conseguenza ci si aspetta un aumento del traffico.
Gruppi di utenti
Il più semplice esempio di gruppo può essere rappresentato da un agente utente o dal sistema operativo utilizzato.
La misurazione degli agenti utente ha accumulato circa mille registrazioni e mi interessava vedere la dinamica della distribuzione degli agenti all'interno del gruppo.

Richiesta SQL del report
SELECT
1 AS 'SideStackedBar: Agenti Utente',
AGENT_OS AS 'OS',
SUM(CASE WHEN AGENT_BOT = 'n.a.' THEN 1 ELSE 0 END ) AS 'Agente Utente di Utenti',
SUM(CASE WHEN AGENT_BOT != 'n.a.' THEN 1 ELSE 0 END ) AS 'Agente Utente di Bot'
FROM DIM_USER_AGENT
WHERE DIM_USER_AGENT_ID != -1
GROUP BY AGENT_OS
ORDER BY 3 DESCLa maggior parte delle varie combinazioni di agenti proviene dal mondo Windows. Tra quelle non definite ci sono anche nomi come WhatsApp, PocketImageCache, PlayStation, SmartTV e simili.
Attività dei gruppi utenti per settimana
Unendo alcuni gruppi, è possibile osservare la distribuzione della loro attività.
Ad esempio, gli utenti del cluster Linux consumano più traffico sul sito rispetto a tutti gli altri.

Richiesta SQL del report
SELECT
1 as 'StackedBar: Volume di traffico per sistema operativo utente e per settimana',
strftime('%W week', datetime(FCT.EVENT_DT, 'unixepoch')) AS 'Settimana',
SUM(CASE WHEN USG.AGENT_OS IN ('Android', 'Linux') THEN FCT.BYTES ELSE 0 END)/1000 AS 'Utenti Android/Linux',
SUM(CASE WHEN USG.AGENT_OS IN ('Windows') THEN FCT.BYTES ELSE 0 END)/1000 AS 'Utenti Windows',
SUM(CASE WHEN USG.AGENT_OS IN ('Macintosh', 'iOS') THEN FCT.BYTES ELSE 0 END)/1000 AS 'Utenti Mac/iOS',
SUM(CASE WHEN USG.AGENT_OS IN ('n.a.', 'BlackBerry') THEN FCT.BYTES ELSE 0 END)/1000 AS 'Altro'
FROM
FCT_ACCESS_USER_AGENT_DD FCT,
DIM_USER_AGENT USG,
DIM_HTTP_STATUS HST
WHERE FCT.DIM_USER_AGENT_ID=USG.DIM_USER_AGENT_ID
AND FCT.DIM_HTTP_STATUS_ID = HST.DIM_HTTP_STATUS_ID
AND USG.AGENT_BOT = 'n.a.' /* solo utenti */
AND HST.STATUS_GROUP IN ('Successful') /* pagine valide */
AND datetime(FCT.EVENT_DT, 'unixepoch') > date('now', '-3 month')
GROUP BY strftime('%W week', datetime(FCT.EVENT_DT, 'unixepoch'))
ORDER BY FCT.EVENT_DTConsumo intenso di traffico
Dalla tabella si possono vedere i gruppi di utenti più attivi e il giorno della loro attività.
I più attivi appartengono al cluster Linux.

Richiesta SQL del report
SELEZIONA
1 AS 'Tabella: Agente Utente con Elevato Utilizzo',
strftime('%d.%m.%Y', datetime(FCT.EVENT_DT, 'unixepoch')) AS 'Giorno',
ROUND(1.0*SUM(FCT.BYTES)/1000000, 1) AS 'Traffico MB',
ROUND(1.0*SUM(FCT.IP_CNT)/SUM(1), 1) AS 'IPs',
ROUND(1.0*SUM(FCT.REQUEST_CNT)/SUM(1), 1) AS 'Richieste',
USA.DIM_USER_AGENT_ID AS 'ID',
MAX(USA.USER_AGENT_NK) AS 'Agente Utente',
MAX(USA.AGENT_BOT) AS 'Bot'
DA
FCT_ACCESS_USER_AGENT_DD FCT,
DIM_USER_AGENT USA
DOVE FCT.DIM_USER_AGENT_ID = USA.DIM_USER_AGENT_ID
E datetime(FCT.EVENT_DT, 'unixepoch') >= date('now', '-30 day')
GRUPPA PER USA.DIM_USER_AGENT_ID, strftime('%d.%m.%Y', datetime(FCT.EVENT_DT, 'unixepoch'))
ORDINA PER SUM(FCT.BYTES) DESC, FCT.EVENT_DT
LIMIT 10Utilizzando gli attributi giorno e ID dell'agente, è possibile trovare rapidamente e monitorare le statistiche sui giorni di gruppi specifici di utenti. Se necessario, è possibile trovare rapidamente informazioni dettagliate nella tabella di staging.
Come ottenere informazioni?
possono diventare ancora più efficaci se integrati ulteriori fonti di dati, introducendo nuovi livelli di aggregazione e raggruppamento.
Dati e entità di base
I dati di base comprendono informazioni su entità: pagine web, immagini, contenuti video e audio, e nel caso di un negozio — prodotti.
Le entità stesse fungono da misurazioni e il processo di salvataggio delle modifiche degli attributi è chiamato storicizzazione. In un database, questo processo è spesso implementato sotto forma di misurazioni a cambiamento lento (SCD).
Le fonti dei dati di base possono provenire da vari sistemi, quindi quasi sempre è necessario integrarli.
Misurazione a cambiamento lento
La misurazione DIM_REQUEST conterrà informazioni sulle richieste al sito in forma storica.
Tabella SCD2
CREATE TABLE DIM_REQUEST ( /* tabella scd per le richieste degli utenti */
DIM_REQUEST_ID INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
DIM_REQUEST_ID_HIST INTEGER NOT NULL DEFAULT -1,
REQUEST_NK TEXT NOT NULL DEFAULT 'n.a.', /* richiesta senza ?parametri */
PAGE_TITLE TEXT NOT NULL DEFAULT 'n.a.',
PAGE_DESCR TEXT NOT NULL DEFAULT 'n.a.',
PAGE_KEYWORDS TEXT NOT NULL DEFAULT 'n.a.',
DELETE_FLAG INTEGER NOT NULL DEFAULT 0,
UPDATE_DT INTEGER NOT NULL DEFAULT 0,
UNIQUE (REQUEST_NK, DIM_REQUEST_ID_HIST)
);
INSERT INTO DIM_REQUEST (DIM_REQUEST_ID) VALUES (-1);In aggiunta, creeremo una vista che mostri sempre tutte le registrazioni nell'ultimo stato. Necessaria per il caricamento della misurazione stessa.

Vista attuale SCD2
/* Content: actual view on scd table */
SELECT HI.DIM_REQUEST_ID,
HI.DIM_REQUEST_ID_HIST,
HI.REQUEST_NK,
HI.PAGE_TITLE,
HI.PAGE_DESCR,
HI.PAGE_KEYWORDS,
NK.CNT AS HIST_CNT,
HI.DELETE_FLAG,
strftime('%d.%m.%Y %H:%M', datetime(HI.UPDATE_DT, 'unixepoch')) AS UPDATE_DT
FROM
( SELECT REQUEST_NK, MAX(DIM_REQUEST_ID) AS DIM_REQUEST_ID, SUM(1) AS CNT
FROM DIM_REQUEST
GROUP BY REQUEST_NK
) NK,
DIM_REQUEST HI
WHERE 1 = 1
AND NK.REQUEST_NK = HI.REQUEST_NK
AND NK.DIM_REQUEST_ID = HI.DIM_REQUEST_ID;E una vista in cui per ogni registrazione è raccolta l'informazione storica. Necessaria per costruire un collegamento storicamente corretto con i fatti.

Rappresentazione storica SCD2
/* Content: actual view on scd table */
SELECT SCD.DIM_REQUEST_ID,
SCD.DIM_REQUEST_ID_HIST,
SCD.REQUEST_NK,
SCD.PAGE_TITLE,
SCD.PAGE_DESCR,
SCD.PAGE_KEYWORDS,
SCD.DELETE_FLAG,
CASE
WHEN HIS.UPDATE_DT IS NULL
THEN 1
ELSE 0 END ACTIVE_FLAG,
SCD.DIM_REQUEST_ID_HIST AS ID_FROM,
SCD.DIM_REQUEST_ID AS ID_TO,
CASE
WHEN SCD.DIM_REQUEST_ID_HIST=-1
THEN 3600
ELSE IFNULL(SCD.UPDATE_DT,3600)
END AS TIME_FROM,
CASE
WHEN HIS.UPDATE_DT IS NULL
THEN 253370764800
ELSE HIS.UPDATE_DT
END AS TIME_TO,
CASE
WHEN SCD.DIM_REQUEST_ID_HIST=-1
THEN STRFTIME('%d.%m.%Y %H:%M', DATETIME(3600, 'unixepoch'))
ELSE STRFTIME('%d.%m.%Y %H:%M', DATETIME(IFNULL(SCD.UPDATE_DT,3600), 'unixepoch'))
END AS ACTIVE_FROM,
CASE
WHEN HIS.UPDATE_DT IS NULL
THEN STRFTIME('%d.%m.%Y %H:%M', DATETIME(253370764800, 'unixepoch'))
ELSE STRFTIME('%d.%m.%Y %H:%M', DATETIME(HIS.UPDATE_DT, 'unixepoch'))
END AS ACTIVE_TO
FROM
DIM_REQUEST SCD
LEFT OUTER JOIN DIM_REQUEST HIS
ON SCD.REQUEST_NK = HIS.REQUEST_NK AND SCD.DIM_REQUEST_ID = HIS.DIM_REQUEST_ID_HIST;Aggregazione dei dati
La compressione (aggregazione) consente di valutare i dati a un livello superiore e di rilevare anomalie e tendenze che non sono visibili nei rapporti dettagliati.
Ad esempio, nell'insieme di misurazioni con i codici di stato delle richieste DIM_HTTP_STATUS aggiungeremo un gruppo:
STATUS / GRUPPO
0xx / n.a.
1xx / Informativo
2xx / Riuscito
3xx / Reindirizzamento
4xx / Errore del Client
5xx / Errore del Server
L'insieme di misurazioni degli agenti utente DIM_USER_AGENT conterrà gli attributi AGENT_OS e AGENT_BOT, responsabili dei gruppi. Questi possono essere popolati durante il processo ETL:
Caricamento DIM_USER_AGENT
/* Propagate the user agent from access log */
INSERT INTO DIM_USER_AGENT (USER_AGENT_NK, AGENT_OS, AGENT_ENGINE, AGENT_DEVICE, AGENT_BOT, UPDATE_DT)
WITH CLS AS (
SELECT BROWSER
FROM STG_ACCESS_LOG WHERE LENGTH(BROWSER)>1
GROUP BY BROWSER
)
SELECT
CLS.BROWSER AS USER_AGENT_NK,
CASE
WHEN INSTR(CLS.BROWSER,'Macintosh')>0
THEN 'Macintosh'
WHEN INSTR(CLS.BROWSER,'iPhone')>0
OR INSTR(CLS.BROWSER,'iPad')>0
OR INSTR(CLS.BROWSER,'iPod')>0
OR INSTR(CLS.BROWSER,'Apple TV')>0
OR INSTR(CLS.BROWSER,'Darwin')>0
THEN 'iOS'
WHEN INSTR(CLS.BROWSER,'Android')>0
THEN 'Android'
WHEN INSTR(CLS.BROWSER,'X11;')>0 OR INSTR(CLS.BROWSER,'Wayland;')>0 OR INSTR(CLS.BROWSER,'linux-gnu')>0
THEN 'Linux'
WHEN INSTR(CLS.BROWSER,'BB10;')>0 OR INSTR(CLS.BROWSER,'BlackBerry')>0
THEN 'BlackBerry'
WHEN INSTR(CLS.BROWSER,'Windows')>0
THEN 'Windows'
ELSE 'n.a.' END AS AGENT_OS, -- OS
CASE
WHEN INSTR(CLS.BROWSER,'AppleCoreMedia')>0
THEN 'AppleWebKit'
WHEN INSTR(CLS.BROWSER,') ')>1 AND LENGTH(CLS.BROWSER)>INSTR(CLS.BROWSER,') ')
THEN COALESCE(SUBSTR(CLS.BROWSER, INSTR(CLS.BROWSER,') ')+2, LENGTH(CLS.BROWSER) - INSTR(CLS.BROWSER,') ')-1), 'N/A')
ELSE 'n.a.' END AS AGENT_ENGINE, -- Engine
CASE
WHEN INSTR(CLS.BROWSER,'iPhone')>0
THEN 'iPhone'
WHEN INSTR(CLS.BROWSER,'iPad')>0
THEN 'iPad'
WHEN INSTR(CLS.BROWSER,'iPod')>0
THEN 'iPod'
WHEN INSTR(CLS.BROWSER,'Apple TV')>0
THEN 'Apple TV'
WHEN INSTR(CLS.BROWSER,'Android ')>0 AND INSTR(CLS.BROWSER,'Build')>0
THEN COALESCE(SUBSTR(CLS.BROWSER, INSTR(CLS.BROWSER,'Android '), INSTR(CLS.BROWSER,'Build')-INSTR(CLS.BROWSER,'Android ')), 'n.a.')
WHEN INSTR(CLS.BROWSER,'Android ')>0 AND INSTR(CLS.BROWSER,'MIUI')>0
THEN COALESCE(SUBSTR(CLS.BROWSER, INSTR(CLS.BROWSER,'Android '), INSTR(CLS.BROWSER,'MIUI')-INSTR(CLS.BROWSER,'Android ')), 'n.a.')
ELSE 'n.a.' END AS AGENT_DEVICE, -- Device
CASE
WHEN INSTR(LOWER(CLS.BROWSER),'yandex.com')>0
THEN 'yandex'
WHEN INSTR(LOWER(CLS.BROWSER),'googlebot')>0
THEN 'google'
WHEN INSTR(LOWER(CLS.BROWSER),'bingbot')>0
THEN 'microsoft'
WHEN INSTR(LOWER(CLS.BROWSER),'ahrefsbot')>0
THEN 'ahrefs'
WHEN INSTR(LOWER(CLS.BROWSER),'jobboersebot')>0 OR INSTR(LOWER(CLS.BROWSER),'jobkicks')>0
THEN 'job.de'
WHEN INSTR(LOWER(CLS.BROWSER),'mail.ru')>0
THEN 'mail.ru'
WHEN INSTR(LOWER(CLS.BROWSER),'baiduspider')>0
THEN 'baidu'
WHEN INSTR(LOWER(CLS.BROWSER),'mj12bot')>0
THEN 'majestic-12'
WHEN INSTR(LOWER(CLS.BROWSER),'duckduckgo')>0
THEN 'duckduckgo'
WHEN INSTR(LOWER(CLS.BROWSER),'bytespider')>0
THEN 'bytespider'
WHEN INSTR(LOWER(CLS.BROWSER),'360spider')>0
THEN 'so.360.cn'
WHEN INSTR(LOWER(CLS.BROWSER),'compatible')>0 OR INSTR(LOWER(CLS.BROWSER),'http')>0
OR INSTR(LOWER(CLS.BROWSER),'libwww')>0 OR INSTR(LOWER(CLS.BROWSER),'spider')>0
OR INSTR(LOWER(CLS.BROWSER),'java')>0 OR INSTR(LOWER(CLS.BROWSER),'python')>0
OR INSTR(LOWER(CLS.BROWSER),'robot')>0 OR INSTR(LOWER(CLS.BROWSER),'curl')>0 OR INSTR(LOWER(CLS.BROWSER),'wget')>0
THEN 'other'
ELSE 'n.a.' END AS AGENT_BOT, -- Bot
STRFTIME('%s','now') AS UPDATE_DT
FROM CLS
LEFT OUTER JOIN DIM_USER_AGENT TRG
ON CLS.BROWSER = TRG.USER_AGENT_NK
WHERE TRG.DIM_USER_AGENT_ID IS NULLIntegrazione dei dati
Include l'organizzazione del trasferimento dati dal sistema operativo al rapporto. A tal fine, è necessario creare una tabella di staging con una struttura simile a quella della sorgente.
Nella staging, le informazioni sulle pagine web vengono trasferite dal backup del CMS sotto forma di richieste di inserimento.
Il caricamento della tabella storica DIM_REQUEST con i dati di base avviene in tre fasi: caricamento di nuove chiavi e attributi, aggiornamento di quelli esistenti e registrazione delle righe eliminate.
Caricamento di nuove righe SCD2
/* Load request table SCD from master data */
INSERT INTO DIM_REQUEST (DIM_REQUEST_ID_HIST, REQUEST_NK, PAGE_TITLE, PAGE_DESCR, PAGE_KEYWORDS, DELETE_FLAG, UPDATE_DT)
WITH CLS AS ( -- prepare keys
SELECT
'/' || NAME AS REQUEST_NK,
TITLE AS PAGE_TITLE,
CASE WHEN DESCRIPTION = '' OR DESCRIPTION IS NULL
THEN 'n.a.' ELSE DESCRIPTION
END AS PAGE_DESCR,
CASE WHEN KEYWORDS = '' OR KEYWORDS IS NULL
THEN 'n.a.' ELSE KEYWORDS
END AS PAGE_KEYWORDS
FROM STG_CMS_MENU
WHERE CONTENT_TYPE != 'folder' -- only web pages
AND PAGE_TITLE != 'n.a.' -- master data which make sense
)
/* new records from stage: CLS */
SELECT
-1 AS DIM_REQUEST_ID_HIST,
CLS.REQUEST_NK,
CLS.PAGE_TITLE,
CLS.PAGE_DESCR,
CLS.PAGE_KEYWORDS,
0 AS DELETE_FLAG,
STRFTIME('%s','now') AS UPDATE_DT
FROM CLS
LEFT OUTER JOIN
(
SELECT
DIM_REQUEST_ID,
REQUEST_NK,
PAGE_TITLE,
PAGE_DESCR,
PAGE_KEYWORDS
FROM DIM_REQUEST_V_ACT
) TRG ON CLS.REQUEST_NK = TRG.REQUEST_NK
WHERE TRG.REQUEST_NK IS NULL -- no such record in data martAggiornamento degli attributi SCD2
/* Load request table SCD from master data */
INSERT INTO DIM_REQUEST (DIM_REQUEST_ID_HIST, REQUEST_NK, PAGE_TITLE, PAGE_DESCR, PAGE_KEYWORDS, DELETE_FLAG, UPDATE_DT)
WITH CLS AS ( -- prepare keys
SELECT
'/' || NAME AS REQUEST_NK,
TITLE AS PAGE_TITLE,
CASE WHEN DESCRIPTION = '' OR DESCRIPTION IS NULL
THEN 'n.a.' ELSE DESCRIPTION
END AS PAGE_DESCR,
CASE WHEN KEYWORDS = '' OR KEYWORDS IS NULL
THEN 'n.a.' ELSE KEYWORDS
END AS PAGE_KEYWORDS
FROM STG_CMS_MENU
WHERE CONTENT_TYPE != 'folder' -- only web pages
AND PAGE_TITLE != 'n.a.' -- master data which make sense
)
/* updated records from stage: CLS and build reference to history: HIST */
SELECT
HIST.DIM_REQUEST_ID AS DIM_REQUEST_ID_HIST,
HIST.REQUEST_NK,
CLS.PAGE_TITLE,
CLS.PAGE_DESCR,
CLS.PAGE_KEYWORDS,
0 AS DELETE_FLAG,
STRFTIME('%s','now') AS UPDATE_DT
FROM CLS,
DIM_REQUEST_V_ACT TRG,
DIM_REQUEST HIST
WHERE CLS.REQUEST_NK = TRG.REQUEST_NK
AND TRG.DIM_REQUEST_ID = HIST.DIM_REQUEST_ID
AND ( CLS.PAGE_TITLE != HIST.PAGE_TITLE /* changes only */
OR CLS.PAGE_DESCR != HIST.PAGE_DESCR
OR CLS.PAGE_KEYWORDS != HIST.PAGE_KEYWORDS )Righe eliminate SCD2
/* Load request table SCD from master data */
INSERT INTO DIM_REQUEST (DIM_REQUEST_ID_HIST, REQUEST_NK, PAGE_TITLE, PAGE_DESCR, PAGE_KEYWORDS, DELETE_FLAG, UPDATE_DT)
WITH CLS AS ( -- prepare keys
SELECT
'/' || NAME AS REQUEST_NK,
TITLE AS PAGE_TITLE
FROM STG_CMS_MENU
WHERE CONTENT_TYPE != 'folder' -- only web pages
AND PAGE_TITLE != 'n.a.' -- master data which make sense
)
/* deleted records in data mart: TRG */
SELECT
TRG.DIM_REQUEST_ID AS DIM_REQUEST_ID_HIST,
TRG.REQUEST_NK,
TRG.PAGE_TITLE,
TRG.PAGE_DESCR,
TRG.PAGE_KEYWORDS,
1 AS DELETE_FLAG,
STRFTIME('%s','now') AS UPDATE_DT
FROM (
SELECT
DIM_REQUEST_ID,
REQUEST_NK,
PAGE_TITLE,
PAGE_DESCR,
PAGE_KEYWORDS
FROM DIM_REQUEST_V_ACT
WHERE PAGE_TITLE != 'n.a.' -- track master data only
AND DELETE_FLAG = 0 -- not already deleted
) TRG
LEFT OUTER JOIN CLS ON TRG.REQUEST_NK = CLS.REQUEST_NK
WHERE CLS.REQUEST_NK IS NULL -- no such record in stageOgni fonte di dati deve essere accompagnata da una descrizione formale, ad esempio, in un file readme.txt:
Destinatario dei dati formalmente/tecnicamente: nome, indirizzo email
Fornitore dei dati formalmente/tecnicamente: nome, indirizzo email
Fonte dei dati: percorso del file, nomi dei servizi
Informazioni di accesso ai dati: utenti e password
Lo schema di movimento dei dati aiuterà nel processo di manutenzione e aggiornamento, ad esempio in forma testuale:
Spostamento del file. Fonte: ftp.domain.net: /logs/access.log Obiettivo: /var/www/access.log
Lettura in stage. Obiettivo: STG_ACCESS_LOG
Caricamento e trasformazione. Obiettivo: FCT_ACCESS_REQUEST_REF_HH
Caricamento e trasformazione. Obiettivo: FCT_ACCESS_USER_AGENT_DD
Rapporto. Obiettivo: /var/www/report.html
Risultato
Così, l'articolo descrive meccanismi come l'integrazione dei dati di base e l'introduzione di nuovi livelli di aggregazione. Sono necessari per la costruzione di data warehouse con l'obiettivo di ottenere ulteriori conoscenze e migliorare la qualità delle informazioni.
Fonte: habr.com
