web-dev-qa-db-fra.com

Python async / attendre le téléchargement d'une liste d'URL

J'essaie de télécharger plus de 30 000 fichiers à partir d'un serveur FTP, et après quelques recherches sur Google en utilisant asynchrone IO semblait une bonne idée. Cependant, le code ci-dessous n'a pas pu télécharger de fichiers et renvoie une erreur de délai d'attente J'apprécierais vraiment toute aide! Merci!

class pdb:
    def __init__(self):
        self.ids = []
        self.dl_id = []
        self.err_id = []


    async def download_file(self, session, url):
        try:
            with async_timeout.timeout(10):
                async with session.get(url) as remotefile:
                    if remotefile.status == 200:
                        data = await remotefile.read()
                        return {"error": "", "data": data}
                    else:
                        return {"error": remotefile.status, "data": ""}
        except Exception as e:
            return {"error": e, "data": ""}

    async def unzip(self, session, work_queue):
        while not work_queue.empty():
            queue_url = await work_queue.get()
            print(queue_url)
            data = await self.download_file(session, queue_url)
            id = queue_url[-11:-7]
            ID = id.upper()
            if not data["error"]:
                saved_pdb = os.path.join("./pdb", ID, f'{ID}.pdb')
                if ID not in self.dl_id:
                    self.dl_id.append(ID)
                with open(f"{id}.ent.gz", 'wb') as f:
                    f.write(data["data"].read())
                with gzip.open(f"{id}.ent.gz", "rb") as inFile, open(saved_pdb, "wb") as outFile:
                    shutil.copyfileobj(inFile, outFile)
                os.remove(f"{id}.ent.gz")
            else:
                self.err_id.append(ID)

    def download_queue(self, urls):
        loop = asyncio.get_event_loop()
        q = asyncio.Queue(loop=loop)
        [q.put_nowait(url) for url in urls]
        con = aiohttp.TCPConnector(limit=10)
        with aiohttp.ClientSession(loop=loop, connector=con) as session:
            tasks = [asyncio.ensure_future(self.unzip(session, q)) for _ in range(len(urls))]
            loop.run_until_complete(asyncio.gather(*tasks))
        loop.close()

Message d'erreur si je supprime la partie try:

Traceback (dernier appel le plus récent):
Fichier "test.py", ligne 111, dans
x.download_queue (urls)
Fichier "test.py", ligne 99, dans download_queue
loop.run_until_complete (asyncio.gather (* tâches))
Fichier "/home/yz/miniconda3/lib/python3.6/asyncio/base_events.py", ligne 467, dans run_until_complete
return future.result ()
Fichier "test.py", ligne 73, en décompressant
data = attendre self.download_file (session, queue_url)
Fichier "test.py", ligne 65, dans download_file
return {"error": remotefile.status, "data": ""}
Fichier "/home/yz/miniconda3/lib/python3.6/site- packages/async_timeout/init. Py", ligne 46, dans exit
soulève asyncio.TimeoutError de None
concurrent.futures._base.TimeoutError

10
Yi Zhou
tasks = [asyncio.ensure_future(self.unzip(session, q)) for _ in range(len(urls))]
loop.run_until_complete(asyncio.gather(*tasks))

Ici, vous démarrez le processus de téléchargement simultané pour toutes vos URL. Cela signifie que vous commencez également à compter le délai d'attente pour chacun d'eux. Une fois que c'est un grand nombre tel que 30 000, cela ne peut pas être fait physiquement dans les 10 secondes en raison de la capacité des réseaux/ram/cpu.

Pour éviter cette situation, vous devez garantir la limite des coroutines démarrées simultanément. Généralement, des primitives de synchronisation telles que asyncio.Semaphore peuvent être utilisées pour y parvenir.

Cela ressemblera à ceci:

sem = asyncio.Semaphore(10)

# ...

async def download_file(self, session, url):
    try:
        async with sem:  # Don't start next download until 10 other currently running
            with async_timeout.timeout(10):
11
Mikhail Gerasimov

Comme alternative à l'approche sémaphore de @ MikhailGerasimov, vous pourriez envisager d'utiliser l'opérateur aiostream.stream.map :

from aiostream import stream, pipe

async def main(urls):
    async with aiohttp.ClientSession() as session:
        ws = stream.repeat(session)
        xs = stream.Zip(ws, stream.iterate(urls))
        ys = stream.starmap(xs, fetch, ordered=False, task_limit=10)
        zs = stream.map(ys, process)
        await zs

Voici une implémentation équivalente utilisant des canaux:

async def main3(urls):
    async with aiohttp.ClientSession() as session:
        await (stream.repeat(session)
               | pipe.Zip(stream.iterate(urls))
               | pipe.starmap(fetch, ordered=False, task_limit=10)
               | pipe.map(process))

Vous pouvez le tester avec les coroutines suivantes:

async def fetch(session, url):
    await asyncio.sleep(random.random())
    return url

async def process(data):
    print(data)

Voir plus d'exemples d'aiostream dans ce démonstration et le documentation .

Avertissement: je suis le responsable du projet.

2
Vincent