libreria example python time urllib2 urlopen cprofile

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



urllib2 python 3 (10)

Aquí hay un ejemplo usando Python Threads . Los otros ejemplos de subprocesos aquí lanzan un subproceso por url, que no es 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 indicados aquí son para 40 URL y dependerán mucho de la velocidad de su conexión a Internet y de la latencia del servidor. Al estar en Australia, mi ping es> 300ms

Con WORKERS=1 se necesitaron 86 segundos para ejecutar
Con WORKERS=4 se necesitaron 23 segundos para ejecutar
con WORKERS=10 se necesitaron 10 segundos para ejecutar

así que tener 10 hilos de descarga es 8.6 veces más rápido que un solo hilo.

Aquí hay una versión actualizada que usa una cola. Hay al menos un par de ventajas.
1. Las URL 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

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 suponía, urlopen ocupa mucho tiempo. ¿Hay alguna manera de buscar las páginas más rápido? ¿O una forma de buscar varias páginas a la vez? Haré lo que sea más simple, ya que soy nuevo en python y desarrollo web.

¡Gracias por adelantado! :)

ACTUALIZACIÓN: Tengo una función llamada fetchURLs() , que utilizo para hacer una matriz de las URL que necesito, algo así como urls = fetchURLS() . Las URL son todos archivos XML de Amazon y las API de eBay (lo que me confunde por qué lleva tanto tiempo cargar, ¿quizás mi servidor web es lento?)

Lo que tengo que hacer es cargar cada URL, leer cada página y enviar esos datos a otra parte del script que analizará y mostrará los datos.

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

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

Aquí es 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 <10> 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)


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

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, reutilizar la misma conexión http probablemente le ayude más que hacer las cosas en paralelo.


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 hace que el código sea más fácil: https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.map


Encuentre la secuencia de comandos de referencia de red de Python para la identificación de lentitud de conexión única:

"""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, DNS y la lentitud de urlopen se identifican fácilmente.


Hoy en día hay una excelente lib de Python que hace esto para las requests llamadas.

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


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

Hay 2 formas de acelerar esto.

  1. Mantenga viva la conexión (vea esta pregunta sobre cómo hacerlo: Python urllib2 con Keep alive )
  2. Use conexiones multiples, puede usar hilos o un enfoque asincrónico como sugirió Aaron Gallagher. Para eso, simplemente use cualquier ejemplo de subprocesamiento y debería hacerlo bien :) También puede usar la lib de multiprocessing para facilitar las cosas.

La mayoría de las respuestas se centraron en buscar varias páginas de diferentes servidores al mismo tiempo (enhebrar) pero no en reutilizar conexiones HTTP ya abiertas. Si OP realiza múltiples solicitudes al mismo servidor / sitio.

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

También hay Requests una biblioteca HTTP que usa urllib3

Esto combinado con el enhebrado debe aumentar la velocidad de búsqueda de páginas


La obtención de páginas web, obviamente, llevará un tiempo ya que no está accediendo a nada local. Si tiene varios para 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])

Este fue el resultado 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

Tomar los datos del hilo añadiéndolos a una lista es probablemente desacertado (Cola sería mejor) pero ilustra que hay una diferencia.


Usar retorcido ! Hace que este tipo de cosas sea absurdamente fácil en comparación con, digamos, el uso de subprocesos.

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, también está listo para dar un 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

Debo decir que me gusta que las recuperaciones secuenciales funcionen mejor para mí.


EDITAR : Estoy expandiendo la respuesta para incluir un ejemplo más pulido. He encontrado mucha hostilidad y desinformación en este post sobre el subprocesamiento vs la asincronización de E / S. Por lo tanto, también agrego más argumentos para refutar cierto reclamo inválido. Espero que esto ayude a las personas a elegir la herramienta adecuada para el trabajo correcto.

Esto es un duplicado de una pregunta hace 3 días.

Python urllib2.open es lento, necesita una mejor manera de leer varias URLs - Python urllib2.urlopen () es lento, necesita una mejor manera de leer varias URL

Estoy puliendo el código para mostrar cómo buscar múltiples páginas web en paralelo usando hilos.

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 pueden colapsarse en 1 línea si es agresivo. No creo que nadie pueda justificar su afirmación de que esto es complejo e imposible de mantener.

Desafortunadamente, la mayoría del otro código de enhebrado publicado aquí tiene algunos defectos. Muchos de ellos realizan encuestas activas para esperar a que el código finalice. join() es una mejor forma de sincronizar el código. Creo que este código ha mejorado en todos los ejemplos de subprocesamiento hasta el momento.

conexión keep-alive

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

retorcido

Aaron Gallagher es un fanático de twisted Framework y es hostil con cualquier persona que sugiera hilo. Desafortunadamente, muchas de sus afirmaciones son información errónea. Por ejemplo, dijo "-1 para sugerir hilos. Esto está ligado 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 a partir del hilo utilizado. De hecho, la aplicación de E / S ligada tiene más que ganar al usar el hilo de Python (frente a ninguna ganancia en la aplicación de límite de CPU). La crítica errónea de Aaron en el hilo muestra que está bastante confundido acerca de la programación paralela en general.

Herramienta correcta para el trabajo correcto

Soy consciente de que los problemas se refieren a la programación paralela utilizando hilos, python, async I / O, etc. Cada herramienta tiene sus pros y sus contras. Para cada situación hay una herramienta apropiada. No estoy en contra de lo retorcido (aunque no he desplegado uno yo mismo). Pero no creo que podamos decir que ese hilo es MALO y retorcido es BUENO en todas las situaciones.

Por ejemplo, si el requisito del OP es buscar 10.000 sitios web en paralelo, será preferible usar la asincronización de E / S. Enhebrar no será apropiado (a menos que sea con Python sin pila).

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

Dado que mi código no tiene dependencia externa, lo llamaré herramienta adecuada 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 enhebrado. 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 explicas la ganancia de velocidad de 233 ms sobre mi código? Creo que incluso los fanáticos retorcidos se abstienen de saltar a la conclusión de atribuir esto a la eficiencia de retorcido. Después de todo, hay 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 retorcido, y así sucesivamente.

Solo para asegurarme de que el enhebrado de Python no incurrirá en una gran cantidad de ineficiencia, hago un punto de referencia rápido para engendrar 5 hilos y 500 hilos. Me siento cómodo al decir que la sobrecarga del hilo de desove 5 es insignificante y no puede 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]: <Queue.Queue instance at 0x038B2878> 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

Las pruebas adicionales en mi búsqueda paralela muestran una gran variabilidad en el tiempo de respuesta en 17 carreras. (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 subprocesamiento es consistentemente más lento que la asincronización de E / S en un margen mensurable. Dado el número de variables involucradas, debo decir que esta no es una prueba válida para medir la diferencia de rendimiento sistemático entre la E / S asíncrona y el enhebrado.