¿Cómo puedo acelerar la búsqueda de páginas con urllib2 en python?

Tengo un script que busca varias páginas web y analiza la información.

(Se puede ver un ejemplo en http://bluedevilbooks.com/search/?DEPT=MATH&CLASS=103&SEC=01 )

Ejecuté cProfile en él, y como asumí, urlopen toma mucho tiempo. ¿Hay una manera de recuperar las páginas más rápido? ¿O una forma de obtener varias páginas a la vez? Haré lo que sea más simple, ya que soy nuevo en Python y en el desarrollo web.

¡Gracias por adelantado! 🙂

ACTUALIZACIÓN: Tengo una función llamada fetchURLs() , que uso para hacer una matriz de las URL que necesito, así que algo como urls = fetchURLS() . Las URL son todos los archivos XML de las API de Amazon y eBay (lo que me confunde por qué) tarda tanto en cargar, tal vez mi webhost es lento?)

Lo que debo hacer es cargar cada URL, leer cada página y enviar esos datos a otra parte de la secuencia de comandos que analizará y mostrará los datos.

Tenga en cuenta que no puedo hacer la última parte hasta que TODAS las páginas hayan sido recuperadas, ese es mi problema.

Además, creo que mi host me limita a 25 procesos a la vez, así que lo que sea más fácil en el servidor sería bueno 🙂


Aquí está por tiempo:

 Sun Aug 15 20:51:22 2010 prof 211352 function calls (209292 primitive calls) in 22.254 CPU seconds Ordered by: internal time List reduced from 404 to 10 due to restriction  ncalls tottime percall cumtime percall filename:lineno(function) 10 18.056 1.806 18.056 1.806 {_socket.getaddrinfo} 4991 2.730 0.001 2.730 0.001 {method 'recv' of '_socket.socket' objects} 10 0.490 0.049 0.490 0.049 {method 'connect' of '_socket.socket' objects} 2415 0.079 0.000 0.079 0.000 {method 'translate' of 'unicode' objects} 12 0.061 0.005 0.745 0.062 /usr/local/lib/python2.6/HTMLParser.py:132(goahead) 3428 0.060 0.000 0.202 0.000 /usr/local/lib/python2.6/site-packages/BeautifulSoup.py:1306(endData) 1698 0.055 0.000 0.068 0.000 /usr/local/lib/python2.6/site-packages/BeautifulSoup.py:1351(_smartPop) 4125 0.053 0.000 0.056 0.000 /usr/local/lib/python2.6/site-packages/BeautifulSoup.py:118(setup) 1698 0.042 0.000 0.358 0.000 /usr/local/lib/python2.6/HTMLParser.py:224(parse_starttag) 1698 0.042 0.000 0.275 0.000 /usr/local/lib/python2.6/site-packages/BeautifulSoup.py:1397(unknown_starttag) 

EDITAR : Estoy ampliando la respuesta para incluir un ejemplo más pulido. He encontrado mucha hostilidad y desinformación en esta publicación con respecto a los subprocesos vs E / S asíncronas. Por lo tanto, también estoy agregando más argumentos para refutar cierta reclamación no válida. Espero que esto ayude a las personas a elegir la herramienta adecuada para el trabajo correcto.

Esto es un dup a una pregunta hace 3 días.

Python urllib2.open es lento, necesita una mejor manera de leer varias direcciones URL – Stack Overflow Python urllib2.urlopen () es lento, necesita una mejor manera de leer varias direcciones URL

Estoy puliendo el código para mostrar cómo obtener múltiples páginas web en paralelo utilizando subprocesos.

 import time import threading import Queue # utility - spawn a thread to execute target for each args def run_parallel_in_threads(target, args_list): result = Queue.Queue() # wrapper to collect return value in a Queue def task_wrapper(*args): result.put(target(*args)) threads = [threading.Thread(target=task_wrapper, args=args) for args in args_list] for t in threads: t.start() for t in threads: t.join() return result def dummy_task(n): for i in xrange(n): time.sleep(0.1) return n # below is the application code urls = [ ('http://www.google.com/',), ('http://www.lycos.com/',), ('http://www.bing.com/',), ('http://www.altavista.com/',), ('http://achewood.com/',), ] def fetch(url): return urllib2.urlopen(url).read() run_parallel_in_threads(fetch, urls) 

Como puede ver, el código específico de la aplicación tiene solo 3 líneas, que se pueden contraer en 1 línea si es agresivo. No creo que nadie pueda justificar su afirmación de que esto es complejo e inigualable.

Desafortunadamente, la mayoría de los otros códigos de subprocesamiento publicados aquí tienen algunos defectos Muchos de ellos realizan un sondeo activo para esperar a que el código termine. join() es una mejor manera de sincronizar el código. Creo que este código ha mejorado en todos los ejemplos de subprocesos hasta ahora.

conexión para mantener vivo

La sugerencia de WoLpH sobre el uso de la conexión keep-alive podría ser muy útil si todas las URL apuntan al mismo servidor.

retorcido

Aaron Gallagher es un fanático del marco twisted y es hostil a cualquier persona que sugiera un hilo. Desafortunadamente muchas de sus afirmaciones son desinformación. Por ejemplo, dijo “-1 para sugerir hilos. Esto está enlazado a IO; los hilos son inútiles aquí”. Esto es contrario a la evidencia, ya que tanto Nick T como yo hemos demostrado una ganancia de velocidad del hilo de uso. De hecho, la aplicación enlazada de E / S tiene más que ganar con el uso del subproceso de Python (en comparación con ninguna ganancia en la aplicación vinculada a la CPU). La crítica equivocada de Aaron en el hilo muestra que está bastante confundido acerca de la progtwigción paralela en general.

Herramienta correcta para el trabajo correcto

Soy muy consciente de los problemas relacionados con la progtwigción paralela que utiliza subprocesos, python, E / S asíncrona, etc. Cada herramienta tiene sus pros y sus contras. Para cada situación hay una herramienta adecuada. No estoy en contra de los retorcidos (aunque no he desplegado uno por mi cuenta). Pero no creo que podamos decir abiertamente que el hilo es MALO y torcido es BUENO en todas las situaciones.

Por ejemplo, si el requisito del OP es obtener 10.000 sitios web en paralelo, será preferible la E / S asíncrona. El subprocesamiento no será apropiado (a menos que con Python sin astackmiento).

La oposición de Aaron a los hilos son en su mayoría generalizaciones. No reconoce que esta es una tarea de paralelización trivial. Cada tarea es independiente y no comparte recursos. Así que la mayor parte de su ataque no se aplica.

Dado que mi código no tiene dependencia externa, lo llamaré la herramienta correcta para el trabajo correcto.

Actuación

Creo que la mayoría de la gente estaría de acuerdo en que el rendimiento de esta tarea depende en gran medida del código de red y del servidor externo, donde el rendimiento del código de la plataforma debería tener un efecto insignificante. Sin embargo, el punto de referencia de Aaron muestra una ganancia de velocidad del 50% sobre el código roscado. Creo que es necesario responder a esta aparente ganancia de velocidad.

En el código de Nick, hay una falla obvia que causó la ineficiencia. Pero, ¿cómo explica la ganancia de velocidad de 233 ms sobre mi código? Creo que incluso los fanáticos retorcidos se abstendrán de saltar a una conclusión para atribuir esto a la eficiencia de retorcido. Después de todo, existe una gran cantidad de variables fuera del código del sistema, como el rendimiento del servidor remoto, la red, el almacenamiento en caché y la implementación de diferencias entre urllib2 y el cliente web torcido, etc.

Solo para asegurar que los subprocesos de Python no incurran en una gran cantidad de ineficiencia, hago un punto de referencia rápido para generar 5 subprocesos y luego 500 subprocesos. Me siento bastante cómodo al decir que la sobrecarga de generar 5 hilos es insignificante y no puedo explicar la diferencia de velocidad de 233 ms.

 In [274]: %time run_parallel_in_threads(dummy_task, [(0,)]*5) CPU times: user 0.00 s, sys: 0.00 s, total: 0.00 s Wall time: 0.00 s Out[275]:  In [276]: %time run_parallel_in_threads(dummy_task, [(0,)]*500) CPU times: user 0.16 s, sys: 0.00 s, total: 0.16 s Wall time: 0.16 s In [278]: %time run_parallel_in_threads(dummy_task, [(10,)]*500) CPU times: user 1.13 s, sys: 0.00 s, total: 1.13 s Wall time: 1.13 s <<<<<<<< This means 0.13s of overhead 

Pruebas adicionales en mi búsqueda paralela muestran una gran variabilidad en el tiempo de respuesta en 17 ejecuciones. (Desafortunadamente no me he torcido para verificar el código de Aaron).

 0.75 s 0.38 s 0.59 s 0.38 s 0.62 s 1.50 s 0.49 s 0.36 s 0.95 s 0.43 s 0.61 s 0.81 s 0.46 s 1.21 s 2.87 s 1.04 s 1.72 s 

Mi prueba no es compatible con la conclusión de Aaron de que el subproceso es sistemáticamente más lento que la E / S asíncrona por un margen medible. Dado el número de variables involucradas, debo decir que esto no es una prueba válida para medir la diferencia de rendimiento sistemático entre E / S asíncronas y subprocesos.

Utilice torcido ! Hace que este tipo de cosas sea absurdamente fácil en comparación con, digamos, usar hilos.

 from twisted.internet import defer, reactor from twisted.web.client import getPage import time def processPage(page, url): # do somewthing here. return url, len(page) def printResults(result): for success, value in result: if success: print 'Success:', value else: print 'Failure:', value.getErrorMessage() def printDelta(_, start): delta = time.time() - start print 'ran in %0.3fs' % (delta,) return delta urls = [ 'http://www.google.com/', 'http://www.lycos.com/', 'http://www.bing.com/', 'http://www.altavista.com/', 'http://achewood.com/', ] def fetchURLs(): callbacks = [] for url in urls: d = getPage(url) d.addCallback(processPage, url) callbacks.append(d) callbacks = defer.DeferredList(callbacks) callbacks.addCallback(printResults) return callbacks @defer.inlineCallbacks def main(): times = [] for x in xrange(5): d = fetchURLs() d.addCallback(printDelta, time.time()) times.append((yield d)) print 'avg time: %0.3fs' % (sum(times) / len(times),) reactor.callWhenRunning(main) reactor.run() 

Este código también funciona mejor que cualquiera de las otras soluciones publicadas (editado después de que cerré algunas cosas que usaban mucho ancho de banda):

 Success: ('http://www.google.com/', 8135) Success: ('http://www.lycos.com/', 29996) Success: ('http://www.bing.com/', 28611) Success: ('http://www.altavista.com/', 8378) Success: ('http://achewood.com/', 15043) ran in 0.518s Success: ('http://www.google.com/', 8135) Success: ('http://www.lycos.com/', 30349) Success: ('http://www.bing.com/', 28611) Success: ('http://www.altavista.com/', 8378) Success: ('http://achewood.com/', 15043) ran in 0.461s Success: ('http://www.google.com/', 8135) Success: ('http://www.lycos.com/', 30033) Success: ('http://www.bing.com/', 28611) Success: ('http://www.altavista.com/', 8378) Success: ('http://achewood.com/', 15043) ran in 0.435s Success: ('http://www.google.com/', 8117) Success: ('http://www.lycos.com/', 30349) Success: ('http://www.bing.com/', 28611) Success: ('http://www.altavista.com/', 8378) Success: ('http://achewood.com/', 15043) ran in 0.449s Success: ('http://www.google.com/', 8135) Success: ('http://www.lycos.com/', 30349) Success: ('http://www.bing.com/', 28611) Success: ('http://www.altavista.com/', 8378) Success: ('http://achewood.com/', 15043) ran in 0.547s avg time: 0.482s 

Y usando el código de Nick T, preparado para dar también el promedio de cinco y mostrar mejor la salida:

 Starting threaded reads: ...took 1.921520 seconds ([8117, 30070, 15043, 8386, 28611]) Starting threaded reads: ...took 1.779461 seconds ([8135, 15043, 8386, 30349, 28611]) Starting threaded reads: ...took 1.756968 seconds ([8135, 8386, 15043, 30349, 28611]) Starting threaded reads: ...took 1.762956 seconds ([8386, 8135, 15043, 29996, 28611]) Starting threaded reads: ...took 1.654377 seconds ([8117, 30349, 15043, 8386, 28611]) avg time: 1.775s Starting sequential reads: ...took 1.389803 seconds ([8135, 30147, 28611, 8386, 15043]) Starting sequential reads: ...took 1.457451 seconds ([8135, 30051, 28611, 8386, 15043]) Starting sequential reads: ...took 1.432214 seconds ([8135, 29996, 28611, 8386, 15043]) Starting sequential reads: ...took 1.447866 seconds ([8117, 30028, 28611, 8386, 15043]) Starting sequential reads: ...took 1.468946 seconds ([8153, 30051, 28611, 8386, 15043]) avg time: 1.439s 

Y usando el código de Wai Yip Tung:

 Fetched 8117 from http://www.google.com/ Fetched 28611 from http://www.bing.com/ Fetched 8386 from http://www.altavista.com/ Fetched 30051 from http://www.lycos.com/ Fetched 15043 from http://achewood.com/ done in 0.704s Fetched 8117 from http://www.google.com/ Fetched 28611 from http://www.bing.com/ Fetched 8386 from http://www.altavista.com/ Fetched 30114 from http://www.lycos.com/ Fetched 15043 from http://achewood.com/ done in 0.845s Fetched 8153 from http://www.google.com/ Fetched 28611 from http://www.bing.com/ Fetched 8386 from http://www.altavista.com/ Fetched 30070 from http://www.lycos.com/ Fetched 15043 from http://achewood.com/ done in 0.689s Fetched 8117 from http://www.google.com/ Fetched 28611 from http://www.bing.com/ Fetched 8386 from http://www.altavista.com/ Fetched 30114 from http://www.lycos.com/ Fetched 15043 from http://achewood.com/ done in 0.647s Fetched 8135 from http://www.google.com/ Fetched 28611 from http://www.bing.com/ Fetched 8386 from http://www.altavista.com/ Fetched 30349 from http://www.lycos.com/ Fetched 15043 from http://achewood.com/ done in 0.693s avg time: 0.715s 

Tengo que decir, me gusta que las búsquedas secuenciales funcionaron mejor para mí.

Aquí hay un ejemplo usando Threads python. Los otros ejemplos de subprocesos aquí inician un subproceso por url, que no es un comportamiento muy amigable si causa demasiados hits para que el servidor los maneje (por ejemplo, es común que las arañas tengan muchas urls en el mismo host)

 from threading import Thread from urllib2 import urlopen from time import time, sleep WORKERS=1 urls = ['http://docs.python.org/library/threading.html', 'http://docs.python.org/library/thread.html', 'http://docs.python.org/library/multiprocessing.html', 'http://docs.python.org/howto/urllib2.html']*10 results = [] class Worker(Thread): def run(self): while urls: url = urls.pop() results.append((url, urlopen(url).read())) start = time() threads = [Worker() for i in range(WORKERS)] any(t.start() for t in threads) while len(results)<40: sleep(0.1) print time()-start 

Nota: Los tiempos dados aquí son para 40 direcciones URL y dependerán mucho de la velocidad de su conexión a Internet y de la latencia del servidor. Estando en Australia, mi ping es> 300ms

Con WORKERS=1 tardó 86 segundos en ejecutarse
Con WORKERS=4 tomó 23 segundos correr
con WORKERS=10 tardó 10 segundos en ejecutarse

por lo tanto, la descarga de 10 subprocesos es 8.6 veces más rápida que un solo subproceso.

Aquí hay una versión actualizada que utiliza una cola. Hay al menos un par de ventajas.
1. Los urls se solicitan en el orden en que aparecen en la lista.
2. Puede usar q.join() para detectar cuándo se han completado todas las solicitudes
3. Los resultados se mantienen en el mismo orden que la lista de URL.

 from threading import Thread from urllib2 import urlopen from time import time, sleep from Queue import Queue WORKERS=10 urls = ['http://docs.python.org/library/threading.html', 'http://docs.python.org/library/thread.html', 'http://docs.python.org/library/multiprocessing.html', 'http://docs.python.org/howto/urllib2.html']*10 results = [None]*len(urls) def worker(): while True: i, url = q.get() # print "requesting ", i, url # if you want to see what's going on results[i]=urlopen(url).read() q.task_done() start = time() q = Queue() for i in range(WORKERS): t=Thread(target=worker) t.daemon = True t.start() for i,url in enumerate(urls): q.put((i,url)) q.join() print time()-start 

La espera real probablemente no urllib2 en urllib2 sino en el servidor y / o su conexión de red al servidor.

Hay 2 maneras de acelerar esto.

  1. Mantenga la conexión activa (consulte esta pregunta sobre cómo hacerlo: Python urllib2 con keep alive )
  2. Use conexiones de multiplicar, puede usar hilos o un enfoque asíncrono como sugirió Aaron Gallagher. Para eso, simplemente use cualquier ejemplo de subprocesos y debería hacerlo bien 🙂 También puede usar la biblioteca de multiprocessing para hacer las cosas bastante fáciles.

La mayoría de las respuestas se centraron en obtener varias páginas de diferentes servidores al mismo tiempo (subprocesos) pero no en reutilizar la conexión HTTP ya abierta. Si OP está haciendo una solicitud múltiple al mismo servidor / sitio.

En urlib2, se crea una conexión independiente con cada solicitud, lo que afecta el rendimiento y, como resultado, una tasa más lenta de recuperación de páginas. urllib3 resuelve este problema utilizando un conjunto de conexiones. Puede leer más aquí urllib3 [También seguro para subprocesos]

También hay Solicitudes de una biblioteca HTTP que utiliza urllib3

Esto combinado con el enhebrado debería boost la velocidad de búsqueda de páginas.

Hoy en día hay una excelente libra de Python que hace esto para sus llamadas solicitudes .

Utilice la API estándar de solicitudes si desea una solución basada en subprocesos o una API asíncrona (utilizando gevent bajo el capó) si desea una solución basada en IO no bloqueante.

Desde que se publicó esta pregunta, parece que hay una abstracción de nivel superior disponible, ThreadPoolExecutor :

https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor-example

El ejemplo de allí pegado aquí por conveniencia:

 import concurrent.futures import urllib.request URLS = ['http://www.foxnews.com/', 'http://www.cnn.com/', 'http://europe.wsj.com/', 'http://www.bbc.co.uk/', 'http://some-made-up-domain.com/'] # Retrieve a single page and report the url and contents def load_url(url, timeout): with urllib.request.urlopen(url, timeout=timeout) as conn: return conn.read() # We can use a with statement to ensure threads are cleaned up promptly with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: # Start the load operations and mark each future with its URL future_to_url = {executor.submit(load_url, url, 60): url for url in URLS} for future in concurrent.futures.as_completed(future_to_url): url = future_to_url[future] try: data = future.result() except Exception as exc: print('%r generated an exception: %s' % (url, exc)) else: print('%r page is %d bytes' % (url, len(data))) 

También hay un map que creo que facilita el código: https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.map

Ray ofrece una manera elegante de hacer esto (tanto en Python 2 como en Python 3). Ray es una biblioteca para escribir Python paralela y distribuida.

Simplemente defina la función fetch con el decorador @ray.remote . Luego puede obtener una URL en segundo plano llamando a fetch.remote(url) .

 import ray import sys ray.init() @ray.remote def fetch(url): if sys.version_info >= (3, 0): import urllib.request return urllib.request.urlopen(url).read() else: import urllib2 return urllib2.urlopen(url).read() urls = ['https://en.wikipedia.org/wiki/Donald_Trump', 'https://en.wikipedia.org/wiki/Barack_Obama', 'https://en.wikipedia.org/wiki/George_W._Bush', 'https://en.wikipedia.org/wiki/Bill_Clinton', 'https://en.wikipedia.org/wiki/George_H._W._Bush'] # Fetch the webpages in parallel. results = ray.get([fetch.remote(url) for url in urls]) 

Si también desea procesar las páginas web en paralelo, puede poner el código de procesamiento directamente a la vista o puede definir una nueva función remota y componerlas juntas.

 @ray.remote def process(html): tokens = html.split() return set(tokens) # Fetch and process the pages in parallel. results = [] for url in urls: results.append(process.remote(fetch.remote(url))) results = ray.get(results) 

Si tiene una lista muy larga de URL que desea obtener, es posible que desee emitir algunas tareas y luego procesarlas en el orden en que se completan. Puedes hacerlo usando ray.wait .

 urls = 100 * urls # Pretend we have a long list of URLs. results = [] in_progress_ids = [] # Start pulling 10 URLs in parallel. for _ in range(10): url = urls.pop() in_progress_ids.append(fetch.remote(url)) # Whenever one finishes, start fetching a new one. while len(in_progress_ids) > 0: # Get a result that has finished. [ready_id], in_progress_ids = ray.wait(in_progress_ids) results.append(ray.get(ready_id)) # Start a new task. if len(urls) > 0: in_progress_ids.append(fetch.remote(urls.pop())) 

Ver la documentación de Ray .

Recuperar páginas web, obviamente, tomará un tiempo, ya que no estás accediendo a nada local. Si tiene varios a los que puede acceder, puede usar el módulo de threading para ejecutar un par a la vez.

Aquí hay un ejemplo muy crudo

 import threading import urllib2 import time urls = ['http://docs.python.org/library/threading.html', 'http://docs.python.org/library/thread.html', 'http://docs.python.org/library/multiprocessing.html', 'http://docs.python.org/howto/urllib2.html'] data1 = [] data2 = [] class PageFetch(threading.Thread): def __init__(self, url, datadump): self.url = url self.datadump = datadump threading.Thread.__init__(self) def run(self): page = urllib2.urlopen(self.url) self.datadump.append(page.read()) # don't do it like this. print "Starting threaded reads:" start = time.clock() for url in urls: PageFetch(url, data2).start() while len(data2) < len(urls): pass # don't do this either. print "...took %f seconds" % (time.clock() - start) print "Starting sequential reads:" start = time.clock() for url in urls: page = urllib2.urlopen(url) data1.append(page.read()) print "...took %f seconds" % (time.clock() - start) for i,x in enumerate(data1): print len(data1[i]), len(data2[i]) 

Esta fue la salida cuando lo ejecuté:

 Starting threaded reads: ...took 2.035579 seconds Starting sequential reads: ...took 4.307102 seconds 73127 19923 19923 59366 361483 73127 59366 361483 

Es probable que no sea aconsejable capturar los datos del hilo adjuntándolos a una lista (la cola sería mejor), pero eso demuestra que hay una diferencia.

Aquí hay una solución de biblioteca estándar. No es tan rápido, pero utiliza menos memoria que las soluciones de subprocesos.

 try: from http.client import HTTPConnection, HTTPSConnection except ImportError: from httplib import HTTPConnection, HTTPSConnection connections = [] results = [] for url in urls: scheme, _, host, path = url.split('/', 3) h = (HTTPConnection if scheme == 'http:' else HTTPSConnection)(host) h.request('GET', '/' + path) connections.append(h) for h in connections: results.append(h.getresponse().read()) 

Además, si la mayoría de sus solicitudes son para el mismo host, entonces reutilizar la misma conexión http probablemente ayudaría más que hacer cosas en paralelo.

Encuentre el script de referencia de red de Python para la identificación de lentitud de una sola conexión:

 """Python network test.""" from socket import create_connection from time import time try: from urllib2 import urlopen except ImportError: from urllib.request import urlopen TIC = time() create_connection(('216.58.194.174', 80)) print('Duration socket IP connection (s): {:.2f}'.format(time() - TIC)) TIC = time() create_connection(('google.com', 80)) print('Duration socket DNS connection (s): {:.2f}'.format(time() - TIC)) TIC = time() urlopen('http://216.58.194.174') print('Duration urlopen IP connection (s): {:.2f}'.format(time() - TIC)) TIC = time() urlopen('http://google.com') print('Duration urlopen DNS connection (s): {:.2f}'.format(time() - TIC)) 

Y ejemplo de resultados con Python 3.6:

 Duration socket IP connection (s): 0.02 Duration socket DNS connection (s): 75.51 Duration urlopen IP connection (s): 75.88 Duration urlopen DNS connection (s): 151.42 

Python 2.7.13 tiene resultados muy similares.

En este caso, la lentitud de DNS y urlopen se identifican fácilmente.