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.
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.
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 Pour ne pas avoir avoir ce décodage, et lire simplement un simple flux d’octets,
on peut utiliser la coroutine |
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()