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.
- Scarica i pacchetti ZIP pubblicati da Frontex.
- Estrae i PDF in cartelle ordinate.
- 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
- Non fa OCR locale.
- Non usa
pdftotext. - Non modifica i PDF originali.
La lettura del documento avviene caricando direttamente il PDF su Gemini.
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.jsonin CSV relazionali (vedere § build_sir_csv.py).
- Ambiente Python già pronto in
.venv - Variabile API:
export GEMINI_API_KEY="la-tua-chiave"./process_sir_zips.sh zip_urls.txtRisultato:
- ZIP in
rawdata/ - PDF in
pdfs/<nome-zip>/
source .venv/bin/activate
python extract_sir_pdf_gemini.py pdfs --output-dir analysis_outputRisultato:
- JSON per ogni PDF in
analysis_output/<cartella>/ summary.csvesummary_totals.jsonper ogni cartellasummary.csvesummary_totals.jsonglobali inanalysis_output/
Lo script extract_sir_pdf_gemini.py salta automaticamente:
-
Cartella già completata
Se trovaanalysis_output/<cartella>/summary.csv, considera quella cartella già processata e la salta. -
Singolo file già processato
Se trova<nomefile>.extracted.json, salta quel PDF. -
Annual report (non-SIR)
Se il path/nome del PDF contiene pattern tipoannual_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-existingIn pratica:
- Carica il PDF binario su Gemini File API.
- Invia al modello un prompt con uno schema JSON preciso da rispettare.
- Valida il JSON con Pydantic (controllo formale dei campi).
- Scrive file
.extracted.jsone i summary CSV/JSON.
Questo approccio è utile anche per PDF senza testo OCR incorporato.
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.
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
}- 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
Per la verifica dei file .extracted.json con records: [] (metodo + risultati):
- Report:
docs/empty-json-audit-2026-02-17.md - Lista
vuoti probabilmente corretti:tmp/empty_json_probably_correct.tsv - Lista
da ricontrollare:tmp/empty_json_to_review.tsv
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.
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
Ogni PDF viene inviato a Gemini: considera tempi di esecuzione e costi API in base al numero di file.
-
google-genai
Link: https://github.com/googleapis/python-genai
Perche utile: e il client ufficiale usato per caricare i PDF su Gemini File API e ottenere la risposta del modello in JSON. -
pydantic
Link: https://docs.pydantic.dev/latest/
Perche utile: valida la struttura dei dati estratti (campi obbligatori, tipi, vincoli), riducendo errori nei risultati finali. -
argparse(standard library)
Link: https://docs.python.org/3/library/argparse.html
Perche utile: gestisce i parametri da riga di comando (--output-dir,--skip-existing,--model, ecc.). -
pathlib(standard library)
Link: https://docs.python.org/3/library/pathlib.html
Perche utile: gestisce percorsi e cartelle in modo robusto (input PDF, output JSON/CSV, skip per cartella).
-
curl
Link: https://curl.se/docs/
Perche utile: scarica gli ZIP Frontex dagli URL nel filezip_urls.txt. -
unzipLink: https://infozip.sourceforge.net/UnZip.html Perche utile: estrae i PDF dagli ZIP mantenendo una struttura ordinata inpdfs/<nome-zip>/.
I documenti pubblicati da Frontex sono in due formati: ZIP (batch di più SIR) e PDF (documento singolo).
| Numero | Dimensione | |
|---|---|---|
| ZIP | 56 | |
| 41 | ||
| Totale | 97 | ~1,3 GB |
| Numero | Dimensione | |
|---|---|---|
| 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.
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.
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="..."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 idempotentesir_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.pyOpzioni:
| 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) |
Scarica i file elencati in zip_urls.txt e li prepara per l'estrazione.
- Per i ZIP: scarica in
rawdata/, estrae i PDF inpdfs/<nome-zip>/ - Per i PDF diretti: scarica in
rawdata/, copia inpdfs/<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 pdfsOpzioni:
| Opzione | Descrizione |
|---|---|
--zip-dir DIR |
Dove salvare i file scaricati (default: rawdata/) |
--pdf-dir DIR |
Dove estrarre i PDF (default: pdfs/) |
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 20Opzioni 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.
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:
- Lo script trova tutti i PDF non ancora processati (senza
.extracted.json). - Ne processa al massimo
Nper ogni lancio. - Si ferma raggiunto il limite, senza scrivere summary parziali.
- 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 PDFOgni 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.
Consolida tutti i file .extracted.json in due CSV relazionali pronti per analisi.
- Legge ricorsivamente tutti i
*.extracted.jsondaanalysis_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_csvOpzioni:
| Opzione | Descrizione |
|---|---|
--input-dir DIR |
Cartella con i .extracted.json (default: analysis_output) |
--output-dir DIR |
Cartella di output (default: 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 |
# 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;
"