DEV Community

Cover image for Comment rechercher dans Google Patents depuis Python sans clé d'API

Comment rechercher dans Google Patents depuis Python sans clé d'API

Google Patents n'a pas d'API officielle. La voie payante est SerpAPI, et les API officielles gratuites ne couvrent chacune qu'un seul office, comme PatentsView de l'USPTO pour les brevets américains. Mais le site de Google Patents charge ses résultats depuis un point d'accès JSON, et ce point d'accès répond à de simples requêtes HTTP sans clé. La recherche de brevets de l'espace Paper de l'application de bureau osFoundry l'utilise depuis Rust. Voici la même requête en Python avec la seule bibliothèque standard, les filtres qu'elle accepte, l'allure de la réponse, et ce qui se passe après cinq requêtes en une demi-minute.

La requête est une query string dans une query string

Le point d'accès est https://patents.google.com/xhr/query. Il prend un seul vrai paramètre, url, dont la valeur est elle-même une query string encodée : celle que l'on voit après le ? quand on cherche sur le site. Les termes de recherche sont donc encodés deux fois.

import html, json, re, urllib.error, urllib.parse, urllib.request

UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"

def search(query, **filters):
    inner = urllib.parse.urlencode({"q": f"({query})", **filters})
    url = "https://patents.google.com/xhr/query?" + urllib.parse.urlencode({"url": inner, "exp": ""})
    req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            status, text = r.status, r.read().decode()
    except urllib.error.HTTPError as e:
        status, text = e.code, e.read().decode(errors="replace")
    if status in (429, 503) or text.lstrip().startswith("<"):
        raise RuntimeError(f"rate limited ({status})")
    return json.loads(text)["results"]
Enter fullscreen mode Exit fullscreen mode

Les parenthèses autour de la requête sont aussi celles que le site envoie. Sans user agent de navigateur, le point d'accès a plus de chances de refuser, donc l'application en envoie un, et cette fonction aussi. La dernière vérification avant l'analyse est expliquée plus bas.

Filtres

Les filtres vont dans la query string interne, à côté de q. Voici ceux que l'application laisse passer, avec le nombre de résultats pour water bottle glows reminder to drink :

paramètre interne exemple résultats
(aucun) 2757
status GRANT 685
status APPLICATION 1739
country JP 157
before / after priority:20190101 non mesuré
sort new ou old non mesuré
num / page 20 / 1 non mesuré
results = search("water bottle glows reminder to drink", status="GRANT")
print(results["total_num_results"])  # 685
Enter fullscreen mode Exit fullscreen mode

Deux choses sont faciles à mal interpréter dans ces chiffres. D'abord, 685 brevets délivrés plus 1739 demandes font 2424, pas 2757 : 333 documents ne sont ni l'un ni l'autre, et les deux filtres ne partagent pas l'ensemble. Ensuite, le total compte des documents, pas des inventions. Une même invention apparaît en général une fois comme demande publiée et une autre comme brevet délivré. « Smart container with interactive, colored lights » figure dans la liste des demandes sous US20220218137A1 et dans celle des brevets délivrés sous US12082729B2, avec le même titre et la même date de priorité, 2019-04-30. Pour compter des inventions, il faut regrouper par date de priorité et titre, ou par famille, avant de compter.

Les filtres de date se comparent à la date de priorité, celle qui compte pour l'antériorité. L'application ne laisse passer qu'une date à 8 chiffres, et seulement les valeurs exactes de pays, de statut et de tri ci-dessus, car ces chaînes sont insérées dans une URL :

/// Filter values are spliced into upstream query strings, so only the exact
/// shapes each upstream documents are passed through.
fn filter_params(filters: &PatentFilters) -> Vec<(&'static str, String)> {
Enter fullscreen mode Exit fullscreen mode

Lire la réponse

Les résultats sont imbriqués sur deux niveaux : results.cluster[].result[].patent. Chaque patent a title, snippet, priority_date, filing_date, grant_date, publication_date, inventor, assignee, publication_number, language, un chemin thumbnail et une liste de figures.

Les champs texte sont échappés en HTML, ce n'est pas du texte brut. Les titres arrivent avec une espace au début (' Liquid intake management system'), les apostrophes sous la forme &#39;, et les extraits finissent par &hellip;. Le Rust de l'application retire aussi les balises, car Google peut entourer les mots trouvés de <b>. Une expression régulière et html.unescape suffisent :

def plain(v):
    return " ".join(html.unescape(re.sub(r"<[^>]*>", "", v or "")).split())

def hits(results):
    for cluster in results["cluster"]:
        for hit in cluster["result"]:
            yield hit["patent"]

for p in hits(search("water bottle glows reminder to drink", status="GRANT")):
    print(p["publication_number"], p["priority_date"], plain(p["title"]))
Enter fullscreen mode Exit fullscreen mode
US12564280B2 2015-03-09 Wireless drink container for monitoring hydration
US12082729B2 2019-04-30 Smart container with interactive, colored lights
JP6067764B2 2013-04-03 Liquid intake management system
RU2707893C2 2014-02-28 Personal steam generator for electronic cigarette
US11581099B1 2017-05-24 Automated home system for senior care
...
Enter fullscreen mode Exit fullscreen mode

Les brevets japonais et russe reviennent avec des titres en anglais parce que Google les traduit automatiquement. Le déposant n'est pas traduit : celui de JP6067764B2 est resté en japonais. Le quatrième résultat est un générateur de vapeur pour cigarette électronique, ce qui montre à quel point la correspondance est lâche. Des mots simples ne forment pas une recherche de phrase exacte : mieux vaut lire les premiers résultats et affiner les mots que se fier au total.

Les images sont des chemins relatifs à https://patentimages.storage.googleapis.com/. La deuxième figure de US12082729B2 se trouve à e6/88/67/673b4fcbbe5404/US12082729-20240910-D00001.png, et cet hôte la sert à une requête simple :

FIG. 1A de US12082729B2, téléchargée depuis patentimages.storage.googleapis.com

La réponse contient aussi total_num_pages et un bloc summary qui compte les déposants, les inventeurs et les classes CPC par année. Les deux s'arrêtent à 1000 documents : le « Total » du résumé valait 1000 pour une recherche à 2757 résultats, et total_num_pages valait 100 avec 10 résultats par page. Sans num, une page contient 10 résultats. L'application en demande 20 et limite la valeur entre 10 et 100.

Ce qui se passe quand on en envoie trop

Un script de test a mis six requêtes en file avec 4 secondes de pause entre elles. Les quatre premières ont reçu une réponse en 2.8 à 4.5 secondes chacune. La cinquième, environ 30 secondes après la première, a renvoyé un HTTP 500 avec une page HTML. Une nouvelle tentative une minute plus tard a reçu un HTTP 503 et la page « Sorry... » de Google, comme toutes les suivantes, la dernière près de 14 minutes après le premier refus. La sixième requête n'a jamais été exécutée.

Le blocage n'est donc pas une erreur JSON que l'on attrape avec le seul code de statut. Ce peut être un 503 ou un 429, et ici le premier était même un 500, mais dans tous les cas le corps était une page HTML. C'est pourquoi la fonction ci-dessus vérifie si le corps commence par < avant de l'analyser. L'application fait de même et, quand l'utilisateur a une clé SerpAPI, elle envoie la même requête via SerpAPI au lieu d'échouer :

// The block page is HTML served as 503 (sometimes 429).
if status.as_u16() == 429 || status.as_u16() == 503 || text.trim_start().starts_with('<') {
    return Err(GoogleError::Limited);
}
Enter fullscreen mode Exit fullscreen mode

Pour un script, les règles pratiques sont : mettre en cache chaque réponse selon sa query string interne, envoyer une requête à la fois et s'arrêter à la première réponse HTML au lieu de réessayer en boucle. Le point d'accès n'est pas documenté, donc sa forme peut changer. Pour tout usage régulier ou volumineux, une source officielle (PatentsView pour les États-Unis, les Open Patent Services de l'OEB pour l'Europe, l'API d'information sur les brevets de l'Office japonais des brevets pour le Japon) ou SerpAPI est le choix le plus sûr.

L'application de bureau qui envoie cette requête est gratuite sur la page de téléchargement d'osFoundry.

Top comments (0)