2015-08-22 11 views
7

Ho difficoltà a completare la libreria Asyncio di Python 3. Ho un elenco di codici di avviamento postale e sto cercando di effettuare chiamate asincrone a un'API per ottenere la città e lo stato di ciascun codice postale corrispondente. Posso farlo con successo in sequenza con un ciclo for, ma voglio renderlo più veloce nel caso di una grande lista di codici di avviamento postale.Esecuzione di più chiamate con asyncio e aggiunta di risultati a un dizionario

Questo è un esempio di mio originale che funziona

import urllib.request, json 

zips = ['90210', '60647'] 

def get_cities(zipcodes): 
    zip_cities = dict() 
    for idx, zipcode in enumerate(zipcodes): 
     url = 'http://maps.googleapis.com/maps/api/geocode/json?address='+zipcode+'&sensor=true' 
     response = urllib.request.urlopen(url) 
     string = response.read().decode('utf-8') 
     data = json.loads(string) 
     city = data['results'][0]['address_components'][1]['long_name'] 
     state = data['results'][0]['address_components'][3]['long_name'] 
     zip_cities.update({idx: [zipcode, city, state]}) 
    return zip_cities 

results = get_cities(zips) 
print(results) 
# returns {0: ['90210', 'Beverly Hills', 'California'], 
#   1: ['60647', 'Chicago', 'Illinois']} 

Questo è il mio terribile tentativo non funzionale a cercare di rendere asincrona

import asyncio 
import urllib.request, json 

zips = ['90210', '60647'] 
zip_cities = dict() 

@asyncio.coroutine 
def get_cities(zipcodes): 
    url = 'http://maps.googleapis.com/maps/api/geocode/json?address='+zipcode+'&sensor=true' 
    response = urllib.request.urlopen(url) 
    string = response.read().decode('utf-8') 
    data = json.loads(string) 
    city = data['results'][0]['address_components'][1]['long_name'] 
    state = data['results'][0]['address_components'][3]['long_name'] 
    zip_cities.update({idx: [zipcode, city, state]}) 

loop = asyncio.get_event_loop() 
loop.run_until_complete([get_cities(zip) for zip in zips]) 
loop.close() 
print(zip_cities) # doesnt work 

Qualsiasi aiuto è molto apprezzato. Tutti i tutorial che ho trovato online sembrano essere un po 'troppo in testa.

Nota: ho visto alcuni esempi utilizzare aiohttp. Speravo di rimanere con le librerie native Python 3 se possibile.

risposta

7

Non è possibile ottenere alcuna concorrenza se si utilizza urllib per eseguire la richiesta HTTP, perché è una libreria sincrona. Il wrapping della funzione che chiama in urllib in un coroutine non lo modifica. È necessario utilizzare un client HTTP asincrono che è integrato in asyncio, come aiohttp:

import asyncio 
import json 
import aiohttp 

zips = ['90210', '60647'] 
zip_cities = dict() 

@asyncio.coroutine 
def get_cities(zipcode,idx): 
    url = 'https://maps.googleapis.com/maps/api/geocode/json?key=abcdfg&address='+zipcode+'&sensor=true' 
    response = yield from aiohttp.request('get', url) 
    string = (yield from response.read()).decode('utf-8') 
    data = json.loads(string) 
    print(data) 
    city = data['results'][0]['address_components'][1]['long_name'] 
    state = data['results'][0]['address_components'][3]['long_name'] 
    zip_cities.update({idx: [zipcode, city, state]}) 

if __name__ == "__main__":   
    loop = asyncio.get_event_loop() 
    tasks = [asyncio.async(get_cities(z, i)) for i, z in enumerate(zips)] 
    loop.run_until_complete(asyncio.wait(tasks)) 
    loop.close() 
    print(zip_cities) 

So che si preferisce utilizzare solo lo stdlib, ma la biblioteca asyncio non include un client HTTP, in modo da dovreste avere fondamentalmente ri-implementare pezzi di aiohttp per ricreare la funzionalità che fornisce. Suppongo che un'altra opzione sarebbe quella di rendere le chiamate urllib in un thread in background, in modo che non blocchino il ciclo degli eventi, ma il suo tipo di sciocchezza da fare quando aiohttp è disponibile (e sorta di sconfitte lo scopo di usare asyncio nel primo posto):

import asyncio 
import json 
import urllib.request 
from concurrent.futures import ThreadPoolExecutor 

zips = ['90210', '60647'] 
zip_cities = dict() 

@asyncio.coroutine 
def get_cities(zipcode,idx): 
    url = 'https://maps.googleapis.com/maps/api/geocode/json?key=abcdfg&address='+zipcode+'&sensor=true' 
    response = yield from loop.run_in_executor(executor, urllib.request.urlopen, url) 
    string = response.read().decode('utf-8') 
    data = json.loads(string) 
    print(data) 
    city = data['results'][0]['address_components'][1]['long_name'] 
    state = data['results'][0]['address_components'][3]['long_name'] 
    zip_cities.update({idx: [zipcode, city, state]}) 

if __name__ == "__main__": 
    executor = ThreadPoolExecutor(10) 
    loop = asyncio.get_event_loop() 
    tasks = [asyncio.async(get_cities(z, i)) for i, z in enumerate(zips)] 
    loop.run_until_complete(asyncio.wait(tasks)) 
    loop.close() 
    print(zip_cities) 
+0

Grazie per questo. Il modo 'aiohttp' è molto più pulito. –

+2

@ anthony-dandrea Se la tua lista di codici postali sarà ENORME, ti consiglio anche di limitare il numero di richieste simultanee a un numero sano come 100 connessioni o così, altrimenti potresti finire per essere bloccato. * Non * parlando per esperienza; -] –

3
Non

fatto molto con asyncio ma asyncio.get_event_loop() dovrebbe essere quello che è necessario, è anche necessario, ovviamente, di cambiare ciò che la vostra funzione prende come argomenti e utilizzare asyncio.wait(tasks) come da docs:

zips = ['90210', '60647'] 
zip_cities = dict() 

@asyncio.coroutine 
def get_cities(zipcode): 
    url = 'https://maps.googleapis.com/maps/api/geocode/json?key=abcdefg&address='+zipcode+'&sensor=true' 
    fut = loop.run_in_executor(None,urllib.request.urlopen, url) 
    response = yield from fut 
    string = response.read().decode('utf-8') 
    data = json.loads(string) 
    city = data['results'][0]['address_components'][1]['long_name'] 
    state = data['results'][0]['address_components'][3]['long_name'] 
    zip_cities.update({idx: [zipcode, city, state]}) 

loop = asyncio.get_event_loop() 
tasks = [asyncio.async(get_cities(z, i)) for i, z in enumerate(zips)] 
loop.run_until_complete(asyncio.wait(tasks)) 
loop.close() 
print(zip_cities) # doesnt work 
{0: ['90210', 'Beverly Hills', 'California'], 1: ['60647', 'Chicago', 'Illinois']} 

non ho> = 3.4.4 così ho dovuto usare asyncio.async invece di asyncio.ensure_future

o modificare la logica e creare il dict da task.result dai compiti:

@asyncio.coroutine 
def get_cities(zipcode): 
    url = 'https://maps.googleapis.com/maps/api/geocode/json?key=abcdefg&address='+zipcode+'&sensor=true' 
    fut = loop.run_in_executor(None,urllib.request.urlopen, url) 
    response = yield from fut 
    string = response.read().decode('utf-8') 
    data = json.loads(string) 
    city = data['results'][0]['address_components'][1]['long_name'] 
    state = data['results'][0]['address_components'][3]['long_name'] 
    return [zipcode, city, state] 

loop = asyncio.get_event_loop() 
tasks = [asyncio.async(get_cities(z)) for z in zips] 
loop.run_until_complete(asyncio.wait(tasks)) 
loop.close() 
zip_cities = {i:tsk.result() for i,tsk in enumerate(tasks)} 
print(zip_cities) 
{0: ['90210', 'Beverly Hills', 'California'], 1: ['60647', 'Chicago', 'Illinois']} 

Se stai guardando moduli esterni c'è anche un port of requests che funziona con asyncio.

+1

Questo è ancora sincrono, perché 'urllib.request.urlopen' bloccherà il ciclo degli eventi. – dano

+0

@dano, sì, ho trascurato questo, non un grande cambiamento per farlo funzionare. –

Problemi correlati