web-dev-qa-db-fra.com

Extraction d'URL parallèle multithreading très simple (sans file d'attente)

J'ai passé toute une journée à rechercher le fetcher d'URL multithread le plus simple possible en Python, mais la plupart des scripts que j'ai trouvés utilisaient des files d'attente, des bibliothèques de traitement multiple ou complexes.

Enfin, j'en ai écrit un moi-même, que je rapporte comme une réponse. S'il vous plaît n'hésitez pas à suggérer toute amélioration.

Je suppose que d'autres personnes ont peut-être cherché quelque chose de similaire.

41
Daniele B

Simplifier votre version originale autant que possible:

import threading
import urllib2
import time

start = time.time()
urls = ["http://www.google.com", "http://www.Apple.com", "http://www.Microsoft.com", "http://www.Amazon.com", "http://www.facebook.com"]

def fetch_url(url):
    urlHandler = urllib2.urlopen(url)
    html = urlHandler.read()
    print "'%s\' fetched in %ss" % (url, (time.time() - start))

threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print "Elapsed Time: %s" % (time.time() - start)

Les seuls nouveaux trucs ici sont:

  • Gardez une trace des discussions que vous créez.
  • Ne vous embêtez pas avec un compteur de threads si vous voulez juste savoir quand ils ont tous terminé. join vous le dit déjà.
  • Si vous n'avez besoin d'aucun état ni d'aucune API externe, vous n'avez pas besoin d'une sous-classe Thread, mais d'une fonction target.
37
abarnert

multiprocessing a un pool de threads qui ne démarre pas d'autres processus:

#!/usr/bin/env python
from multiprocessing.pool import ThreadPool
from time import time as timer
from urllib2 import urlopen

urls = ["http://www.google.com", "http://www.Apple.com", "http://www.Microsoft.com", "http://www.Amazon.com", "http://www.facebook.com"]

def fetch_url(url):
    try:
        response = urlopen(url)
        return url, response.read(), None
    except Exception as e:
        return url, None, e

start = timer()
results = ThreadPool(20).imap_unordered(fetch_url, urls)
for url, html, error in results:
    if error is None:
        print("%r fetched in %ss" % (url, timer() - start))
    else:
        print("error fetching %r: %s" % (url, error))
print("Elapsed Time: %s" % (timer() - start,))

Les avantages par rapport à la solution Thread-:

  • ThreadPool permet de limiter le nombre maximum de connexions simultanées (20 dans l'exemple de code)
  • la sortie n'est pas tronquée car toute la sortie est dans le thread principal 
  • les erreurs sont enregistrées
  • le code fonctionne à la fois sur Python 2 et 3 sans modification (en supposant que from urllib.request import urlopen sur Python 3).
24
jfs

L'exemple principal dans le concurrent.futures fait tout ce que vous voulez, beaucoup plus simplement. De plus, il peut gérer un grand nombre d'URL en en faisant seulement 5 à la fois, et il gère les erreurs beaucoup plus facilement.

Bien entendu, ce module est uniquement intégré à Python 3.2 ou version ultérieure… mais si vous utilisez la version 2.5-3.1, vous pouvez simplement installer le backport, futures , en dehors de PyPI. Dans l'exemple de code, il vous suffit de rechercher et de remplacer concurrent.futures par futures et, pour 2.x, urllib.request avec urllib2.

Voici l'exemple de backported en 2.x, modifié pour utiliser votre liste d'URL et pour ajouter les heures:

import concurrent.futures
import urllib2
import time

start = time.time()
urls = ["http://www.google.com", "http://www.Apple.com", "http://www.Microsoft.com", "http://www.Amazon.com", "http://www.facebook.com"]

# Retrieve a single page and report the url and contents
def load_url(url, timeout):
    conn = urllib2.urlopen(url, timeout=timeout)
    return conn.readall()

# 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 '"%s" fetched in %ss' % (url,(time.time() - start))
print "Elapsed Time: %ss" % (time.time() - start)

Mais vous pouvez rendre cela encore plus simple. Vraiment, tout ce dont vous avez besoin est:

def load_url(url):
    conn = urllib2.urlopen(url, timeout)
    data = conn.readall()
    print '"%s" fetched in %ss' % (url,(time.time() - start))
    return data

with futures.ThreadPoolExecutor(max_workers=5) as executor:
    pages = executor.map(load_url, urls)

print "Elapsed Time: %ss" % (time.time() - start)
12
abarnert

Je publie maintenant une solution différente, par en ayant les threads de travail non-deamon et en les joignant au thread principal (ce qui signifie bloquer le thread principal jusqu'à ce que tous les threads de travail soient terminés) au lieu de notifier la fin de l'exécution de chaque thread de travail avec un rappel à une fonction globale (comme je l'ai fait dans la réponse précédente), comme dans certains commentaires, il a été noté qu'une telle manière n'est pas thread-safe.

import threading
import urllib2
import time

start = time.time()
urls = ["http://www.google.com", "http://www.Apple.com", "http://www.Microsoft.com", "http://www.Amazon.com", "http://www.facebook.com"]

class FetchUrl(threading.Thread):
    def __init__(self, url):
        threading.Thread.__init__(self)
        self.url = url

    def run(self):
        urlHandler = urllib2.urlopen(self.url)
        html = urlHandler.read()
        print "'%s\' fetched in %ss" % (self.url,(time.time() - start))

for url in urls:
    FetchUrl(url).start()

#Join all existing threads to main thread.
for thread in threading.enumerate():
    if thread is not threading.currentThread():
        thread.join()

print "Elapsed Time: %s" % (time.time() - start)
0
Daniele B