Cómo acelerar el raspado web en python

Estoy trabajando en un proyecto para la escuela y estoy tratando de obtener datos sobre películas. Logré escribir un script para obtener los datos que necesito de IMDbPY y Open Movie DB API (omdbapi.com). El desafío que estoy experimentando es que estoy tratando de obtener datos para 22,305 películas y cada solicitud toma aproximadamente 0,7 segundos. Esencialmente mi script actual tardará aproximadamente 8 horas en completarse. Buscando cualquier forma de utilizar múltiples solicitudes al mismo tiempo o cualquier otra sugerencia para acelerar significativamente el proceso de obtención de estos datos.

import urllib2 import json import pandas as pd import time import imdb start_time = time.time() #record time at beginning of script #used to make imdb.com think we are getting this data from a browser user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' headers = { 'User-Agent' : user_agent } #Open Movie Database Query url for IMDb IDs url = 'http://www.omdbapi.com/?tomatoes=true&i=' #read the ids from the imdb_id csv file imdb_ids = pd.read_csv('ids.csv') cols = [u'Plot', u'Rated', u'tomatoImage', u'Title', u'DVD', u'tomatoMeter', u'Writer', u'tomatoUserRating', u'Production', u'Actors', u'tomatoFresh', u'Type', u'imdbVotes', u'Website', u'tomatoConsensus', u'Poster', u'tomatoRotten', u'Director', u'Released', u'tomatoUserReviews', u'Awards', u'Genre', u'tomatoUserMeter', u'imdbRating', u'Language', u'Country', u'imdbpy_budget', u'BoxOffice', u'Runtime', u'tomatoReviews', u'imdbID', u'Metascore', u'Response', u'tomatoRating', u'Year', u'imdbpy_gross'] #create movies dataframe movies = pd.DataFrame(columns=cols) i=0 for i in range(len(imdb_ids)-1): start = time.time() req = urllib2.Request(url + str(imdb_ids.ix[i,0]), None, headers) #request page response = urllib2.urlopen(req) #actually call the html request the_page = response.read() #read the json from the omdbapi query movie_json = json.loads(the_page) #convert the json to a dict #get the gross revenue and budget from IMDbPy data = imdb.IMDb() movie_id = imdb_ids.ix[i,['imdb_id']] movie_id = movie_id.to_string() movie_id = int(movie_id[-7:]) data = data.get_movie_business(movie_id) data = data['data'] data = data['business'] #get the budget $ amount out of the budget IMDbPy string try: budget = data['budget'] budget = budget[0] budget = budget.replace('$', '') budget = budget.replace(',', '') budget = budget.split(' ') budget = str(budget[0]) except: None #get the gross $ amount out of the gross IMDbPy string try: budget = data['budget'] budget = budget[0] budget = budget.replace('$', '') budget = budget.replace(',', '') budget = budget.split(' ') budget = str(budget[0]) #get the gross $ amount out of the gross IMDbPy string gross = data['gross'] gross = gross[0] gross = gross.replace('$', '') gross = gross.replace(',', '') gross = gross.split(' ') gross = str(gross[0]) except: None #add gross to the movies dict try: movie_json[u'imdbpy_gross'] = gross except: movie_json[u'imdbpy_gross'] = 0 #add gross to the movies dict try: movie_json[u'imdbpy_budget'] = budget except: movie_json[u'imdbpy_budget'] = 0 #create new dataframe that can be merged to movies DF tempDF = pd.DataFrame.from_dict(movie_json, orient='index') tempDF = tempDF.T #add the new movie to the movies dataframe movies = movies.append(tempDF, ignore_index=True) end = time.time() time_took = round(end-start, 2) percentage = round(((i+1) / float(len(imdb_ids))) * 100,1) print i+1,"of",len(imdb_ids),"(" + str(percentage)+'%)','completed',time_took,'sec' #increment counter i+=1 #save the dataframe to a csv file movies.to_csv('movie_data.csv', index=False) end_time = time.time() print round((end_time-start_time)/60,1), "min" 

Usa la librería Eventlet para buscar al mismo tiempo.

Como se aconseja en los comentarios, deberás buscar tus feeds al mismo tiempo. Esto se puede hacer mediante el uso de la banda de treading , el multiprocessing o el uso de eventlet .

Instalar eventlet

 $ pip install eventlet 

Probar ejemplo de rastreador web de eventlet

Consulte: http://eventlet.net/doc/examples.html#web-crawler

Entendiendo la concurrencia con eventlet

Con el sistema de threading se cuida de cambiar entre sus hilos. Esto trae un gran problema en caso de que tenga que acceder a algunas estructuras de datos comunes, como nunca se sabe, a qué otro hilo está accediendo actualmente a sus datos. Luego comienza a jugar con bloques sincronizados, lockings, semáforos, solo para sincronizar el acceso a sus estructuras de datos compartidas.

Con eventlet , es mucho más simple: siempre ejecuta un solo hilo y salta entre ellos solo en las instrucciones de E / S o en otras llamadas de eventlet . El rest de su código se ejecuta de forma ininterrumpida y sin ningún riesgo, otra cadena podría desordenar nuestros datos.

Solo tienes que ocuparte de lo siguiente:

  • todas las operaciones de E / S deben ser sin locking (esto es más fácil, eventlet proporciona versiones sin locking para la mayoría de las E / S que necesita).

  • su código restante no debe ser costoso para la CPU, ya que bloquearía el cambio entre subprocesos “verdes” durante más tiempo y la potencia de los subprocesos múltiples “verdes” desaparecerá.

La gran ventaja con eventlet es que permite escribir código de forma directa sin estropearlo (demasiado) con Locks, Semaphores, etc.

Aplicar eventlet a su código

Si lo comprendo correctamente, se conoce de antemano la lista de direcciones URL a buscar y el orden de su procesamiento en su análisis no es importante. Esto permitirá casi copia directa del ejemplo de eventlet . Veo que un índice i tiene cierta importancia, por lo que podría considerar mezclar la URL y el índice como una tupla y procesarlos como trabajos independientes.

Definitivamente hay otros métodos, pero personalmente he encontrado que eventlet muy fácil de usar, comparándolo con otras técnicas y obteniendo resultados realmente buenos (especialmente con la obtención de feeds). Solo tiene que comprender los conceptos principales y tener un poco de cuidado al seguir los requisitos de eventlet (no lockings).

Obteniendo urls usando peticiones y eventlets – erequests

Hay varios paquetes para el procesamiento asíncrono con requests , uno de ellos utiliza eventlet y se denomina erequests consulte https://github.com/saghul/erequests

Muestra de recuperación simple conjunto de urls

 import erequests # have list of urls to fetch urls = [ 'http://www.heroku.com', 'http://python-tablib.org', 'http://httpbin.org', 'http://python-requests.org', 'http://kennethreitz.com' ] # erequests.async.get(url) creates asynchronous request async_reqs = [erequests.async.get(url) for url in urls] # each async request is ready to go, but not yet performed # erequests.map will call each async request to the action # what returns processed request `req` for req in erequests.map(async_reqs): if req.ok: content = req.content # process it here print "processing data from:", req.url 

Problemas para procesar esta pregunta específica

Podemos recuperar y procesar de alguna manera todas las URL que necesitamos. Pero en esta pregunta, el procesamiento está vinculado a un registro particular en los datos de origen, por lo que tendremos que hacer coincidir la solicitud procesada con el índice de registro que necesitamos para obtener más detalles para el procesamiento final.

Como veremos más adelante, el procesamiento asíncrono no respeta el orden de las solicitudes, algunas se procesan antes y otras más tarde, y map resultados de los map se completan.

Una opción es adjuntar el índice de la url dada a las solicitudes y usarlo más tarde al procesar los datos devueltos.

Muestra compleja de extracción y procesamiento de URL con índices de URL de preservación.

Nota: la siguiente muestra es bastante compleja, si puede vivir con la solución proporcionada anteriormente, omita esto. Pero asegúrese de que no tenga problemas detectados y resueltos a continuación (se están modificando las URL, las solicitudes se realizan después de las redirecciones).

 import erequests from itertools import count, izip from functools import partial urls = [ 'http://www.heroku.com', 'http://python-tablib.org', 'http://httpbin.org', 'http://python-requests.org', 'http://kennethreitz.com' ] def print_url_index(index, req, *args, **kwargs): content_length = req.headers.get("content-length", None) todo = "PROCESS" if req.status_code == 200 else "WAIT, NOT YET READY" print "{todo}: index: {index}: status: {req.status_code}: length: {content_length}, {req.url}".format(**locals()) async_reqs = (erequests.async.get(url, hooks={"response": partial(print_url_index, i)}) for i, url in izip(count(), urls)) for req in erequests.map(async_reqs): pass 

Adjuntar ganchos a pedido.

requests (y las erequests también) permiten definir enganches al evento llamado response . Cada vez que la solicitud recibe una respuesta, se llama a esta función de enlace y puede hacer algo o incluso modificar la respuesta.

La siguiente línea define algunos enganches a la respuesta:

 erequests.async.get(url, hooks={"response": partial(print_url_index, i)}) 

Pasando índice de url a la función de gancho

La firma de cualquier gancho será func(req, *args, *kwargs)

Pero necesitamos pasar a la función de enlace también el índice de url que estamos procesando.

Para este propósito, utilizamos functools.partial que permite la creación de funciones simplificadas mediante la fijación de algunos parámetros a un valor específico. Esto es exactamente lo que necesitamos. Si ve la firma print_url_index , solo necesitamos corregir el valor del index , el rest se ajustará a los requisitos para la función de enlace.

En nuestra llamada, usamos partial con el nombre de la función simplificada print_url_index y proveyendo para cada índice único de url.

El índice podría proporcionarse en el bucle por enumerate , en caso de que haya un mayor número de parámetros, podemos trabajar de manera más eficiente con la memoria y usar el count , lo que genera cada número incrementado de tiempo, comenzando por defecto desde 0.

Vamos a ejecutarlo:

 $ python ereq.py WAIT, NOT YET READY: index: 3: status: 301: length: 66, http://python-requests.org/ WAIT, NOT YET READY: index: 4: status: 301: length: 58, http://kennethreitz.com/ WAIT, NOT YET READY: index: 0: status: 301: length: None, http://www.heroku.com/ PROCESS: index: 2: status: 200: length: 7700, http://httpbin.org/ WAIT, NOT YET READY: index: 1: status: 301: length: 64, http://python-tablib.org/ WAIT, NOT YET READY: index: 4: status: 301: length: None, http://kennethreitz.org WAIT, NOT YET READY: index: 3: status: 302: length: 0, http://docs.python-requests.org WAIT, NOT YET READY: index: 1: status: 302: length: 0, http://docs.python-tablib.org PROCESS: index: 3: status: 200: length: None, http://docs.python-requests.org/en/latest/ PROCESS: index: 1: status: 200: length: None, http://docs.python-tablib.org/en/latest/ PROCESS: index: 0: status: 200: length: 12064, https://www.heroku.com/ PROCESS: index: 4: status: 200: length: 10478, http://www.kennethreitz.org/ 

Esto muestra que:

  • Las solicitudes no se procesan en el orden en que fueron generadas.
  • Algunas solicitudes siguen a la redirección, por lo que la función de enlace se llama varias veces
  • Inspeccionando cuidadosamente los valores de URL que podemos ver, que no se informa de la urls original de la lista por respuesta, incluso para el índice 2 obtuvimos datos adicionales / agregados. Es por eso que una simple búsqueda de url de respuesta en la lista original de urls no nos ayudaría.