DEV Community

Cover image for Comment trouver le PDF gratuit d'un article scientifique à partir de son DOI

Comment trouver le PDF gratuit d'un article scientifique à partir de son DOI

Vous avez un DOI, la page de l'éditeur demande une connexion, et vous voulez seulement lire la section méthodes. Très souvent, une copie légale et gratuite existe quelque part : le dépôt de l'université de l'auteur, un preprint, PubMed Central. À la main, il faut essayer quatre ou cinq sites. En code, il suffit de quelques API publiques appelées dans le bon ordre.

L'ordre ci-dessous est celui du bouton View de l'espace Paper de l'application de bureau osFoundry, dont le backend essaie chaque source tour à tour jusqu'à obtenir un vrai PDF (la page de l'espace de rédaction et de recherche d'articles montre l'écran autour). Les exemples utilisent deux vrais DOI sur la même question : un téléphone posé sur le bureau nuit-il à la concentration ?

  • 10.1037/tmb0000123, une méta-analyse de 2024 (Hartanto et al.)
  • 10.1086/691462, l'étude « Brain Drain » de 2017, très citée (Ward et al.)

Le bouton View sur un résultat de recherche ouvre le texte de la méta-analyse dans l'application

Demander à OpenAlex tous les emplacements, pas seulement l'indicateur open access

OpenAlex liste chaque endroit où un article est hébergé et indique, pour chacun, s'il y a un PDF direct :

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

Pour la méta-analyse, la requête renvoie trois emplacements : le lien PDF de l'éditeur, un PDF dans le dépôt de la James Cook University et une page d'accueil dans la bibliothèque de la Singapore Management University.

Pour l'article Brain Drain, la même requête affiche "is_oa": true et "oa_status": "green", pourtant tous les pdf_url valent null. La seule copie ouverte est une page de dépôt. Donc is_oa indique qu'une version gratuite existe quelque part, pas qu'on peut la télécharger. Lisez pdf_url sur chaque emplacement.

Sans clé d'API, les requêtes OpenAlex se partagent un petit budget quotidien gratuit avec tous ceux qui utilisent votre adresse IP, et le champ meta.notice de la réponse le précise. Une clé gratuite (api_key= dans la requête) vous donne votre propre budget, et l'application en lit une dans OPENALEX_API_KEY si vous la définissez.

Vérifier les cinq premiers octets

Un pdf_url n'est encore qu'une promesse. Les éditeurs renvoient des murs de cookies, des contrôles anti-bot et des pages de connexion avec un statut 200 et un corps HTML. Le code Rust de l'application n'accepte un téléchargement que si le corps commence par le nombre magique du PDF :

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

La même idée en Python, avec la seule bibliothèque 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"]))
Enter fullscreen mode Exit fullscreen mode

Sur la méta-analyse, le lien de l'éditeur a refusé le script et la copie du dépôt est revenue sous forme d'un PDF de 1,6 Mo. Sur Brain Drain, le script échoue aussitôt avec « no pdf_url at all ». Garder la liste de ce qui a été essayé et de la raison de l'échec vaut les quelques lignes en plus ; un simple « pas de texte intégral » vous renvoie à la recherche manuelle.

La limite de quatre liens par recherche et la déduplication des URL viennent aussi de l'application. Certains articles ont une douzaine d'emplacements qui pointent tous vers le même fichier.

Le reste de la chaîne

La requête OpenAlex n'est qu'une étape. L'ordre complet de l'application :

  1. Le lien PDF fourni avec le résultat de recherche, s'il y en avait un.
  2. arXiv. Un DOI de la forme 10.48550/arXiv.<id> est un article arXiv, et https://arxiv.org/pdf/<id> est toujours gratuit.
  3. Les copies ouvertes du DOI, via Unpaywall ou OpenAlex. Unpaywall (https://api.unpaywall.org/v2/<doi>?email=<your email>) renvoie best_oa_location.url_for_pdf et une liste oa_locations, et refuse les requêtes sans véritable adresse e-mail. Les deux services viennent de la même organisation à but non lucratif, OurResearch ; l'application interroge donc Unpaywall quand UNPAYWALL_EMAIL est défini et lit sinon les emplacements d'OpenAlex, jamais les deux.
  4. CORE. Son API v3 (/v3/search/works?q=doi:"<doi>", avec une clé bearer) agrège des copies de dépôts et renvoie souvent le texte extrait directement dans fullText. L'application garde ce texte s'il dépasse 500 caractères, sinon elle essaie le downloadUrl.
  5. PubMed Central. Avec un PMCID, Europe PMC fournit l'article complet en XML JATS à l'adresse https://www.ebi.ac.uk/europepmc/webservices/rest/<PMCID>/fullTextXML. Ce n'est pas un PDF, mais les sections, figures et références y sont structurées de façon à se convertir proprement en HTML.

Les résumés arrivent sous forme d'index inversé

Un dernier détail d'OpenAlex qui piège souvent : il n'y a pas de champ abstract. On reçoit abstract_inverted_index, une table qui associe chaque mot aux positions où il apparaît, et il faut reconstruire le texte soi-même :

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

Cela suffit pour les résultats de recherche et le tri. Pour l'article Brain Drain, dont la seule copie ouverte est une page de dépôt, le résumé plus le texte intégral de la méta-analyse, c'est à peu près tout ce que permet la voie gratuite, et c'est généralement le moment de vérifier si votre bibliothèque a accès à l'article.

Top comments (0)