Dev.to AI 🤖 Ai 👁 0 📖 5 min read

Cómo encontrar el PDF gratuito de un artículo científico a partir de su DOI

Tienes un DOI, la página de la editorial pide iniciar sesión y solo necesitas leer la sección de métodos. Muy a menudo existe una copia gratuita y legal en algún sitio: el repositorio de la universidad del autor, un prep

Cómo encontrar el PDF gratuito de un artículo científico a partir de su DOI

Tienes un DOI, la página de la editorial pide iniciar sesión y solo necesitas leer la sección de métodos. Muy a menudo existe una copia gratuita y legal en algún sitio: el repositorio de la universidad del autor, un preprint, PubMed Central. Buscarla a mano obliga a probar cuatro o cinco webs. Hacerlo desde código requiere un puñado de APIs públicas llamadas en un orden razonable.

El orden que sigue es el del botón Ver del espacio de trabajo Paper de la aplicación de escritorio osFoundry, cuyo backend prueba cada fuente por turno hasta que una devuelve un PDF de verdad (la página del espacio de trabajo de redacción e investigación de artículos muestra la pantalla que lo rodea). Los ejemplos usan dos DOI reales sobre la misma pregunta: si tener el móvil sobre la mesa perjudica la concentración.

  • 10.1037/tmb0000123, un metaanálisis de 2024 (Hartanto et al.)
  • 10.1086/691462, el muy citado estudio "Brain Drain" de 2017 (Ward et al.)

Al pulsar Ver en un resultado de búsqueda se abre el texto del metaanálisis dentro de la aplicación

Pide a OpenAlex todas las ubicaciones, no solo el indicador de acceso abierto

OpenAlex enumera cada lugar donde está alojado un artículo y, para cada uno, si hay un PDF directo:

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

Para el metaanálisis devuelve tres ubicaciones: el enlace al PDF de la editorial, un PDF en el repositorio de la James Cook University y una página de aterrizaje en la biblioteca de la Singapore Management University.

Para el artículo Brain Drain, la misma petición muestra "is_oa": true y "oa_status": "green", pero todos los pdf_url son null. La única copia abierta es la página de un repositorio. Es decir, is_oa indica que existe una versión gratuita en algún lugar, no que puedas descargarla. Lee pdf_url en cada ubicación.

Sin clave de API, las peticiones a OpenAlex comparten con todos los usuarios de tu IP un pequeño presupuesto diario gratuito, y el meta.notice de la respuesta lo indica. Una clave gratuita (api_key= en la consulta) te da tu propio presupuesto, y la aplicación lee una de OPENALEX_API_KEY si la defines.

Comprueba los cinco primeros bytes

Un pdf_url sigue siendo solo una promesa. Las editoriales devuelven muros de cookies, comprobaciones antibot y páginas de login con estado 200 y cuerpo HTML. El código Rust de la aplicación solo acepta una descarga si el cuerpo empieza por el número mágico 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 misma idea en Python, usando solo la biblioteca estándar:

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"]))

Con el metaanálisis, el enlace de la editorial rechazó el script y la copia del repositorio llegó como un PDF de 1,6 MB. Con Brain Drain, falla enseguida con "no pdf_url at all". Merece la pena guardar la lista de lo que se probó y por qué falló; un simple "no hay texto completo" te devuelve a hacerlo todo a mano.

El límite de cuatro enlaces por búsqueda y la eliminación de URL duplicadas también vienen de la aplicación. Algunos artículos tienen una docena de ubicaciones que apuntan todas al mismo archivo.

El resto de la cadena

La consulta a OpenAlex es solo un paso. El orden completo de la aplicación es este:

  1. El enlace al PDF que venía con el resultado de búsqueda, si lo había.
  2. arXiv. Un DOI con la forma 10.48550/arXiv.<id> es un artículo de arXiv, y https://arxiv.org/pdf/<id> siempre es gratuito.
  3. Copias abiertas del DOI, de Unpaywall o de OpenAlex. Unpaywall (https://api.unpaywall.org/v2/<doi>?email=<your email>) devuelve best_oa_location.url_for_pdf y una lista de oa_locations, y rechaza las peticiones sin un correo real. Los dos servicios pertenecen a la misma organización sin ánimo de lucro, OurResearch, así que la aplicación consulta Unpaywall cuando hay un UNPAYWALL_EMAIL definido y, si no, lee las ubicaciones de OpenAlex; nunca ambos.
  4. CORE. Su API v3 (/v3/search/works?q=doi:"<doi>", con una clave bearer) reúne copias de repositorios y a menudo devuelve el texto extraído directamente en fullText. La aplicación usa ese texto si tiene más de 500 caracteres y, si no, prueba con el downloadUrl.
  5. PubMed Central. Con un PMCID, Europe PMC sirve el artículo completo como XML JATS en https://www.ebi.ac.uk/europepmc/webservices/rest/<PMCID>/fullTextXML. No es un PDF, pero trae secciones, figuras y referencias en una estructura que se convierte limpiamente a HTML.

Los resúmenes llegan como un índice invertido

Un último detalle de OpenAlex que confunde a mucha gente: no existe un campo abstract. Se recibe abstract_inverted_index, un mapa de cada palabra a las posiciones en las que aparece, y el texto hay que reconstruirlo:

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

Con eso basta para los resultados de búsqueda y para hacer una primera criba. En el caso de Brain Drain, cuya única copia abierta es una página de repositorio, el resumen más el texto completo del metaanálisis es lo más lejos que llega la vía gratuita, y suele ser el momento de comprobar si tu biblioteca tiene acceso.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.