Comment télécharger de gros volumes de données via un proxy et ne pas tout recommencer après une coupure
Sommaire de l'article
- Introduction : pourquoi un long téléchargement est presque toujours interrompu, et pourquoi c'est normal
- Préparation initiale : outils, accès et environnement
- Concepts de base : le vocabulaire du téléchargement robuste en termes simples
- Étape 1 : reprise http via l'en-tête range
- Étape 2 : points de contrôle pour les téléchargements paginés
- Étape 3 : idempotence, pour qu'une reprise ne crée pas de doublons
- Étape 4 : déduplication des résultats sans gonfler la mémoire
- Étape 5 : parallélisme sans pertes
- Étape 6 : reprise après une longue pause
- Étape 7 : structure prête à l'emploi d'un téléchargeur robuste en python
- Vérification du résultat : liste de contrôle d'un téléchargement robuste
- Erreurs courantes et leurs solutions
- Fonctionnalités supplémentaires et optimisation
- Faq : questions fréquentes sur le téléchargement robuste
- Conclusion : ce que vous savez faire maintenant et où aller ensuite
Introduction : pourquoi un long téléchargement est presque toujours interrompu, et pourquoi c'est normal
Si vous avez déjà lancé un gros téléchargement de données, vous connaissez cette sensation. Le processus a tourné pendant des heures, atteint quatre-vingt-dix pour cent et s'est interrompu. La connexion a lâché, le serveur a renvoyé une erreur, l'ordinateur portable s'est mis en veille. Et il faut tout recommencer. Ce guide est écrit pour que cela ne se reproduise plus.
Ce que vous obtiendrez au final. Vous apprendrez à construire un téléchargeur qui survit aux coupures. Il reprend les fichiers là où ils se sont arrêtés, se souvient de la page atteinte, ne crée pas de doublons en cas de reprise et peut redémarrer même après une longue pause. Vous obtiendrez une structure Python prête à adapter à votre cas d'usage.
À qui s'adresse ce guide. Aux ingénieurs, analystes et développeurs qui téléchargent des données depuis des API, récupèrent de gros fichiers ou collectent des résultats paginés via un proxy. Niveau intermédiaire. Vous devez comprendre les bases de HTTP et savoir lire du code Python. Aucune connaissance approfondie en programmation réseau n'est requise.
Ce qu'il faut savoir au préalable. Python de base, la notion de requête et de réponse HTTP, ce que sont les en-têtes et les codes de statut. Si vous avez déjà utilisé la bibliothèque requests, c'est suffisant.
Combien de temps cela prendra. La lecture et la compréhension des concepts prendront environ quarante minutes. La construction d'un téléchargeur fonctionnel à partir de notre structure prendra une à trois heures, selon votre source de données.
Précision importante sur le sujet. Nous n'aborderons pas les codes de statut comme 429 ni les stratégies de retry avec délai (backoff). Il y a un article dédié à cela. Ici, l'accent est mis sur une seule chose : l'état du processus et sa reprise. Comment sauvegarder la progression, comment ne pas perdre ni dupliquer les données, comment continuer là où on s'est arrêté.
Conseil : Gardez à portée de main un carnet ou un fichier séparé où vous noterez les paramètres de votre source : prend-elle en charge la reprise, y a-t-il une pagination, quel est son format de curseur. Ces notes serviront à chaque étape.
Préparation initiale : outils, accès et environnement
Avant d'écrire du code, mettons en place un environnement de travail. Cela prendra dix minutes mais économisera des heures de débogage.
Ce qu'il faut installer
- Installez Python version 3.10 ou supérieure. Vérifiez la version avec la commande
python --versiondans le terminal. - Créez un environnement virtuel avec la commande
python -m venv venv, pour que les dépendances du projet ne se mélangent pas avec celles du système. - Activez l'environnement. Sur Windows, c'est
venv\Scripts\activate, sur macOS et Linux, c'estsource venv/bin/activate. - Installez la bibliothèque pour les requêtes HTTP avec
pip install requests. - Pour un travail plus rapide avec la base d'état, rien de plus à installer : le module
sqlite3fait déjà partie de la bibliothèque standard de Python.
Ce qu'il faut comme accès
- L'accès à votre source de données : URL, token ou clé API si nécessaire.
- Un proxy Proxeon avec adresse, port et identifiants d'authentification. Sans proxy stable, un téléchargement robuste n'a pas de sens, car c'est le proxy qui répartit la charge et rend les connexions prévisibles.
- De l'espace disque pour le fichier d'état et pour les données téléchargées.
Vérification du proxy Proxeon
- Prenez la chaîne de connexion de la forme
http://login:motdepasse@adresse:port. - Testez-la avec une requête simple. Dans le terminal, exécutez
curl -x http://login:motdepasse@adresse:port https://api.ipify.orget assurez-vous que c'est l'adresse IP du proxy qui est renvoyée, et non la vôtre.
Conseil : Enregistrez la chaîne de connexion au proxy dans une variable d'environnement, pas dans le code. Ainsi, vous n'enverrez pas accidentellement le mot de passe dans le système de contrôle de version. Dans le code, lisez-la via os.environ.
⚠️ Attention : Travaillez toujours uniquement avec les sources de données auxquelles vous avez légalement accès. Respectez les conditions d'utilisation du service et les limites indiquées par son propriétaire. Le proxy Proxeon est destiné à un travail d'ingénierie légal : répartition de la charge, stabilité des connexions et téléchargement correct des données.
✅ Vérification : L'environnement est prêt si la commande python -c "import requests, sqlite3" s'est exécutée sans erreur et si la requête via le proxy a renvoyé l'adresse du serveur proxy.
Concepts de base : le vocabulaire du téléchargement robuste en termes simples
Avant d'écrire du code, passons en revue les termes clés. Sans eux, les étapes suivantes ressembleront à des incantations.
Reprise
La reprise est la continuation du téléchargement d'un fichier à partir de l'octet où il s'est interrompu. Au lieu de retélécharger le fichier depuis le début, vous demandez au serveur de ne renvoyer que le morceau manquant. Cela fonctionne via l'en-tête HTTP Range.
Point de contrôle
Un point de contrôle est un point de progression sauvegardé. Imaginez une sauvegarde dans un jeu vidéo. Si quelque chose se passe mal, vous revenez à la dernière sauvegarde, pas au début du jeu. Dans un téléchargement, le point de contrôle stocke la page ou l'enregistrement où vous vous êtes arrêté.
Curseur
Un curseur est une marque que l'API vous donne pour vous permettre de demander la portion suivante de données. C'est souvent une chaîne comme eyJvZmZzZXQiOjEwMH0. Vous la renvoyez, et le serveur comprend où reprendre.
Idempotence
L'idempotence est la propriété d'une opération dont la répétition ne change pas le résultat. Si vous écrivez deux fois la même ligne avec la même clé, au final il n'y a qu'une ligne, pas deux. C'est une protection contre les doublons lors des reprises.
Déduplication
La déduplication consiste à éliminer les enregistrements en double. Même en travaillant proprement, le même objet peut arriver deux fois. La déduplication garantit qu'il n'en restera qu'un dans votre ensemble final.
Principe fondamental
Un téléchargeur robuste repose sur une seule idée : il faut sauvegarder la progression en permanence, pas seulement à la fin. N'importe quelle étape peut être la dernière avant une coupure. Donc, après chaque morceau de travail réussi, l'état doit être écrit sur le disque. La reprise consiste alors simplement à lire l'état et à continuer.
Conseil : Retenez la règle des trois questions pour tout téléchargement. Première : où me suis-je arrêté ? Deuxième : comment ne pas dupliquer ce qui a déjà été reçu ? Troisième : qu'est-ce qui va expirer pendant mon absence ? Les réponses à ces questions constituent la robustesse.
Étape 1 : Reprise HTTP via l'en-tête Range
Objectif de l'étape. Apprendre à télécharger un gros fichier de manière à reprendre après une coupure à partir de l'octet non téléchargé, et non depuis zéro.
Comment ça marche
HTTP permet de demander non pas tout le fichier, mais une partie. Pour cela, on ajoute l'en-tête Range à la requête. Par exemple, Range: bytes=1048576- signifie : donne-moi tout à partir de l'octet numéro 1048576. Mais il faut d'abord s'assurer que le serveur en est capable.
- Envoyez une requête HEAD ou un GET classique sur le fichier et regardez les en-têtes de réponse.
- Cherchez l'en-tête
Accept-Ranges. Si sa valeur estbytes, le serveur prend en charge la reprise. - S'il n'y a pas d'en-tête ou si la valeur est
none, la reprise est impossible. Dans ce cas, il faudra télécharger le fichier entier en une seule fois ou chercher une source alternative.
Vérification de la prise en charge de la reprise
Voici le code qui vérifie si le serveur sait renvoyer des parties du fichier.
import requests
def supports_resume(url, proxies):
resp = requests.head(url, proxies=proxies, timeout=30, allow_redirects=True)
accept = resp.headers.get("Accept-Ranges", "none")
total = resp.headers.get("Content-Length")
return accept.lower() == "bytes", totalReprise d'un fichier depuis le milieu
Maintenant le code principal. Il regarde combien d'octets ont déjà été téléchargés localement et demande au serveur uniquement le reste.
import os
import requests
def download_resumable(url, dest, proxies):
already = 0
if os.path.exists(dest):
already = os.path.getsize(dest)
headers = {}
if already > 0:
headers["Range"] = f"bytes={already}-"
mode = "ab" if already > 0 else "wb"
with requests.get(url, headers=headers, proxies=proxies,
stream=True, timeout=60) as r:
if already > 0 and r.status_code == 200:
mode = "wb"
already = 0
with open(dest, mode) as f:
for chunk in r.iter_content(chunk_size=65536):
if chunk:
f.write(chunk)
return os.path.getsize(dest)Examinons les points importants. Si le serveur renvoie le statut 206, il a honnêtement renvoyé une partie du fichier et l'ajout se passera correctement. Si le serveur renvoie 200 malgré l'en-tête Range, cela signifie qu'il a ignoré la reprise et renvoie le fichier entier. Dans ce cas, nous passons en mode réécriture complète pour ne pas coller l'ancien morceau avec le nouveau et corrompre le fichier.
⚠️ Attention : N'ajoutez jamais de données en mode ab si vous n'êtes pas sûr que le serveur a répondu avec le code 206. Sinon, vous obtiendrez un fichier corrompu, où le début est le reste de la tentative précédente et la suite est le nouveau fichier complet. Un tel fichier s'ouvrira avec une erreur, et vous perdrez du temps à chercher la cause.
Conseil : Ne téléchargez pas directement dans le fichier cible, mais dans un fichier temporaire avec l'extension .part. Une fois le téléchargement entièrement terminé, renommez-le avec le nom final. Ainsi, vous ne confondrez jamais un fichier terminé avec un fichier partiellement téléchargé.
Contrôle d'intégrité
Après le téléchargement complet, il est bon de vérifier que le fichier n'est pas corrompu. Si le serveur a envoyé l'en-tête Content-Length, comparez-le avec la taille réelle du fichier sur le disque. Si les tailles correspondent, le fichier est arrivé entier.
def verify_size(dest, expected):
if expected is None:
return True
return os.path.getsize(dest) == int(expected)✅ Vérification : Interrompez le téléchargement à mi-chemin en fermant le programme. Relancez-le. Dans les logs, vous devez voir que la requête est partie avec l'en-tête Range et que le fichier a été complété, et non recommencé. La taille finale correspond à celle attendue.
Étape 2 : Points de contrôle pour les téléchargements paginés
Objectif de l'étape. Configurer la sauvegarde de la progression pour les API qui renvoient des données par pages, afin de reprendre à la bonne page après une coupure.
Ce qu'il faut sauvegarder exactement
Un fichier se reprend octet par octet, mais un téléchargement paginé suit une toute autre logique. Ici, il n'y a pas d'octets, il y a des pages et des enregistrements. Donc, dans le point de contrôle, il faut stocker autre chose.
- Le curseur si l'API fonctionne avec des curseurs. C'est l'option la plus fiable, car le curseur sait lui-même où reprendre.
- Le numéro de page ou l'offset si l'API fonctionne avec offset et limit. Stockez le numéro de la dernière page traitée avec succès.
- L'identifiant du dernier enregistrement s'il est possible de trier par ID ou date croissante. La requête suivante demandera alors les enregistrements avec un ID supérieur à celui sauvegardé.
- Le compteur d'enregistrements traités pour le contrôle et les rapports.
Où stocker l'état
Vous avez trois options principales, de la plus simple à la plus robuste.
- Fichier JSON. Le plus simple. Vous écrivez un dictionnaire avec le curseur et le compteur dans un fichier après chaque page. Convient aux téléchargements uniques, non parallèles.
- Base SQLite. Plus robuste. Elle offre des transactions, donc l'état ne se corrompra pas en cas de coupure au moment de l'écriture. Idéal quand les données sont nombreuses et qu'il faut dédupliquer.
- Base de données externe. Pour les gros téléchargements distribués, quand plusieurs processus se partagent le travail.
Sauvegarde du point de contrôle en JSON
import json
import os
def save_checkpoint(path, cursor, page, last_id, count):
tmp = path + ".tmp"
data = {
"cursor": cursor,
"page": page,
"last_id": last_id,
"count": count,
}
with open(tmp, "w") as f:
json.dump(data, f)
os.replace(tmp, path)
def load_checkpoint(path):
if not os.path.exists(path):
return {"cursor": None, "page": 0, "last_id": None, "count": 0}
with open(path) as f:
return json.load(f)Remarquez l'astuce du fichier temporaire. Nous écrivons dans un fichier avec le suffixe .tmp, puis nous le renommons atomiquement via os.replace. Cela protège contre la situation où le programme plante juste pendant l'écriture du point de contrôle. L'ancien point de contrôle reste alors intact, au lieu de se transformer en un demi-JSON illisible.
⚠️ Attention : N'écrivez jamais le point de contrôle directement dans le même fichier par-dessus l'ancien sans fichier temporaire. Une coupure au milieu de l'écriture laissera un point de contrôle corrompu, et la reprise deviendra impossible. Le remplacement atomique résout complètement ce problème.
Conseil : Ne sauvegardez le point de contrôle qu'après que les données de la page ont réellement été écrites dans le stockage. L'ordre est le suivant : recevoir la page, écrire les données, puis mettre à jour le point de contrôle. Si vous inversez l'ordre, en cas de coupure vous sauterez une page et perdrez des données.
Boucle principale avec points de contrôle
def paginate(fetch_page, save_data, cp_path, proxies):
cp = load_checkpoint(cp_path)
cursor = cp["cursor"]
count = cp["count"]
while True:
items, next_cursor = fetch_page(cursor, proxies)
if not items:
break
save_data(items)
count += len(items)
last_id = items[-1].get("id")
save_checkpoint(cp_path, next_cursor, cp["page"] + 1,
last_id, count)
cursor = next_cursor
if next_cursor is None:
break
return count✅ Vérification : Lancez le téléchargement, laissez-le traiter quelques pages, interrompez-le. Ouvrez le fichier de point de contrôle et assurez-vous qu'il contient le curseur et le compteur actuels. Relancez : le téléchargement doit reprendre à partir du curseur sauvegardé, et non depuis la première page.
Étape 3 : Idempotence, pour qu'une reprise ne crée pas de doublons
Objectif de l'étape. Faire en sorte qu'un relancement ou qu'une répétition d'une requête spécifique n'entraîne pas l'apparition d'enregistrements identiques dans votre stockage.
Pourquoi les doublons apparaissent
Imaginez : vous avez reçu une page de données, vous l'avez écrite dans un fichier, mais le programme a planté avant que le point de contrôle ne soit mis à jour. Au prochain lancement, vous demanderez à nouveau la même page. Les données reviendront et seront écrites une seconde fois. C'est ainsi que naissent les doublons. C'est une conséquence inévitable des coupures, et il faut la combattre au niveau de l'architecture.
Clé de déduplication
L'outil principal de l'idempotence est la clé de déduplication. C'est un champ ou une combinaison de champs qui identifient de manière unique un enregistrement. Le bon choix de clé résout la moitié du problème.
- ID naturel. Si l'enregistrement possède un identifiant unique fourni par la source, utilisez-le. C'est la clé idéale.
- Combinaison de champs. S'il n'y a pas d'ID unique, composez une clé à partir de plusieurs champs stables. Par exemple, email plus date d'inscription.
- Hash du contenu. S'il n'y a aucun champ stable, calculez un hash de l'enregistrement entier. C'est l'option de dernier recours, car toute modification d'un champ créera une nouvelle clé.
Écriture sans doublons via UPSERT
Si vous stockez le résultat dans SQLite ou une autre base, utilisez l'insertion avec ignorance du conflit. Alors, une réécriture avec la même clé ne fera simplement rien.
import sqlite3
def init_db(path):
conn = sqlite3.connect(path)
conn.execute(
"CREATE TABLE IF NOT EXISTS records ("
"dedup_key TEXT PRIMARY KEY, payload TEXT)"
)
conn.commit()
return conn
def save_records(conn, items):
rows = [(item["id"], json.dumps(item)) for item in items]
conn.executemany(
"INSERT OR IGNORE INTO records (dedup_key, payload) "
"VALUES (?, ?)", rows
)
conn.commit()Le détail clé ici est la PRIMARY KEY sur le champ dedup_key. La base rejettera elle-même une réinsertion avec la même clé, car INSERT OR IGNORE avalera le conflit silencieusement. Vous n'avez pas besoin de vérifier manuellement si un tel enregistrement existe déjà. La base le fait pour vous, et rapidement.
Conseil : Choisissez la clé de déduplication une fois pour toutes au début du projet et fixez-la dans la documentation. Changer de clé en cours de téléchargement signifie que les anciens et les nouveaux enregistrements ne correspondront plus, et des doublons apparaîtront quand même. La stabilité de la clé importe plus que son élégance.
✅ Vérification : Lancez le téléchargement deux fois de suite sur la même plage de données. Comptez le nombre de lignes dans la base avec la commande SELECT COUNT(*) FROM records. Le nombre doit être identique après le premier et après le second lancement.
Étape 4 : Déduplication des résultats sans gonfler la mémoire
Objectif de l'étape. Filtrer les enregistrements en double sur des millions de lignes, sans charger toute la mémoire de l'ordinateur avec un ensemble de clés déjà vues.
Approche naïve et son problème
La déduplication la plus simple : garder en mémoire un ensemble set de toutes les clés vues. Pour chaque nouvel enregistrement, vérifier si la clé est dans l'ensemble. Cela fonctionne parfaitement sur des centaines de milliers de lignes. Mais sur des millions et des dizaines de millions, l'ensemble grossit et consomme des gigaoctets de mémoire vive. Le programme ralentit ou plante.
Première solution : faire confiance à la base
La méthode la plus simple et la plus fiable sur de gros volumes est de ne pas stocker du tout ce qui a été vu en mémoire, mais de confier la vérification à la base via la PRIMARY KEY, comme nous l'avons fait à l'étape précédente. La base stocke l'index sur le disque, pas dans la mémoire de votre processus. Elle gérera des dizaines de millions de clés sans solliciter votre mémoire vive.
Deuxième solution : hash de l'enregistrement
Quand il n'y a pas de clé naturelle, calculez un hash compact de l'enregistrement. Le hash occupe un volume fixe et réduit, indépendamment de la taille de l'enregistrement lui-même.
import hashlib
import json
def record_hash(item):
raw = json.dumps(item, sort_keys=True, ensure_ascii=False)
return hashlib.sha256(raw.encode("utf-8")).hexdigest()Le paramètre sort_keys=True est critique ici. Il garantit que des enregistrements au contenu identique produiront le même hash, même si les champs y apparaissent dans un ordre différent. Sans ce tri, deux objets identiques pourraient obtenir des hash différents et passer pour des enregistrements distincts.
Troisième solution : filtre de Bloom pour économiser la mémoire
Si vous avez tout de même besoin d'une vérification rapide en mémoire sur d'énormes volumes, on utilise un filtre de Bloom. C'est une structure qui occupe peu de place et répond rapidement si l'on a vu la clé ou si on ne l'a certainement pas vue. Elle a une particularité : elle peut rarement dire à tort qu'une clé existait déjà, alors qu'elle n'existait pas. C'est pourquoi le filtre de Bloom est utilisé comme pré-filtrage rapide, et la vérification finale est laissée à la base.
- On vérifie la clé avec le filtre de Bloom.
- Si le filtre dit qu'on ne l'a certainement pas vue, on écrit immédiatement dans la base.
- Si le filtre dit qu'on l'a peut-être vue, on fait une vérification précise dans la base.
⚠️ Attention : N'essayez pas de dédupliquer des dizaines de millions de lignes avec un simple ensemble en mémoire. Sur un ordinateur portable typique, cela entraînera un épuisement de la mémoire et un crash du processus au milieu du téléchargement. Déportez la charge sur le disque via la base ou utilisez un filtre de Bloom.
Conseil : Si vous téléchargez des données par lots et que des doublons sont possibles au sein d'un même lot, dédupliquez le lot en mémoire avec un ensemble classique avant de l'écrire dans la base. Le lot est petit, la mémoire ne souffrira pas, et moins d'insertions inutiles iront vers la base.
✅ Vérification : Lancez la déduplication sur un grand jeu de test avec des répétitions intentionnelles. Vérifiez que le nombre final d'enregistrements uniques est correct et que la consommation mémoire du processus reste stable et ne croît pas linéairement avec le nombre de lignes.
Étape 5 : Parallélisme sans pertes
Objectif de l'étape. Accélérer le téléchargement grâce à des requêtes parallèles, sans perdre aucune tâche et en relançant correctement celles qui ont échoué.
File de tâches
La base d'un parallélisme sûr est une file de tâches. Vous découpez à l'avance le travail en morceaux indépendants. Par exemple, une liste de pages ou de plages. Vous les mettez dans une file. Plusieurs workers prennent des tâches dans la file, les exécutent et déposent le résultat. Si un worker plante, sa tâche peut être remise dans la file et confiée à un autre.
Limitation de la simultanéité
On ne peut pas lancer un nombre infini de requêtes parallèles. Cela surchargerait la source et votre proxy. La bonne approche est de limiter le nombre de workers simultanés à une valeur raisonnable. Commencez par un petit nombre et augmentez en observant la stabilité.
from concurrent.futures import ThreadPoolExecutor, as_completed
def run_parallel(tasks, worker, proxies, max_workers=5):
results = []
failed = []
with ThreadPoolExecutor(max_workers=max_workers) as pool:
future_map = {
pool.submit(worker, t, proxies): t for t in tasks
}
for future in as_completed(future_map):
task = future_map[future]
try:
results.append(future.result())
except Exception:
failed.append(task)
return results, failedReprise des tâches échouées
La liste failed collectée n'est pas une donnée perdue, mais la liste de ce qu'il faut répéter. Après le premier passage, vous relancez les tâches échouées. En général, cela suffit pour terminer le reste.
def run_with_retry(tasks, worker, proxies, rounds=3):
remaining = tasks
for _ in range(rounds):
done, remaining = run_parallel(remaining, worker, proxies)
if not remaining:
break
return remainingLe rôle du proxy Proxeon dans le parallélisme. En travail parallèle, le proxy répartit les connexions, ce qui rend le téléchargement plus stable et plus prévisible. Chaque worker travaille via sa propre connexion, et la charge ne se concentre pas en un seul point.
⚠️ Attention : En écriture parallèle dans un même fichier ou un même point de contrôle, des conditions de course apparaissent. Deux workers peuvent écraser l'état l'un de l'autre. N'écrivez les résultats que dans une base avec transactions, ou utilisez un fichier séparé par worker, et assemblez le point de contrôle consolidé dans un thread séparé.
Conseil : Faites des tâches petites et indépendantes. Si une tâche couvre une plage trop grande, sa coupure fera perdre beaucoup de travail. Les petites tâches se répètent à moindre coût et de manière presque invisible.
✅ Vérification : Lancez un téléchargement parallèle, faites planter artificiellement une partie des workers. Après les tours de reprise, la liste remaining doit être vide et l'ensemble final de données complet. Comparez le nombre d'enregistrements obtenus avec celui attendu.
Étape 6 : Reprise après une longue pause
Objectif de l'étape. Reprendre correctement un téléchargement si beaucoup de temps s'est écoulé entre les tentatives, et comprendre ce qui a pu expirer pendant cette période.
Ce qui expire avec le temps
Une coupure d'une minute et une pause d'un jour sont des situations différentes. Après une longue pause, une partie de votre état peut devenir invalide.
- La session. De nombreux services gardent une session pendant une durée limitée. Après une longue pause, le serveur l'oubliera et les requêtes commenceront à renvoyer une erreur d'autorisation.
- Le token d'accès. Les tokens API ont souvent une durée de vie de quelques minutes ou heures. Un token expiré doit être rafraîchi avant de continuer.
- Le curseur. Certains curseurs vivent peu de temps. Si le curseur a expiré, il faudra repartir du point stable le plus proche, par exemple l'identifiant du dernier enregistrement.
- Les données elles-mêmes. Pendant la pause, de nouveaux enregistrements ont pu apparaître dans la source ou d'anciens ont pu changer. Cela affecte les offsets lors d'une pagination par offset.
Stratégie de reprise sûre
- Au démarrage, vérifiez l'âge du point de contrôle. S'il est ancien, préparez-vous à ce qu'une partie de l'état soit obsolète.
- Rafraîchissez le token d'accès et créez une nouvelle session avant la première requête. Ne vous fiez pas aux anciens.
- Privilégiez la reprise par identifiant du dernier enregistrement plutôt que par numéro de page. L'ID est stable, tandis que le numéro de page se décale si les données changent.
- Faites une requête d'essai avec le curseur sauvegardé. Si elle renvoie une erreur de curseur invalide, passez à la reprise par last_id.
def resume(cp, fetch_by_id, fetch_by_cursor, proxies):
if cp["cursor"]:
try:
return fetch_by_cursor(cp["cursor"], proxies)
except CursorExpired:
pass
return fetch_by_id(cp["last_id"], proxies)Pourquoi la reprise par ID est plus fiable. Imaginez que vous vous êtes arrêté à la page 50 en triant par date. Pendant votre absence, de nouveaux enregistrements se sont ajoutés au début. Maintenant, la page 50 contient des données complètement différentes, et vous sauterez une partie des enregistrements. La reprise par identifiant du dernier enregistrement n'en souffre pas : vous demandez simplement tout ce qui est supérieur à l'ID sauvegardé.
Conseil : Sauvegardez toujours dans le point de contrôle à la fois le curseur et l'identifiant du dernier enregistrement. Le curseur est plus rapide, mais l'ID est votre corde de sécurité au cas où le curseur expirerait pendant une longue pause.
✅ Vérification : Arrêtez le téléchargement, attendez suffisamment longtemps pour que le token ou le curseur expirent, puis relancez. Le téléchargeur doit rafraîchir le token, détecter le curseur expiré et continuer par identifiant sans perte ni duplication d'enregistrements.
Étape 7 : Structure prête à l'emploi d'un téléchargeur robuste en Python
Objectif de l'étape. Rassembler tout ce qui a été étudié dans un cadre fonctionnel unique, que vous adapterez à votre source de données.
Ci-dessous se trouve un cadre qui combine les points de contrôle, la déduplication via la base, le rafraîchissement du token et la reprise. Vous remplacez les fonctions de récupération de page par les vôtres, adaptées à votre API.
import os
import json
import sqlite3
import requests
class ResilientLoader:
def __init__(self, cp_path, db_path, proxies):
self.cp_path = cp_path
self.proxies = proxies
self.conn = sqlite3.connect(db_path)
self.conn.execute(
"CREATE TABLE IF NOT EXISTS records ("
"dedup_key TEXT PRIMARY KEY, payload TEXT)"
)
self.conn.commit()
def load_cp(self):
if not os.path.exists(self.cp_path):
return {"cursor": None, "last_id": None, "count": 0}
with open(self.cp_path) as f:
return json.load(f)
def save_cp(self, cp):
tmp = self.cp_path + ".tmp"
with open(tmp, "w") as f:
json.dump(cp, f)
os.replace(tmp, self.cp_path)
def save_records(self, items):
rows = [(str(i["id"]), json.dumps(i)) for i in items]
self.conn.executemany(
"INSERT OR IGNORE INTO records "
"(dedup_key, payload) VALUES (?, ?)", rows
)
self.conn.commit()
def run(self, fetch_page):
cp = self.load_cp()
while True:
items, next_cursor = fetch_page(
cp["cursor"], cp["last_id"], self.proxies
)
if not items:
break
self.save_records(items)
cp["count"] += len(items)
cp["last_id"] = items[-1]["id"]
cp["cursor"] = next_cursor
self.save_cp(cp)
if next_cursor is None:
break
return cp["count"]Exemple de fonction de récupération de page pour votre source. Ici, vous implémentez la logique de requête et l'analyse de la réponse.
def fetch_page(cursor, last_id, proxies):
params = {"limit": 100}
if cursor:
params["cursor"] = cursor
elif last_id:
params["after_id"] = last_id
r = requests.get(
"https://example-source/api/records",
params=params, proxies=proxies, timeout=60
)
r.raise_for_status()
data = r.json()
return data["items"], data.get("next_cursor")Le lancement de tout le mécanisme est simple.
proxies = {
"http": os.environ["PROXEON_URL"],
"https": os.environ["PROXEON_URL"],
}
loader = ResilientLoader("state.json", "out.db", proxies)
total = loader.run(fetch_page)
print("Всего записей:", total)Conseil : Ajoutez au cycle la journalisation de chaque centaine d'enregistrements : heure, compteur, curseur actuel. Ainsi, vous verrez la progression et comprendrez facilement si le téléchargement est bloqué au même endroit.
✅ Vérification : Lancez le cadre sur une source réelle, interrompez-le à mi-chemin, relancez-le. Le nombre final d'enregistrements après reprise correspondra au nombre total d'enregistrements de la source, et un second lancement n'augmentera pas le compteur de lignes uniques.
Vérification du résultat : liste de contrôle d'un téléchargement robuste
Passez en revue cette liste. Si tous les points sont respectés, votre téléchargeur est véritablement robuste.
- La reprise de fichier continue à partir de l'octet non téléchargé, et non depuis zéro.
- Le téléchargeur gère correctement le cas où le serveur ignore l'en-tête Range.
- Le point de contrôle est sauvegardé après chaque page traitée, et non seulement à la fin.
- Le point de contrôle est écrit atomiquement via un fichier temporaire et un renommage.
- Un relancement ne crée pas de doublons dans l'ensemble final.
- La déduplication ne croît pas en mémoire linéairement avec le nombre de lignes.
- Les workers parallèles ne perdent pas les tâches échouées et les répètent.
- Après une longue pause, le token est rafraîchi et un curseur expiré est remplacé par une reprise par ID.
Comment tester
- Lancez un téléchargement complet d'un petit ensemble et notez le nombre d'enregistrements.
- Relancez-le sur le même ensemble et vérifiez que le nombre n'a pas changé.
- Interrompez le téléchargement à différents endroits : au début, au milieu, vers la fin.
- Après chaque interruption, relancez et vérifiez que le résultat est identique.
Indicateurs de succès. Le nombre d'enregistrements uniques est stable entre les lancements. La consommation mémoire ne croît pas de manière incontrôlée. La reprise continue toujours à partir du point sauvegardé. Il n'y a pas de fichiers corrompus après la reprise.
Erreurs courantes et leurs solutions
Problème : le fichier ne s'ouvre pas après la reprise. Cause : les données ont été ajoutées en mode ab, alors que le serveur a répondu avec le code 200 et renvoyé le fichier entier. Solution : vérifiez le statut de la réponse, en cas de 200 passez à une réécriture complète du fichier depuis zéro.
Problème : après une coupure, le téléchargement repart de la première page. Cause : le point de contrôle n'était sauvegardé qu'à la fin du travail, ou pas du tout. Solution : sauvegardez le point de contrôle après chaque page traitée, juste après l'écriture des données.
Problème : le point de contrôle est illisible, le JSON est corrompu. Cause : le programme a planté pendant l'écriture directement dans le fichier cible. Solution : écrivez dans un fichier temporaire et remplacez atomiquement via os.replace.
Problème : des doublons dans l'ensemble final. Cause : pas de clé de déduplication ou clé instable. Solution : définissez une PRIMARY KEY sur une clé fiable et utilisez INSERT OR IGNORE.
Problème : le processus plante par manque de mémoire sur de gros volumes. Cause : toutes les clés vues sont gardées dans un ensemble en mémoire. Solution : déportez la vérification d'unicité vers la base ou utilisez un filtre de Bloom.
Problème : après une longue pause, les requêtes renvoient une erreur d'autorisation. Cause : le token ou la session ont expiré pendant la pause. Solution : rafraîchissez le token et créez une nouvelle session à chaque démarrage.
Problème : après une pause, une partie des enregistrements est sautée ou dupliquée. Cause : la reprise s'est faite par numéro de page, alors que les données de la source ont changé. Solution : reprenez par identifiant du dernier enregistrement, pas par offset.
Problème : les workers parallèles perdent une partie des données. Cause : plusieurs workers écrivaient dans un même point de contrôle et s'écrasaient mutuellement. Solution : écrivez les résultats dans une base avec transactions, et non dans un fichier d'état commun.
Fonctionnalités supplémentaires et optimisation
Écriture par lots
N'écrivez pas dans la base ligne par ligne. Rassemblez un lot de plusieurs centaines d'enregistrements et insérez-les d'un coup via executemany. Cela accélère considérablement l'écriture sur de gros volumes.
Validation périodique
Appelez commit non pas à chaque écriture, mais toutes les quelques centaines de lignes. Un commit trop fréquent ralentit la base. Un commit trop rare risque de perdre plus de données en cas de coupure. Trouvez l'équilibre adapté à votre charge.
Rapport de progression
Ajoutez une estimation du temps restant. En connaissant la vitesse de traitement des pages et le nombre total d'enregistrements, vous estimerez combien de temps attendre encore. C'est pratique pour les longs téléchargements.
Stockages séparés des données brutes et traitées
Stockez les réponses brutes séparément des enregistrements analysés. Si vous modifiez plus tard la logique d'analyse, vous n'aurez pas à retélécharger les données. Il suffira de repasser les réponses brutes dans le nouveau parseur.
Conseil : Configurez le proxy Proxeon pour que les connexions soient stables tout au long du téléchargement. Une connexion stable réduit le nombre de coupures, donc votre téléchargeur passe moins souvent en reprise et travaille plus vite.
FAQ : questions fréquentes sur le téléchargement robuste
Comment savoir si le serveur prend en charge la reprise de fichier ? Envoyez une requête HEAD et regardez l'en-tête Accept-Ranges. La valeur bytes indique la prise en charge. L'absence d'en-tête ou none signifie que la reprise est impossible.
Que faire si l'API ne fournit pas de curseur, seulement des pages ? Sauvegardez le numéro de page et, si possible, l'identifiant du dernier enregistrement. Reprenez de préférence par ID, car les numéros de page se décalent quand les données changent.
À quelle fréquence sauvegarder le point de contrôle ? Après chaque page traitée et écrite avec succès. Ainsi, en cas de coupure, vous perdez au maximum une page de travail, et non tout le téléchargement.
Peut-on dédupliquer sans base de données ? Sur de petits volumes, oui, avec un ensemble en mémoire. Sur des millions de lignes, c'est dangereux pour la mémoire. Mieux vaut utiliser une base avec PRIMARY KEY ou un filtre de Bloom.
Que choisir comme clé de déduplication ? L'ID unique naturel de la source, s'il existe. Sinon, une combinaison de champs stables. En dernier recours, un hash de l'enregistrement entier avec tri des clés.
Pourquoi la reprise par ID est-elle plus fiable que par numéro de page ? Parce que les données de la source peuvent changer. De nouveaux enregistrements décalent les pages, et par numéro vous sauterez ou dupliquerez des données. L'ID n'en dépend pas.
Combien de workers parallèles faut-il mettre ? Commencez par un petit nombre et augmentez en observant la stabilité et les limites de la source. Un parallélisme excessif nuit plus qu'il n'aide.
Comment stocker la chaîne de connexion au proxy en toute sécurité ? Dans une variable d'environnement, pas dans le code. Lisez-la via os.environ. Ainsi, le mot de passe ne finira pas dans le système de contrôle de version.
Que faire si le curseur a expiré pendant une longue pause ? Capturez l'erreur de curseur invalide et passez à la reprise par l'identifiant du dernier enregistrement sauvegardé.
Faut-il vérifier l'intégrité du fichier téléchargé ? Oui. Comparez la taille réelle du fichier avec l'en-tête Content-Length. Si le serveur fournit une somme de contrôle, vérifiez-la aussi.
Conclusion : ce que vous savez faire maintenant et où aller ensuite
Vous avez parcouru le chemin depuis un téléchargement fragile, qui s'effondre à la première coupure, jusqu'à un téléchargeur robuste. Vous disposez désormais de tous les outils pour qu'une coupure cesse d'être une catastrophe et devienne une situation de travail ordinaire.
Ce que vous avez maîtrisé. La reprise de fichiers via l'en-tête Range avec vérification d'Accept-Ranges. La sauvegarde de la progression via des points de contrôle atomiques. L'idempotence au niveau de la clé de déduplication. La déduplication sur des millions de lignes sans gonfler la mémoire. Le parallélisme avec reprise des tâches échouées. La reprise après une longue pause avec rafraîchissement des tokens et remplacement des curseurs expirés. Et surtout, un cadre Python prêt à l'emploi qui réunit tout cela.
Que faire ensuite. Prenez votre source de données réelle et adaptez-y la fonction de récupération de page. Commencez par un petit volume, déboguez la reprise sur les interruptions, puis montez en échelle. Configurez un proxy Proxeon stable pour que les connexions soient prévisibles tout au long du téléchargement.
Vers quoi évoluer. Approfondissez l'écriture par lots et les filtres de Bloom. Ajoutez la surveillance de la progression et l'estimation du temps. Séparez le stockage des données brutes et traitées. Progressivement