154 lines
7.3 KiB
Markdown
154 lines
7.3 KiB
Markdown
# TP : Téléchargements synchrone et asynchrone (asyncio)
|
||
|
||
## Informations générales
|
||
|
||
**Cours** : Python Avancé > Programmation concurrente > Tâches IO-bound concurrentes avec asyncio \
|
||
**Objectifs pédagogiques** :
|
||
- Python avancé : programmation asynchrone
|
||
- Outils modernes (poetry, PyCharm)
|
||
- Bonnes pratiques de l'entreprise
|
||
|
||
---
|
||
|
||
## Prérequis
|
||
|
||
### Installation et configuration de l’environnement
|
||
|
||
- Faites `poetry install` après import du projet (fichier lock déja présent), pour s'assurer que toutes les dépendances sont installées.
|
||
- Assurez-vous que l'interpréteur Python du projet pointe bien vers l'environnement virtuel créé par Poetry, ce qui devrait être le cas par défaut (sinon cliquer en bas à droite sur le nom de l'interpréteur puis `Add new interpreter`, ensuite `Add local interpreter...`, et sélectionner `poetry` avec Python `3.13`).
|
||
- Attention, si vous êtes derrière un proxy, pour ce qui est de la version asynchrone en particulier, vous allez peut-être devoir passer le paramètre `proxy="http://host:port"` à la méthode `session.get()`.
|
||
|
||
### Connaissances préalables
|
||
|
||
- Connaissances de base en programmation
|
||
|
||
---
|
||
|
||
## Énoncé
|
||
|
||
### Contexte
|
||
|
||
Le téléchargement de plusieurs fichiers depuis Internet est une tâche (IO-bound) courante en développement.
|
||
Cependant, la manière dont on s'y prend peut avoir un impact considérable sur la performance et la rapidité d'exécution d'un script.
|
||
Cet exercice a pour but de comparer deux approches fondamentales : le téléchargement séquentiel (synchrone) et le téléchargement parallèle (asynchrone).
|
||
|
||
Les deux scripts Python permettent de télécharger la même liste d'images.
|
||
Le premier le fera de manière traditionnelle, une image après l'autre.
|
||
Le second utilisera les capacités asynchrones de Python pour effectuer les téléchargements en parallèle, afin d'observer le gain de performance.
|
||
|
||
---
|
||
|
||
### Partie 1 : téléchargement synchrone
|
||
|
||
Dans cette première partie, vous allez créer un script qui télécharge une liste d'images de manière séquentielle en utilisant la bibliothèque `requests`.
|
||
|
||
```mermaid
|
||
graph TD
|
||
subgraph "Processus synchrone bloc par bloc"
|
||
direction TB
|
||
A[Début] --> B{Télécharger Image 1};
|
||
B --> C[Attente Réseau 1];
|
||
C --> D{Sauvegarder Image 1};
|
||
D --> E{Télécharger Image 2};
|
||
E --> F[Attente Réseau 2];
|
||
F --> G{Sauvegarder Image 2};
|
||
G --> H[...];
|
||
H --> I{Télécharger Image N};
|
||
I --> J[Attente Réseau N];
|
||
J --> K{Sauvegarder Image N};
|
||
K --> L[Fin];
|
||
end
|
||
```
|
||
|
||
#### Objectif
|
||
|
||
À partir du squelette de code fourni, écrire un script Python nommé `sync_downloader.py` qui :
|
||
1. Télécharge une liste d'images depuis des URLs.
|
||
2. Sauvegarde ces images dans un répertoire local.
|
||
3. Mesure et affiche le temps total d'exécution.
|
||
|
||
#### Cahier des charges
|
||
|
||
1. **Configuration de base** :
|
||
* Utilisez la liste d'URLs et le chemin du répertoire de sortie fournis ci-dessous.
|
||
* Le script doit créer le répertoire de sortie s'il n'existe pas. Utilisez le module `pathlib` pour une gestion propre des chemins.
|
||
|
||
2. **Logging** :
|
||
* Mettez en place un système de `logging` simple pour afficher des informations pertinentes dans la console (début/fin du script, URL en cours de téléchargement, succès, erreurs).
|
||
* Le format des logs doit inclure l'heure, le niveau du log et le message.
|
||
|
||
3. **Logique de téléchargement** :
|
||
* Utilisez la bibliothèque `requests`. Pour optimiser les connexions réseau, effectuez toutes vos requêtes à travers une `requests.Session`.
|
||
* Créez une fonction `download_and_save` qui prend en charge le téléchargement et la sauvegarde d'une seule image.
|
||
* Parcourez la liste d'URLs et appelez cette fonction pour chaque URL.
|
||
|
||
4. **Gestion des erreurs** :
|
||
* Votre script doit être robuste. Utilisez des blocs `try...except` pour gérer les erreurs potentielles comme les erreurs réseau (`requests.exceptions.RequestException`) ou les problèmes d'écriture sur le disque (`IOError`).
|
||
* Loguez les erreurs de manière explicite sans pour autant arrêter le script (une image qui ne se télécharge pas ne doit pas empêcher les autres de se télécharger).
|
||
|
||
5. **Mesure de performance** :
|
||
* Utilisez le module `time` (par exemple, `time.perf_counter()`) pour calculer la durée totale d'exécution du processus de téléchargement.
|
||
* Affichez le résultat à la fin du script.
|
||
|
||
---
|
||
|
||
### Partie 2 : téléchargement asynchrone
|
||
|
||
Maintenant que vous avez une version fonctionnelle, mais relativement lente, vous allez la réécrire en utilisant `asyncio` pour paralléliser les téléchargements et réduire le temps d'attente.
|
||
|
||
```mermaid
|
||
graph TD
|
||
A[Début] --> B{Lancement simultané des tâches};
|
||
|
||
subgraph "Exécution parallèle gérée par la boucle asyncio"
|
||
T1[Tâche 1: Get URL 1] --> W1[Attente Réseau 1] --> S1[Sauvegarde 1];
|
||
T2[Tâche 2: Get URL 2] --> W2[Attente Réseau 2] --> S2[Sauvegarde 2];
|
||
T3[...] --> W3[...] --> S3[...];
|
||
TN[Tâche N: Get URL N] --> WN[Attente Réseau N] --> SN[Sauvegarde N];
|
||
end
|
||
|
||
B --> T1;
|
||
B --> T2;
|
||
B --> T3;
|
||
B --> TN;
|
||
|
||
S1 --> G{Fin de toutes les tâches};
|
||
S2 --> G;
|
||
S3 --> G;
|
||
SN --> G;
|
||
|
||
G --> H[Fin du script];
|
||
```
|
||
|
||
#### Objectif
|
||
|
||
À partir du squelette de code fourni, compléter le script `async_downloader.py` qui accomplit la même tâche que le précédent, mais de manière asynchrone pour une performance maximale.
|
||
|
||
#### Cahier des charges
|
||
|
||
1. **Structure asynchrone** :
|
||
* Transformez votre logique en utilisant `async` et `await`. La fonction principale (`main`) et la fonction de téléchargement (`download_and_save`) doivent devenir des coroutines (`async def`).
|
||
* Utilisez `asyncio.run(main())` pour démarrer l'exécution.
|
||
|
||
2. **Logique de téléchargement asynchrone** :
|
||
* Remplacez `requests.Session` par `aiohttp.ClientSession`.
|
||
* Dans votre fonction `download_and_save`, utilisez `session.get()` pour la requête et `response.read()` pour obtenir le contenu binaire de l'image.
|
||
* Pour l'écriture des fichiers, remplacez `open()` par `aiofiles.open()` pour ne pas bloquer la boucle événementielle.
|
||
|
||
3. **Exécution en parallèle** :
|
||
* Dans votre fonction `main`, créez une liste de "tâches" (une pour chaque image à télécharger).
|
||
* Utilisez `asyncio.gather()` pour lancer toutes les tâches en parallèle et attendre qu'elles soient toutes terminées.
|
||
|
||
4. **Gestion des erreurs et logging** :
|
||
* Adaptez la gestion des erreurs pour les exceptions spécifiques à `aiohttp` (comme `aiohttp.ClientError`).
|
||
* Conservez le système de `logging`. Notez que dans un contexte asynchrone, les messages de log des différentes tâches peuvent s'entremêler, ce qui est normal.
|
||
|
||
5. **Mesure de performance** :
|
||
* Mesurez à nouveau le temps d'exécution avec `time.perf_counter()`.
|
||
* Comparez ce temps avec celui obtenu dans la Partie 1. Qu'observez-vous ?
|
||
* Si vous avez le temps, essayez de télécharger un plus grand nombre d'images, provenant éventuellement de serveurs HTTP différents, pour voir l'impact de l'asynchronisme.
|
||
|
||
#### Question de réflexion
|
||
|
||
Pourquoi l'approche asynchrone est-elle efficace pour ce type de tâche (opérations d'entrée/sortie réseau) par rapport à l'approche synchrone ?
|