web-dev-qa-db-fra.com

Quel est le moyen le plus rapide d’envoyer 100 000 requêtes HTTP en Python?

J'ouvre un fichier qui a 100 000 URL. J'ai besoin d'envoyer une requête HTTP à chaque URL et d'imprimer le code d'état. J'utilise Python 2.6 et, jusqu'à présent, j'ai examiné les nombreuses façons source de confusion que Python implémente le threading/la concurrence. J'ai même regardé la bibliothèque python concurrence , mais je n'arrive pas à comprendre comment écrire ce programme correctement. Quelqu'un at-il rencontré un problème similaire? De manière générale, je suppose que je dois savoir comment effectuer des milliers de tâches dans Python le plus rapidement possible. Je suppose que cela signifie "simultanément".

237
IgorGanapolsky

Twisted Less solution:

from urlparse import urlparse
from threading import Thread
import httplib, sys
from Queue import Queue

concurrent = 200

def doWork():
    while True:
        url = q.get()
        status, url = getStatus(url)
        doSomethingWithResult(status, url)
        q.task_done()

def getStatus(ourl):
    try:
        url = urlparse(ourl)
        conn = httplib.HTTPConnection(url.netloc)   
        conn.request("HEAD", url.path)
        res = conn.getresponse()
        return res.status, ourl
    except:
        return "error", ourl

def doSomethingWithResult(status, url):
    print status, url

q = Queue(concurrent * 2)
for i in range(concurrent):
    t = Thread(target=doWork)
    t.daemon = True
    t.start()
try:
    for url in open('urllist.txt'):
        q.put(url.strip())
    q.join()
except KeyboardInterrupt:
    sys.exit(1)

Celui-ci est légèrement plus rapide que la solution torsadée et utilise moins de processeur.

179
Tarnay Kálmán

Une solution utilisant tornado bibliothèque réseau asynchrone

from tornado import ioloop, httpclient

i = 0

def handle_request(response):
    print(response.code)
    global i
    i -= 1
    if i == 0:
        ioloop.IOLoop.instance().stop()

http_client = httpclient.AsyncHTTPClient()
for url in open('urls.txt'):
    i += 1
    http_client.fetch(url.strip(), handle_request, method='HEAD')
ioloop.IOLoop.instance().start()
50
mher

Les fils ne sont absolument pas la réponse ici. Ils fourniront à la fois des goulots d'étranglement au niveau du processus et du noyau, ainsi que des limites de débit inacceptables si l'objectif général est "le moyen le plus rapide".

Un peu de twisted et son client asynchrone HTTP vous donneraient de bien meilleurs résultats.

37
ironfroggy

Les choses ont bien changé depuis 2010, date à laquelle cela a été publié. Je n'ai pas essayé toutes les autres réponses, mais j'en ai essayé quelques-unes, et j'ai trouvé que cela fonctionnait le mieux pour moi avec python3.6.

J'ai pu extraire environ 150 domaines uniques par seconde exécutés sur AWS.

import pandas as pd
import concurrent.futures
import requests
import time

out = []
CONNECTIONS = 100
TIMEOUT = 5

tlds = open('../data/sample_1k.txt').read().splitlines()
urls = ['http://{}'.format(x) for x in tlds[1:]]

def load_url(url, timeout):
    ans = requests.head(url, timeout=timeout)
    return ans.status_code

with concurrent.futures.ThreadPoolExecutor(max_workers=CONNECTIONS) as executor:
    future_to_url = (executor.submit(load_url, url, TIMEOUT) for url in urls)
    time1 = time.time()
    for future in concurrent.futures.as_completed(future_to_url):
        try:
            data = future.result()
        except Exception as exc:
            data = str(type(exc))
        finally:
            out.append(data)

            print(str(len(out)),end="\r")

    time2 = time.time()

print(f'Took {time2-time1:.2f} s')
print(pd.Series(out).value_counts())
32
Glen Thompson

Utilisez grequests , c'est une combinaison de requêtes + module Gevent.

GRequests vous permet d'utiliser Requests avec Gevent pour créer facilement des requêtes HTTP asynchrones.

L'utilisation est simple:

import grequests

urls = [
   'http://www.heroku.com',
   'http://tablib.org',
   'http://httpbin.org',
   'http://python-requests.org',
   'http://kennethreitz.com'
]

Créez un ensemble de demandes non envoyées:

>>> rs = (grequests.get(u) for u in urls)

Envoyez-les tous en même temps:

>>> grequests.map(rs)
[<Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>]
14
Akshay Pratap Singh

Une bonne approche pour résoudre ce problème consiste à écrire d’abord le code requis pour obtenir un résultat, puis à incorporer du code de threading pour paralléliser l’application.

Dans un monde parfait, cela signifie simplement que 100 000 threads doivent être démarrés simultanément et que leurs résultats sont exportés dans un dictionnaire ou une liste pour un traitement ultérieur, mais en pratique, le nombre de demandes HTTP parallèles que vous pouvez émettre de cette manière est limité. Localement, vous avez une limite quant au nombre de sockets que vous pouvez ouvrir simultanément, au nombre de threads d'exécution que votre interprète Python autorisera. À distance, le nombre de connexions simultanées peut être limité si toutes les demandes concernent un ou plusieurs serveurs. Ces limitations nécessiteront probablement que vous écriviez le script de manière à n’interroger qu’une petite fraction des URL à la fois (100, comme l’a mentionné une autre affiche, correspond probablement à une taille de pool de threads décente, bien que vous constatiez peut-être que peut déployer avec succès beaucoup plus).

Vous pouvez suivre ce modèle pour résoudre le problème ci-dessus:

  1. Démarrer un thread qui lance de nouveaux threads de requête jusqu'à ce que le nombre de threads en cours d'exécution (vous pouvez les suivre via threading.active_count () ou en poussant les objets de thread dans une structure de données) est égal à> = votre nombre maximal de requêtes simultanées (par exemple 100). , puis dort pendant un court délai. Ce fil doit se terminer lorsqu'il n'y a plus d'URL à traiter. Ainsi, le thread continuera à se réveiller, lançant de nouveaux threads et dormira jusqu'à ce que vous ayez terminé.
  2. Demandez aux unités d'exécution de stocker leurs résultats dans une structure de données en vue d'une récupération et d'une sortie ultérieures. Si la structure dans laquelle vous stockez les résultats est une variable list ou dict dans CPython, vous pouvez ajouter ou insérer en toute sécurité des éléments uniques de vos threads sans verro , mais si vous écrivez Pour un fichier ou une interaction plus complexe entre données , vous devez utiliser un verrou d'exclusion mutuelle pour protéger cet état de la corruption .

Je vous suggère d'utiliser le module threading . Vous pouvez l'utiliser pour lancer et suivre les threads en cours d'exécution. Le support de threading de Python est simple, mais la description de votre problème suggère qu'il est complètement suffisant pour vos besoins.

Enfin, si vous souhaitez voir une application assez simple d’une application réseau parallèle écrite en Python, consultez ssh.py . C'est une petite bibliothèque qui utilise Python threading pour mettre en parallèle de nombreuses connexions SSH. La conception est suffisamment proche de vos besoins pour que vous puissiez la trouver comme une bonne ressource.

7
Erik Garrison

Si vous souhaitez obtenir les meilleures performances possibles, envisagez d'utiliser des E/S asynchrones plutôt que des threads. La surcharge associée à des milliers de threads de système d'exploitation n'est pas triviale et la commutation de contexte au sein de l'interpréteur Python en ajoute encore plus. Les threads vont certainement faire le travail, mais je pense qu’une route asynchrone offrira de meilleures performances.

Plus précisément, je suggérerais le client Web asynchrone dans la bibliothèque Twisted ( http://www.twistedmatrix.com ). La courbe d’apprentissage est certes raide, mais elle est assez facile à utiliser une fois que vous maîtrisez bien le style de programmation asynchrone de Twisted.

Un guide pratique sur l'API client Web asynchrone de Twisted est disponible à l'adresse suivante:

http://twistedmatrix.com/documents/current/web/howto/client.html

7
Rakis

Une solution:

from twisted.internet import reactor, threads
from urlparse import urlparse
import httplib
import itertools


concurrent = 200
finished=itertools.count(1)
reactor.suggestThreadPoolSize(concurrent)

def getStatus(ourl):
    url = urlparse(ourl)
    conn = httplib.HTTPConnection(url.netloc)   
    conn.request("HEAD", url.path)
    res = conn.getresponse()
    return res.status

def processResponse(response,url):
    print response, url
    processedOne()

def processError(error,url):
    print "error", url#, error
    processedOne()

def processedOne():
    if finished.next()==added:
        reactor.stop()

def addTask(url):
    req = threads.deferToThread(getStatus, url)
    req.addCallback(processResponse, url)
    req.addErrback(processError, url)   

added=0
for url in open('urllist.txt'):
    added+=1
    addTask(url.strip())

try:
    reactor.run()
except KeyboardInterrupt:
    reactor.stop()

Temps de test:

[kalmi@ubi1:~] wc -l urllist.txt
10000 urllist.txt
[kalmi@ubi1:~] time python f.py > /dev/null 

real    1m10.682s
user    0m16.020s
sys 0m10.330s
[kalmi@ubi1:~] head -n 6 urllist.txt
http://www.google.com
http://www.bix.hu
http://www.godaddy.com
http://www.google.com
http://www.bix.hu
http://www.godaddy.com
[kalmi@ubi1:~] python f.py | head -n 6
200 http://www.bix.hu
200 http://www.bix.hu
200 http://www.bix.hu
200 http://www.bix.hu
200 http://www.bix.hu
200 http://www.bix.hu

Pingtime:

bix.hu is ~10 ms away from me
godaddy.com: ~170 ms
google.com: ~30 ms
5
Tarnay Kálmán

Je sais que c’est une vieille question, mais dans Python 3.7, vous pouvez le faire avec asyncio et aiohttp.

import asyncio
import aiohttp
from aiohttp import ClientSession, ClientConnectorError

async def fetch_html(url: str, session: ClientSession, **kwargs) -> Tuple:
    try:
        resp = await session.request(method="GET", url=url, **kwargs)
    except ClientConnectorError:
        return (url, 404)
    return (url, resp.status)

async def make_requests(urls: set, **kwargs) -> None:
    async with ClientSession() as session:
        tasks = []
        for url in urls:
            tasks.append(
                fetch_html(url=url, session=session, **kwargs)
            )
        results = await asyncio.gather(*tasks)

    for result in results:
        print(f'{result[1]} - {str(result[0])}')

if __== "__main__":
    import pathlib
    import sys

    assert sys.version_info >= (3, 7), "Script requires Python 3.7+."
    here = pathlib.Path(__file__).parent

    with open(here.joinpath("urls.txt")) as infile:
        urls = set(map(str.strip, infile))

    asyncio.run(make_requests(urls=urls))

Vous pouvez en lire plus à ce sujet et voir un exemple ici .

2
Marius Stănescu

Créer un objet epoll,
ouvrir plusieurs sockets client TCP,
ajuster leurs tampons d'envoi pour être un peu plus que l'en-tête de demande,
envoie un en-tête de requête - il devrait être immédiat, il suffit de le placer dans un tampon pour enregistrer socket dans epoll objet,
ne .poll sur epoll obect,
lit les 3 premiers octets de chaque socket de .poll,
écrivez-les dans sys.stdout suivi de \n (ne pas vider), fermez le socket client.

Limiter le nombre de sockets ouverts simultanément - gérer les erreurs lors de la création de sockets. Créez un nouveau socket uniquement si un autre est fermé.
Ajustez les limites du système d'exploitation.
Essayez d’interfacer quelques processus (pas beaucoup): cela peut aider à utiliser le processeur un peu plus efficacement.

1
George Sovetov

Utiliser un pool de threads est une bonne option, et cela facilitera la tâche. Malheureusement, python ne dispose pas d'une bibliothèque standard qui rend les pools de threads extrêmement faciles. Mais voici une bonne bibliothèque qui devrait vous aider à démarrer: http://www.chrisarndt.de/projects/threadpool/

Exemple de code de leur site:

pool = ThreadPool(poolsize)
requests = makeRequests(some_callable, list_of_args, callback)
[pool.putRequest(req) for req in requests]
pool.wait()

J'espère que cela t'aides.

1
Kevin Wiskia

Ce client Web asynchrone tordu va assez vite.

#!/usr/bin/python2.7

from twisted.internet import reactor
from twisted.internet.defer import Deferred, DeferredList, DeferredLock
from twisted.internet.defer import inlineCallbacks
from twisted.web.client import Agent, HTTPConnectionPool
from twisted.web.http_headers import Headers
from pprint import pprint
from collections import defaultdict
from urlparse import urlparse
from random import randrange
import fileinput

pool = HTTPConnectionPool(reactor)
pool.maxPersistentPerHost = 16
agent = Agent(reactor, pool)
locks = defaultdict(DeferredLock)
codes = {}

def getLock(url, simultaneous = 1):
    return locks[urlparse(url).netloc, randrange(simultaneous)]

@inlineCallbacks
def getMapping(url):
    # Limit ourselves to 4 simultaneous connections per Host
    # Tweak this number, but it should be no larger than pool.maxPersistentPerHost 
    lock = getLock(url,4)
    yield lock.acquire()
    try:
        resp = yield agent.request('HEAD', url)
        codes[url] = resp.code
    except Exception as e:
        codes[url] = str(e)
    finally:
        lock.release()


dl = DeferredList(getMapping(url.strip()) for url in fileinput.input())
dl.addCallback(lambda _: reactor.stop())

reactor.run()
pprint(codes)
0
Robᵩ

Dans votre cas, les threads feront probablement l'affaire, car vous passerez probablement le plus de temps à attendre une réponse. Des modules utiles tels que file d'attente dans la bibliothèque standard pourraient vous aider.

J'avais déjà fait la même chose avec le téléchargement parallèle de fichiers auparavant et c'était assez bon pour moi, mais ce n'était pas à l'échelle dont vous parlez.

Si votre tâche était davantage liée au processeur, vous voudrez peut-être consulter le module multitraitement , qui vous permettra d'utiliser plus de processeurs/cœurs/threads le verrouillage est par processus)

0
Mattias Nilsson

Pensez à utiliser Windmill , bien que Windmill ne puisse probablement pas faire autant de threads.

Vous pouvez le faire avec un script Python roulé à la main sur 5 machines, chacune se connectant en sortie à l'aide des ports 40000 à 60000, ouvrant ainsi 100 000 connexions de port.

En outre, il pourrait être utile de faire un exemple de test avec une application QA bien filetée telle que OpenSTA afin de savoir combien chaque serveur peut gérer.

Essayez également d’utiliser Perl avec la classe LWP :: ConnCache. Vous obtiendrez probablement plus de performances (plus de connexions) de cette façon.

0
djangofan