dernière modification : 2024

Web scraping - aiohttp

1. Un client mono-tâche

Le module Python requests dispose de fonctions (get, post, etc.) implémentant chaque méthode HTTP.

exemple
response = requests.get('http://math.univ-angers.fr')

Ces fonctions renvoient un objet réponse composé d’attributs status_code, headers, content-type, encoding et text.

exemple
import requests

r = requests.get('http://math.univ-angers.fr')
print(f"status_code: {r.status_code}, headers: {r.headers}")
print(f"content-type: {r.headers['content-type']}")
print(f"encoding: {r.encoding}")
print(f"text: {r.text}")

2. Un client multi-tâche

Le module aiohttp offre des coroutines pour faire la même chose. En minimisant les temps d’attente, il permet de réaliser des programmes requeteurs dont le temps d’exécution est inférieur à la somme des temps d’exécution des requêtes.

Le module aiohttp offre des coroutines pour soumettre des requêtes HTTP asynchrones, ce qui est particulièrement intéressant pour le moissonnage d’informations sur un grand nombre de serveurs disparates.

Le principe est :

<1> Création d’une session

Une session permet de factoriser un contexte pour un ensemble de requêtes. Par exemple, des requêtes soumises à authentification peuvent être regroupées en une session. L’authentification (login/mot de passe) est alors fournie une seule fois (à la création de la session), et toutes les requêtes de cette session en bénéficient (sans avoir à les spécifier à chaque fois).

<2> Dans cette session, envoi d’une rafale de requêtes

Cela consiste à planifier l’exécution asynchrone d’un ensemble de requêtes GET, qui s’exécuteront indépendamment les unes des autres, chacunes à leur rythme.

Schématiquement, cela donne :

async with aiohttp.ClientSession(...) as session: (1)
  for u in urls: (2)
    async with session.get(u) as response: (3)
        print(response.status)
1 crée une session nommée session
2 pour une liste d’URLs urls…​
3 …​ soumettre une requête pour chaque URL u, et récupérer la réponse dans response

L’objet response dispose d’attributs et de méthodes pour récupérer les données de la réponse (code de retour, headers et body). Par exemple, response.status donne le code de retour HTTP, et le body peut être récupéré en asynchrone avec la coroutine text() (await response.text()). Récupérer le corps de la réponse en asynchrone est particulièrement intéressant lorsqu’il s’agit d’une ressource volumineuse (comme par exemple une vidéo).

La coroutine text() de response décode automatiquement ce qu’elle lit. Par défaut, elle suppose lire de l’UTF-8, sinon on peut lui fournir un argument encoding.

Pour ne pas avoir avoir ce décodage, et lire simplement un simple flux d’octets, on peut utiliser la coroutine read() à la place.

Malgré tout cela, si le site web interrogé met un temps infini à répondre, la coroutine va attendre indéfiniment (sans toute fois bloquer les autres). Donc le programme ne se terminera jamais. Pour éviter cela, on peut prévoir un timeout à l’aide du module (à inclure) async_timeout :

for u in urls:
  try:
    async with async_timeout.timeout(10): (1)
      async with session.get(u) as response:
      ...
  except asyncio.TimeoutError:
    print("interrompu, car trop long à répondre")
1 l’exécution du sous-bloc est interrompu au bout de 10 secondes, et une exception asyncio.TimeoutError est lancée

3. Ressources volumineuses

Les coroutines text() et read() sont à utiliser avec précaution, car elles chargent la ressource en mémoire vive. Si les ressources à récupérer sont volumineuses, il faut procéder par morceaux. L’objet réponse dispose d’un attribut content (de type aiohttp.StreamReader), offrant une coroutine read() pour récupérer un nombre d’octets déterminé (passé en argument).

4. Le module aiofiles

Si le stockage se fait sur un système distribué, il peut être intéressant d’aller plus loin en parallélisant aussi les écritures. C’est le but du module aiofile. Les fichiers de ce type s’utilisent de façon similaire aux fichiers habituels (ouverture avec open), mais disposent d’une coroutine write().

import aiofiles
import aiohttp
import asyncio
import async_timeout
import os

async def download_coroutine(session, url):
    with async_timeout.timeout(10):
        async with session.get(url) as response:
            filename = os.path.basename(url)
            async with aiofiles.open(filename, 'wb') as fd:
                while True:
                    bloc = await response.content.read(1024)
                    if not block:
                        break
                    await fd.write(bloc)
            return await response.release()