Monitorare l'intelligenza artificiale significava aprire una quindicina di blog ogni mattina: OpenAI, Anthropic, Google DeepMind, Mistral, Hugging Face, NVIDIA… Volevo un unico posto, alimentato da solo. È diventato
Veille IA, online dal 19 settembre 2026.
Ecco la pipeline che gira dietro, le scelte che ho fatto e perché — e soprattutto ciò che non ha funzionato al primo colpo, perché è lì che ho imparato di più.
Una costrizione volontaria: nessun framework
Lavoro quotidianamente con Symfony. Per questo progetto mi sono imposto il contrario: PHP 8.4 nativo. Router, container di iniezione delle dipendenze (risoluzione automatica per riflessione), motore di template, cache e logging sono scritti per l'occasione. Una sola dipendenza di produzione: HTMLPurifier.
L'obiettivo non era reinventare Symfony, ma capire cosa fa al posto mio: il ciclo richiesta-risposta, la catena di middleware, la risoluzione delle dipendenze. Il prezzo è reale: circa 14.700 righe di PHP da mantenere, e ogni garanzia che un framework offre di serie — token CSRF, sessioni, escaping — deve essere scritta e testata. Vedremo più avanti che una di queste garanzie mi è costata una serata di debug.
La pipeline
Pipeline di raccolta di Veille IA Attivata da un cron giornaliero e da una coda, la pipeline concatena nove fasi: fonti, raccolta HTTP, analisi, normalizzazione, deduplicazione, pulizia, classificazione, archiviazione e diffusione. Cron giornaliero · coda (5 min) 1 Fonti16 fonti attive: RSS, Atom, RDF, HTML 2 Raccolta HTTProbots.txt · ETag \/ Last-Modified · anti-SSRF 3 AnalisiParser DOM anti-XXE · JSON-LD, Open Graph 4 NormalizzazioneURL canonico · date convertite in UTC 5 DeduplicazioneURL → identificatore del feed → titolo + data 6 PuliziaHTMLPurifier · solo estratto + link alla fonte 7 ClassificazioneRegole ponderate · soglia · categoria di fallback 8 ArchiviazioneMySQL 8.4 · indice FULLTEXT 9 DiffusioneSito · ricerca · feed RSS · sitemap
Le nove fasi di una pubblicazione, dalla fonte alla pagina. Ogni esecuzione rielabora solo ciò che è cambiato.
Raccogliere senza abusare delle fonti
La prima regola del progetto era comportarsi da ospite corretto sui siti che si leggono:
- robots.txt viene letto e rispettato prima di ogni richiesta (implementazione della RFC 9309), con un robot identificabile dal suo user agent e un ritardo tra due chiamate.
- Richieste condizionali: quando una fonte fornisce un ETag o un Last-Modified, lo ripropongo all'esecuzione successiva. Una risposta 304 Not Modified significa che nulla è cambiato: nulla viene scaricato, nulla viene rielaborato. In una mattina ordinaria è il caso di quasi la metà delle fonti.
- Solo estratto: il sito mostra il titolo, i metadati, un breve estratto e un link visibile alla pubblicazione originale. Il testo integrale non viene mai memorizzato, salvo che la licenza della fonte lo autorizzi esplicitamente — cosa che oggi non vale per nessuna.
Tre fonti sono volutamente disattivate: Meta AI, le cui condizioni d'uso vietano la raccolta automatizzata; Perplexity, protetta da un dispositivo anti-robot; VentureBeat, che limita fortemente il throughput (HTTP 429). Una fonte che vieta la raccolta non viene raccolta, anche quando è tecnicamente possibile.
Una URL proveniente da un feed è un input non affidabile
Un feed RSS contiene URL che non ho scelto io, e il mio server le chiamerà. È esattamente lo scenario di un attacco SSRF, in cui si fa interrogare a un server i propri servizi interni. Da qui diverse protezioni:
- gli indirizzi privati e riservati sono rifiutati;
- l'indirizzo IP risolto viene fissato per la connessione, il che impedisce un cambio di risoluzione DNS tra la verifica e la richiesta;
- i redirect vengono seguiti manualmente e riverificati a ogni salto;
- la dimensione delle risposte è limitata.
Lato XML, il parser DOM è configurato contro le entità esterne (XXE), con un test dedicato affinché una regressione non passi inosservata. Lato HTML, HTMLPurifier pulisce il contenuto all'importazione, al salvataggio e alla visualizzazione.
Deduplicare: tre impronte
Lo stesso articolo circola in più forme: URL con o senza parametri di tracciamento, ripubblicazione, feed che cambia identificatore. Il rilevamento procede in tre passaggi:
- l'URL canonico, normalizzato (parametri di tracciamento come utm_* o fbclid rimossi) e poi sottoposto a hash;
- l'identificatore fornito dal feed (guid in RSS, id in Atom);
- in mancanza, un'impronta stabile del titolo normalizzato e della data.
public function externalHash(FeedItem $item): string
{
$id = $item->externalId !== null ? trim($item->externalId) : '';
if ($id !== '') {
return hash('sha256', 'id:' . $id);
}
// Pas d'identifiant : empreinte du titre normalisé et de la date de publication
$title = trim((string) preg_replace('/[^a-z0-9]+/', ' ', Text::fold($item->title)));
return hash('sha256', 'fp:' . $title . '|' . ($item->publishedAt?->format('Y-m-d') ?? ''));
}La data è considerata al giorno, non al secondo: lo stesso articolo ripubblicato con un orario diverso mantiene la stessa impronta. Un'impronta del contenuto permette poi di rilevare gli aggiornamenti, e un articolo corretto a mano nell'amministrazione viene contrassegnato affinché la sincronizzazione successiva non sovrascriva la correzione.
Cercare «IA» in un indice FULLTEXT
La ricerca si basa sull'indice FULLTEXT di MySQL in modalità booleana, con il titolo ponderato tre volte più del resto nel calcolo della pertinenza. Si scontra con un tranello classico: InnoDB ignora le parole più corte di innodb_ft_min_token_size (3 caratteri per impostazione predefinita). Su un sito dedicato all'IA, «IA» e «AI» erano quindi introvabili. E un termine come gpt-4o viene spezzato al trattino.
Pour ces termes, la recherche bascule sur REGEXP_LIKE, le moteur d'expressions régulières ICU de MySQL 8, avec un motif borné par mot. C'est plus lent, mais réservé aux seuls termes que l'index ne sait pas traiter.
Per questi termini, la ricerca passa a REGEXP_LIKE, il motore di espressioni regolari ICU di MySQL 8, con un pattern delimitato per parola. È più lento, ma riservato ai soli termini che l'indice non sa gestire.
La sincronizzazione gira ogni mattina via cron. Dall'amministrazione, il pulsante «sincronizza» non esegue nulla da sé: deposita una richiesta in una coda che il cron svuota ogni cinque minuti. Il processo web non lancia mai comandi di sistema, e le funzioni che lo permetterebbero (exec, shell_exec, proc_open…) sono disattivate nella configurazione di PHP. Un lock impedisce che due sincronizzazioni si sovrappongano.
Ciò che non ha funzionato
Una pipeline che sembrava mezzo rotta
Un revisore mi ha segnalato che gli ultimi articoli venivano tutti da TechCrunch e The Verge, e che OpenAI e Anthropic non apparivano nel blocco «per fornitore» della home. Primo riflesso: feed rotti. I log dicevano il contrario: 15 delle 16 fonti si erano sincronizzate quella stessa mattina, con successo o con 304. L'unica in errore, Hugging Face, restituiva un errore 503 passeggero, senza rapporto con il sintomo.
La causa era altrove. Il blocco tratteneva le fonti che avevano pubblicato più di recente, e i media pubblicano tutti i giorni, weekend compreso — non i laboratori. Due posti su sei andavano ai media, il che spingeva OpenAI al settimo. Il blocco ora è riservato ai laboratori. Lezione: leggere i log prima di correggere. Una pipeline corretta può benissimo sembrare rotta.
La classificazione a regole mostra i suoi limiti
La classificazione si basa su 106 regole ponderate: ogni parola chiave o espressione regolare aggiunge il suo peso a una categoria, che viene scelta oltre una soglia. Due casi mi hanno mostrato i limiti del sistema.
Un anello connesso per prendere appunti è stato classificato in «Sicurezza & etica», perché il suo estratto parlava di «privacy questions». La parola chiave privacy pesava 2: esattamente la soglia. Bastava una sola parola incidentale. E Google Research, un laboratorio di ricerca, vedeva 27 dei suoi 30 articoli finire nella categoria di fallback, per mancanza di una regola che riconoscesse il suo vocabolario: in totale, 125 pubblicazioni su 330 vi finiscono.
Non è ancora corretto. I prossimi passi sono impedire a una parola isolata di raggiungere la soglia e dare a ogni fonte una categoria predefinita, prima di considerare una classificazione tramite modello linguistico.
Tre guasti in produzione, nessuno in locale
Tutti i form rispondevano 419. Impossibile accedere all'amministrazione in produzione, mentre tutto funzionava in locale. Per isolare la causa, ho inviato lo stesso token CSRF in due modi: in un header HTTP, la richiesta passava (303); nel corpo del form, falliva (419). La sessione e il token erano quindi sani: era il corpo della richiesta che PHP non leggeva. In causa, enable_post_data_reading = Off nel php.ini del server, che lascia $_POST vuoto qualunque cosa si invii. L'impostazione è ripristinata a livello del pool PHP-FPM dell'applicazione. È la garanzia «offerta di serie» dai framework di cui parlavo sopra: senza framework, tocca a me sapere dove guardare.
Un 403 su tutto il sito dopo un aggiornamento. I file appartengono a un utente di sistema dedicato, e Apache vi accede solo attraverso il gruppo di quell'utente. Un aggiornamento lanciato da un altro account ha riassegnato i file: Apache non poteva più leggere nulla. Ora il codice arriva solo via rsync dal mio computer, e lo script di deploy ripara i permessi prima di ogni altra fase.
Delle sitemap false senza il minimo errore. Un errore di battitura nella configurazione, https:\/\/https:\/\/, rendeva invalidi tutti gli URL delle sitemap. Né errore né riga di log: è stata la Search Console a rivelarlo, con zero pagine scoperte. Lezione: una configurazione aberrante dovrebbe impedire all'applicazione di avviarsi, non passare in silenzio.
In cifre
IndicatoreValore
Fonti configurate \/ attive | 19 \/ 16, di cui 3 media
Pubblicazioni raccolte dalla messa online | 330
Categorie · temi · regole di classificazione | 9 · 11 · 106
Test automatizzati (PHPUnit) | 106
Codice applicativo | ≈ 14.700 righe di PHP, 1 dipendenza di produzione
Stack | PHP 8.4, MySQL 8.4, Tailwind CSS 4, Apache e PHP-FPM, cron
Limiti e prossimi passi
- Tutto il contenuto raccolto è in inglese per un sito in francese. Il prossimo passo è un breve riassunto in francese prodotto lato server, presentato come tale, correggibile a mano e limitato ai fatti presenti nella fonte — senza rimettere in discussione la politica dell'estratto.
- La classificazione deve guadagnare in affidabilità, come descritto sopra.
- Le analisi personali: il sito deve distinguere chiaramente ciò che è raccolto da ciò che scrivo io stesso.
La parte più difficile non è stata leggere feed RSS. È stato rispettare le fonti, trattare ogni dato in ingresso come potenzialmente ostile, e capire perché un'applicazione che funziona perfettamente in locale cade in produzione. È anche ciò che questo progetto mi ha insegnato di più utile.