Hai un DOI, la pagina dell'editore chiede il login e a te serve solo leggere la sezione dei metodi. Molto spesso esiste da qualche parte una copia legale e gratuita: il repository dell'università dell'autore, un preprint, PubMed Central. A mano bisogna provare quattro o cinque siti. In codice bastano poche API pubbliche chiamate in un ordine sensato.

L'ordine qui sotto è quello del pulsante View nello spazio di lavoro Paper dell'app desktop osFoundry, il cui backend prova ogni fonte a turno finché una restituisce un vero PDF (la pagina dello spazio di lavoro per scrivere e ricercare articoli mostra la schermata intorno). Gli esempi usano due DOI reali sulla stessa domanda: un telefono appoggiato sulla scrivania danneggia la concentrazione?

  • 10.1037/tmb0000123, una meta-analisi del 2024 (Hartanto et al.)
  • 10.1086/691462, il molto citato studio «Brain Drain» del 2017 (Ward et al.)

View su un risultato di ricerca apre il testo della meta-analisi dentro l'app

Chiedi a OpenAlex tutte le posizioni, non solo il flag open access

OpenAlex elenca ogni posto in cui un articolo è ospitato e, per ciascuno, se c'è un PDF diretto:

curl -s "https://api.openalex.org/works/doi:10.1037/tmb0000123?select=open_access,best_oa_location,locations" 

Per la meta-analisi restituisce tre posizioni: il link al PDF dell'editore, un PDF nel repository della James Cook University e una landing page nella biblioteca della Singapore Management University.

Per l'articolo Brain Drain la stessa richiesta mostra "is_oa": true e "oa_status": "green", ma ogni pdf_url è null. L'unica copia aperta è la landing page di un repository. Quindi is_oa dice che una versione gratuita esiste da qualche parte, non che puoi scaricarla. Leggi pdf_url su ogni posizione.

Senza chiave API, le richieste a OpenAlex condividono un piccolo budget giornaliero gratuito con tutti quelli che usano il tuo indirizzo IP, e meta.notice nella risposta lo segnala. Una chiave gratuita (api_key= nella query) ti dà un budget tuo, e l'app ne legge una da OPENALEX_API_KEY se la imposti.

Controlla i primi cinque byte

Anche un pdf_url è solo una promessa. Gli editori restituiscono muri di cookie, controlli anti-bot e pagine di login con stato 200 e corpo HTML. Il codice Rust dell'app accetta un download solo se il corpo inizia con il magic number del PDF:

if !fetched.bytes.starts_with(b"%PDF-") { self.tried.push(format!("{source}: link was a web page, not a PDF")); return None; } 

La stessa idea in Python, con la sola libreria standard:

import json import urllib.error import urllib.request UA = {"User-Agent": "paper-finder/1.0"} def get(url): with urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=30) as r: return r.read() def pdf_links(doi): try: work = json.loads(get(f"https://api.openalex.org/works/doi:{doi}" "?select=best_oa_location,locations")) except urllib.error.HTTPError as e: if e.code == 404: return [] raise links = [] for loc in [work.get("best_oa_location")] + work.get("locations", []): url = (loc or {}).get("pdf_url") if url and url not in links: links.append(url) return links def first_real_pdf(doi): tried = [] for url in pdf_links(doi)[:4]: try: body = get(url) except (urllib.error.URLError, TimeoutError) as e: tried.append(f"{url}: {e}") continue if body[:5] == b"%PDF-": return url, body tried.append(f"{url}: a web page, not a PDF") raise LookupError("no free PDF; tried: " + "; ".join(tried or ["no pdf_url at all"])) 

Sulla meta-analisi il link dell'editore ha rifiutato lo script e la copia del repository è arrivata come PDF da 1,6 MB. Su Brain Drain fallisce subito con «no pdf_url at all». Tenere l'elenco di cosa è stato provato e perché è fallito vale le righe in più; un semplice «nessun testo completo» ti rimanda a fare tutto a mano.

Anche il limite di quattro link per ricerca e la deduplicazione degli URL vengono dall'app. Alcuni articoli hanno una dozzina di posizioni che puntano tutte allo stesso file.

Il resto della catena

La ricerca su OpenAlex è solo un passo. L'ordine completo dell'app:

  1. Il link al PDF arrivato con il risultato di ricerca, se c'era.
  2. arXiv. Un DOI del tipo 10.48550/arXiv.<id> è un articolo arXiv, e https://arxiv.org/pdf/<id> è sempre gratuito.
  3. Le copie aperte del DOI, da Unpaywall o da OpenAlex. Unpaywall (https://api.unpaywall.org/v2/<doi>?email=<your email>) restituisce best_oa_location.url_for_pdf e un elenco oa_locations, e rifiuta le richieste senza un indirizzo e-mail vero. I due servizi sono della stessa organizzazione non profit, OurResearch, quindi l'app interroga Unpaywall quando UNPAYWALL_EMAIL è impostata e altrimenti legge le posizioni di OpenAlex, mai entrambi.
  4. CORE. La sua API v3 (/v3/search/works?q=doi:"<doi>", con una chiave bearer) aggrega copie dai repository e spesso restituisce direttamente il testo estratto in fullText. L'app usa quel testo se supera i 500 caratteri, altrimenti prova il downloadUrl.
  5. PubMed Central. Con un PMCID, Europe PMC serve l'articolo intero come XML JATS all'indirizzo https://www.ebi.ac.uk/europepmc/webservices/rest/<PMCID>/fullTextXML. Non è un PDF, ma ha sezioni, figure e riferimenti in una struttura che si converte bene in HTML.

Gli abstract arrivano come indice invertito

Un ultimo dettaglio di OpenAlex su cui si inciampa spesso: non esiste un campo abstract. Si riceve abstract_inverted_index, una mappa da ogni parola alle posizioni in cui compare, e il testo va ricostruito da soli:

words = {pos: w for w, positions in idx.items() for pos in positions} abstract = " ".join(words[i] for i in sorted(words)) 

Basta per i risultati di ricerca e per una prima selezione. Per l'articolo Brain Drain, la cui unica copia aperta è una pagina di repository, l'abstract più il testo completo della meta-analisi è circa il massimo a cui arriva la via gratuita, ed è di solito il momento di controllare se la tua biblioteca ha accesso.