Skip to content

ondata/frontex-sir

Repository files navigation

Frontex SIR Extractor (Guida pratica per giornalisti)

Questo progetto serve a trasformare i PDF dei Serious Incident Reports (SIR) di Frontex in dati strutturati (JSON/CSV), utili per analisi giornalistiche.

Non devi programmare: il flusso base è in 2 comandi.

Cosa fa, in parole semplici

  1. Scarica i pacchetti ZIP pubblicati da Frontex.
  2. Estrae i PDF in cartelle ordinate.
  3. Legge ogni PDF con Gemini e produce un output strutturato con:
    • ID del SIR
    • date
    • luogo (descrizione chiara + granularità)
    • geocodificabilità (yes/no) e query suggerita
    • possibili violazioni dei diritti fondamentali (array strutturato per SIR)
    • morti/feriti/dispersi (confermati o possibili)
    • citazione testuale di evidenza
    • pagine del PDF usate come fonte

Per cercare i file ZIP da scaricare, usa questa pagina: https://prd.frontex.europa.eu/?form-fields%5Bsearch%5D

Su quella pagina cerca: Serious Incident Reports.

Link diretto con filtro gia impostato su Serious Incident Reports: https://prd.frontex.europa.eu/?form-fields%5Bdocument-tag%5D%5B0%5D=409

Cosa NON fa

  • Non fa OCR locale.
  • Non usa pdftotext.
  • Non modifica i PDF originali.

La lettura del documento avviene caricando direttamente il PDF su Gemini.

File principali (in root)

  • process_sir_zips.sh
    Scarica ZIP ed estrae PDF.
  • zip_urls.txt
    Elenco URL ZIP da scaricare (uno per riga).
  • extract_sir_pdf_gemini.py
    Estrae i dati strutturati dai PDF con Gemini.
  • build_sir_csv.py
    Consolida tutti i file .extracted.json in CSV relazionali (vedere § build_sir_csv.py).

Requisiti minimi

  • Ambiente Python già pronto in .venv
  • Variabile API:
export GEMINI_API_KEY="la-tua-chiave"

Flusso standard (2 passi)

1) Scarica ZIP ed estrai PDF

./process_sir_zips.sh zip_urls.txt

Risultato:

  • ZIP in rawdata/
  • PDF in pdfs/<nome-zip>/

2) Estrai dati strutturati dai PDF

source .venv/bin/activate
python extract_sir_pdf_gemini.py pdfs --output-dir analysis_output

Risultato:

  • JSON per ogni PDF in analysis_output/<cartella>/
  • summary.csv e summary_totals.json per ogni cartella
  • summary.csv e summary_totals.json globali in analysis_output/

Logica di skip (per non rifare lavoro già fatto)

Lo script extract_sir_pdf_gemini.py salta automaticamente:

  1. Cartella già completata
    Se trova analysis_output/<cartella>/summary.csv, considera quella cartella già processata e la salta.

  2. Singolo file già processato
    Se trova <nomefile>.extracted.json, salta quel PDF.

  3. Annual report (non-SIR)
    Se il path/nome del PDF contiene pattern tipo annual_report / annual-report / annual report, il file viene saltato prima della chiamata API.

Se vuoi forzare la riesecuzione:

python extract_sir_pdf_gemini.py pdfs --output-dir analysis_output --no-skip-existing

Come legge i PDF e crea l'output strutturato

In pratica:

  1. Carica il PDF binario su Gemini File API.
  2. Invia al modello un prompt con uno schema JSON preciso da rispettare.
  3. Valida il JSON con Pydantic (controllo formale dei campi).
  4. Scrive file .extracted.json e i summary CSV/JSON.

Questo approccio è utile anche per PDF senza testo OCR incorporato.

Prompt usato

Il prompt inviato a Gemini insieme al PDF è in prompts/extract_sir.txt.

Contiene: schema JSON atteso, regole di estrazione, rubrica per il campo confidence, regole per geocodable e coordinate, ed estrazione di possible_violations.

Esempio di output: summary_totals.json

Generato processando i 3 ZIP in zip_urls.txt:

  • https://prd.frontex.europa.eu/wp-content/uploads/pad-2025-00427.zip (SIR 2017)
  • https://prd.frontex.europa.eu/wp-content/uploads/pad-2025-00419.zip (SIR 2021)
  • https://prd.frontex.europa.eu/wp-content/uploads/pad-2025-00475.zip (SIR 2022/2024)
{
  "generated_at_utc": "2026-02-13T07:23:16.169626+00:00",
  "model": "gemini-2.5-flash",
  "input_path": "pdfs",
  "files_processed": 14,
  "files_failed": 0,
  "records_total": 30,
  "dead_confirmed_total": 59,
  "injured_confirmed_total": 23,
  "missing_confirmed_total": 9,
  "dead_possible_total_min": 8,
  "dead_possible_total_max": 120
}

Dove guardare i risultati

  • Vista sintetica globale: analysis_output/summary.csv
  • Totali globali: analysis_output/summary_totals.json
  • Dettaglio per batch: analysis_output/<cartella>/summary.csv
  • Dettaglio per documento: analysis_output/<cartella>/<file>.extracted.json

Audit JSON vuoti

Per la verifica dei file .extracted.json con records: [] (metodo + risultati):

Nota su Git LFS

I file PDF e ZIP sono tracciati con Git LFS. Sono documenti statici pubblicati da Frontex: non cambiano nel tempo e non ha senso versionarli. LFS li archivia separatamente, tenendo nel repository solo puntatori leggeri ed evitando di appesantire la storia dei commit.

Per clonare il repo con i file binari inclusi è sufficiente avere git-lfs installato: il download avviene in automatico durante git clone o git pull.

Struttura cartelle

frontex/
├── process_sir_zips.sh
├── zip_urls.txt
├── extract_sir_pdf_gemini.py
├── build_sir_csv.py
├── rawdata/              # ZIP scaricati
├── pdfs/                 # PDF estratti dagli ZIP
├── analysis_output/      # Output strutturati (JSON, summary CSV)
└── output_csv/           # CSV relazionali consolidati

Nota operativa

Ogni PDF viene inviato a Gemini: considera tempi di esecuzione e costi API in base al numero di file.

Librerie e strumenti usati (con link)

Python

Script download (shell)

Dimensioni dell'archivio

I documenti pubblicati da Frontex sono in due formati: ZIP (batch di più SIR) e PDF (documento singolo).

File scaricati (rawdata/)

Numero Dimensione
ZIP 56
PDF 41
Totale 97 ~1,3 GB

PDF estratti (pdfs/)

Numero Dimensione
PDF 418 ~1,4 GB

I 418 PDF sono il risultato dell'estrazione degli ZIP (ognuno ne contiene più di uno) più i 41 PDF singoli.


Note finali

Questo è un primo esperimento esplorativo.

Il modello usato è gemini-2.5-flash: non è il più potente disponibile, ma permette di fare test iniziali a costo zero grazie al piano gratuito di Google AI Studio.

Non è stata ancora fatta nessuna verifica della qualità dei dati estratti, né automatica né manuale. I risultati vanno trattati come bozza da validare.


Setup

PDF e ZIP sono tracciati via Git LFS. Serve git-lfs installato prima del clone — il download avviene in automatico.

# Installa git-lfs (una tantum)
# macOS:  brew install git-lfs
# Ubuntu: apt install git-lfs

git clone https://github.com/ondata/frontex-sir
cd frontex-sir
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
export GEMINI_API_KEY="..."

Script

fetch_sir_zip_urls.py

Scarica l'elenco aggiornato di tutti i documenti SIR dal registro pubblico Frontex.

Itera tutte le pagine del registro, raccoglie i metadati e i link di download da ogni scheda, e produce:

  • zip_urls.txt — un URL di download per riga (ZIP o PDF), append idempotente
  • sir_documents.jsonl — un record JSON per documento con i seguenti campi:
Campo Descrizione
doc_id ID interno del documento nel registro Frontex
title Titolo del documento
publication_date Data di pubblicazione (formato ISO YYYY-MM-DD)
language Lingua (es. EN)
document_format Formato del file (ZIP o PDF)
tags Tag associati (es. PAD 2025, SIR)
download_urls Lista di oggetti {url, label} con i link di download
document_page_url URL della pagina del documento sul sito Frontex

È sicuro da eseguire periodicamente (es. settimanalmente): aggiunge solo i documenti non già presenti.

# Vedi cosa ci sarebbe di nuovo senza scrivere nulla
python3 fetch_sir_zip_urls.py --dry-run

# Aggiorna zip_urls.txt e sir_documents.jsonl
python3 fetch_sir_zip_urls.py

Opzioni:

Opzione Descrizione
--output FILE File URL list (default: zip_urls.txt)
--jsonl FILE File metadati (default: sir_documents.jsonl)
--dry-run Stampa i nuovi URL senza scrivere
--pages N Limita la scansione a N pagine (default: 20)

process_sir_zips.sh

Scarica i file elencati in zip_urls.txt e li prepara per l'estrazione.

  • Per i ZIP: scarica in rawdata/, estrae i PDF in pdfs/<nome-zip>/
  • Per i PDF diretti: scarica in rawdata/, copia in pdfs/<nome-file>/

I file già presenti vengono saltati (idempotente).

# Uso base
./process_sir_zips.sh zip_urls.txt

# Cartelle personalizzate
./process_sir_zips.sh zip_urls.txt --zip-dir rawdata --pdf-dir pdfs

Opzioni:

Opzione Descrizione
--zip-dir DIR Dove salvare i file scaricati (default: rawdata/)
--pdf-dir DIR Dove estrarre i PDF (default: pdfs/)

extract_sir_pdf_gemini.py

Legge ogni PDF con Gemini e produce dati strutturati in JSON e CSV.

Carica il PDF su Gemini File API, invia il prompt di estrazione, valida la risposta con Pydantic e scrive un .extracted.json per ogni PDF. Nei run completi (non incrementali) scrive anche i file di riepilogo per cartella e globali.

source .venv/bin/activate

# Processa tutti i PDF
python3 extract_sir_pdf_gemini.py pdfs --output-dir analysis_output

# Forza la rielaborazione (ignora i file già esistenti)
python3 extract_sir_pdf_gemini.py pdfs --output-dir analysis_output --no-skip-existing

# Un singolo PDF
python3 extract_sir_pdf_gemini.py pdfs/pad-2025-00419/somefile.pdf --output-dir analysis_output

# Prompt alternativo (per A/B testing)
python3 extract_sir_pdf_gemini.py pdfs --prompt-path prompts/extract_sir_v2.txt

# Batch incrementale: processa al massimo 5 nuovi PDF per run (senza rifare i già fatti)
python3 extract_sir_pdf_gemini.py pdfs --output-dir analysis_output --max-new-files 5

# Comando base consigliato: 20 PDF per volta (senza summary globali/parziali)
python3 extract_sir_pdf_gemini.py pdfs --output-dir analysis_output --max-new-files 20

Opzioni principali:

Opzione Descrizione
--model NAME Modello Gemini usato (default: gemini-2.5-flash)
--output-dir DIR Cartella output (default: analysis_output)
--prompt-path FILE File prompt alternativo (default: prompts/extract_sir.txt)
--no-skip-existing Rielabora anche i PDF già processati
--exclude PATTERN Esclude file per pattern glob (ripetibile)
--min-seconds-between-calls N Pausa tra chiamate API (default: 4s)
--max-new-files N Processa al massimo N nuovi file per esecuzione (0 = nessun limite)
--no-skip-completed-groups Non saltare cartelle con summary.csv (utile per batch incrementali)
--no-skip-annual-reports Non saltare i PDF annual report (default: vengono saltati)

Nota: quando usi --max-new-files, lo script lavora in modalità incrementale:

  • processa solo file nuovi (non già estratti);
  • si ferma appena raggiunge il limite;
  • non aggiorna i summary CSV/JSON globali o per cartella, per evitare riepiloghi parziali.

Modalità incrementale (--max-new-files)

Permette di processare i PDF a piccoli blocchi, senza dover lanciare tutto in una volta. Utile quando l'archivio è grande e si vuole distribuire le chiamate API nel tempo (es. per rispettare quote o costi).

Come funziona:

  1. Lo script trova tutti i PDF non ancora processati (senza .extracted.json).
  2. Ne processa al massimo N per ogni lancio.
  3. Si ferma raggiunto il limite, senza scrivere summary parziali.
  4. Al lancio successivo riparte dai PDF ancora da fare.

I PDF già estratti non vengono mai rilavorati, anche se la cartella non ha ancora un summary.csv.

Esempio — processare 10 PDF alla volta:

# Primo lancio: processa i primi 10 nuovi
python3 extract_sir_pdf_gemini.py pdfs --output-dir analysis_output --max-new-files 10

# Secondo lancio: processa i successivi 10
python3 extract_sir_pdf_gemini.py pdfs --output-dir analysis_output --max-new-files 10

# Terzo lancio: e così via, fino a esaurimento dei PDF

Ogni lancio stampa quanti file ha processato:

[DONE] Incremental batch processed 10/10 new files; failures=0.

Quando non ci sono più PDF da processare:

[DONE] Incremental batch: no new files found.

build_sir_csv.py

Consolida tutti i file .extracted.json in due CSV relazionali pronti per analisi.

  • Legge ricorsivamente tutti i *.extracted.json da analysis_output/
  • Produce due tabelle linkabili via record_uid
source .venv/bin/activate

# Usa le cartelle di default (analysis_output → output_csv)
python3 build_sir_csv.py

# Cartelle personalizzate
python3 build_sir_csv.py --input-dir analysis_output --output-dir output_csv

Opzioni:

Opzione Descrizione
--input-dir DIR Cartella con i .extracted.json (default: analysis_output)
--output-dir DIR Cartella di output (default: output_csv)

Output: output_csv/

sir_records.csv — una riga per SirRecord

Campo Note
record_uid Chiave primaria (intero progressivo)
batch Nome della cartella batch di origine
source_file Path del PDF sorgente
record_index Indice del record nel PDF (utile se un PDF contiene più SIR)
model Modello Gemini usato
generated_at_utc Timestamp di estrazione
sir_id ID del SIR (formato DDDDD/YYYY)
report_date Data del rapporto
incident_date Data dell'incidente
location_details Descrizione estesa del luogo
where_clear Luogo sintetico chiaro
location_text_raw Testo grezzo del luogo dal PDF
country_or_area Paese o area geografica
location_type sea / land / facility / mixed / unknown
precision_level exact / approximate / broad / unknown
geocodable yes / no
geocodable_query Query suggerita per geocodifica
lat / lon Coordinate (se disponibili)
uncertainty_note Note sull'incertezza della localizzazione
dead_confirmed Morti confermati
injured_confirmed Feriti confermati
missing_confirmed Dispersi confermati
dead_possible_min/max Range di morti possibili
possible_violations_count Numero di violazioni elencate
context_note Nota contestuale
libyan_coast_guard_involved Coinvolgimento guardia costiera libica
evidence_quote Citazione testuale di evidenza
confidence high / medium / low
evidence_pages Pagine PDF usate come fonte (es. "1,2,3")

violations.csv — una riga per violazione dei diritti fondamentali

Campo Note
record_uid FK → sir_records.record_uid
sir_id Per join alternativo
source_file Per join alternativo
violation_index Ordine nella lista
violation_name Nome della violazione
legal_basis Base legale (nullable)
assessment likely / possible / unclear / not_stated

Esempio di analisi con DuckDB

# Totali generali
duckdb :memory: "
SELECT
  count(*) AS records,
  count(DISTINCT sir_id) AS unique_sirs,
  sum(dead_confirmed::int) AS total_dead,
  sum(missing_confirmed::int) AS total_missing
FROM 'output_csv/sir_records.csv';
"

# Violazioni più frequenti
duckdb :memory: "
SELECT violation_name, count(*) AS n
FROM 'output_csv/violations.csv'
GROUP BY 1 ORDER BY 2 DESC LIMIT 10;
"

# Join records + violations
duckdb :memory: "
SELECT r.sir_id, r.incident_date, v.violation_name, v.assessment
FROM 'output_csv/sir_records.csv' r
JOIN 'output_csv/violations.csv' v ON r.record_uid = v.record_uid
LIMIT 20;
"

About

Estrazione dati strutturati dai Serious Incident Reports (SIR) di Frontex via Gemini

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages