Was ist der schnellste Weg, um 100.000 HTTP-Anfragen in Python zu senden?

Lesezeit: 12 Minuten

Benutzer-Avatar
IgorGanapolsky

Ich öffne eine Datei mit 100.000 URLs. Ich muss eine HTTP-Anforderung an jede URL senden und den Statuscode drucken. Ich verwende Python 2.6 und habe mir bisher die vielen verwirrenden Möglichkeiten angesehen, wie Python Threading/Parallelität implementiert. Ich habe mir sogar die Python angesehen Übereinstimmung Bibliothek, kann aber nicht herausfinden, wie man dieses Programm richtig schreibt. Ist jemand auf ein ähnliches Problem gestoßen? Ich denke, im Allgemeinen muss ich wissen, wie ich Tausende von Aufgaben in Python so schnell wie möglich ausführen kann – ich nehme an, das bedeutet “gleichzeitig”.

  • Stellen Sie sicher, dass Sie nur die HEAD-Anfrage ausführen (damit Sie nicht das gesamte Dokument herunterladen). Siehe: stackoverflow.com/questions/107405/…

    – Tarnay Kalman

    13. April 2010 um 19:34 Uhr

  • Ausgezeichneter Punkt, Kalmi. Wenn alles, was Igor will, der Status der Anfrage ist, werden diese 100.000 Anfragen viel, viel, viel schneller gehen. Viel schneller.

    – Adam Crossland

    13. April 2010 um 19:41 Uhr

  • Sie brauchen dafür keine Threads; Der effizienteste Weg ist wahrscheinlich die Verwendung einer asynchronen Bibliothek wie Twisted.

    – Prachtfink

    13. April 2010 um 20:16 Uhr

  • Hier sind gevent-, twisted- und asyncio-basierte Codebeispiele (auf 1000000 Anfragen getestet)

    – jfs

    7. Oktober 2014 um 13:21 Uhr


  • @TarnayKálmán ist möglich für requests.get und requests.head (dh eine Seitenanforderung im Vergleich zu einer Kopfanforderung), um unterschiedliche Statuscodes zurückzugeben, daher ist dies nicht der beste Rat

    – Alex

    10. März 2017 um 0:47 Uhr

Benutzer-Avatar
Tarnay Kalman

Twistedless-Lösung:

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)

Diese ist etwas schneller als die verdrehte Lösung und verbraucht weniger CPU.

  • @Kalmi, warum setzt du Queue auf concurrent*2 ?

    – Marcel Wilson

    6. November 2013 um 19:45 Uhr

  • Vergiss es nicht schließen Sie die Verbindung conn.close(). Das Öffnen zu vieler HTTP-Verbindungen kann Ihr Skript irgendwann anhalten und Speicher verbrauchen.

    – Aamir Rind

    8. Januar 2014 um 16:20 Uhr


  • @hyh, die Queue Modul wurde umbenannt in queue in Python 3. Dies ist Python 2-Code.

    – Tarnay Kalman

    29. Dezember 2014 um 0:29 Uhr

  • Wie viel schneller können Sie gehen, wenn Sie jedes Mal mit dem gleichen Server sprechen möchten, indem Sie die Verbindung beibehalten? Kann dies sogar über Threads oder mit einer dauerhaften Verbindung pro Thread erfolgen?

    – langanhaltend

    12. Januar 2015 um 15:25 Uhr

  • @mptevsion, wenn Sie CPython verwenden, können Sie (zum Beispiel) einfach “print status, url” durch “my_global_list.append ((status, url))” ersetzen. (Die meisten Operationen auf) Listen sind in CPython (und einigen anderen Python-Implementierungen) aufgrund der GIL implizit Thread-sicher, daher ist dies sicher.

    – Tarnay Kalman

    5. März 2016 um 15:41 Uhr


Benutzer-Avatar
Glen Thompson

Die Dinge haben sich seit 2010, als dies gepostet wurde, ziemlich geändert, und ich habe nicht alle anderen Antworten ausprobiert, aber ich habe einige ausprobiert, und ich fand, dass dies mit Python 3.6 am besten für mich funktioniert.

Ich konnte etwa 150 eindeutige Domains pro Sekunde abrufen, die auf AWS ausgeführt wurden.

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')

  • Ich frage nur, weil ich es nicht weiß, aber könnte dieses Futures-Zeug durch async/await ersetzt werden?

    – TankorSmash

    9. Oktober 2017 um 21:16 Uhr

  • Es könnte, aber ich habe festgestellt, dass das oben genannte besser funktioniert. Sie könnten aiohttp verwenden, aber es ist nicht Teil der Standardbibliothek und ändert sich ziemlich stark. Es funktioniert, aber ich habe einfach nicht gefunden, dass es so gut funktioniert. Ich erhalte höhere Fehlerraten, wenn ich es verwende, und für mein ganzes Leben kann ich es nicht so gut zum Laufen bringen wie gleichzeitige Futures, obwohl es theoretisch scheint, dass es besser funktionieren sollte, siehe: stackoverflow.com/questions/45800857/… Wenn es gut funktioniert, poste bitte deine Antwort, damit ich es testen kann.

    – Glen Thompson

    9. Oktober 2017 um 21:52 Uhr


  • Dies ist ein Nitpick, aber ich denke, es ist viel sauberer zu setzen time1 = time.time() am Anfang der for-Schleife und time2 = time.time() direkt nach der for-Schleife.

    – Mattwmaster58

    12. Mai 2018 um 17:51 Uhr


  • Ich habe Ihr Snippet getestet, irgendwie wird es zweimal ausgeführt. Mache ich etwas falsch? Oder soll es zweimal laufen? Wenn es der letztere Fall ist, können Sie mir auch helfen zu verstehen, wie es zweimal ausgelöst wird?

    – Ronnie

    17. März 2020 um 3:17 Uhr


  • Es sollte nicht zweimal laufen. Ich bin mir nicht sicher, warum Sie das sehen.

    – Glen Thompson

    18. März 2020 um 15:13 Uhr

Benutzer-Avatar
mher

Eine Lösung mit Tornado asynchrone Netzwerkbibliothek

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()

Dieser Code verwendet nicht blockierende Netzwerk-E/A und hat keine Einschränkung. Es kann auf Zehntausende offener Verbindungen skaliert werden. Es wird in einem einzigen Thread ausgeführt, ist aber viel schneller als jede Threading-Lösung. Kasse nicht blockierende E/A

  • Es ist ein Zähler, um zu bestimmen, wann der „ioloop“ verlassen werden soll – also wenn Sie fertig sind.

    – Michael Dörner

    17. Juli 2015 um 7:54 Uhr


  • @mher Ich habe deinen Code getestet, aber ich bekomme nur 599 Antwortcode. Wissen Sie nicht, wo ein Problem sein kann? Vielen Dank

    – Joozty

    28. November 2017 um 13:30 Uhr

  • @mher – Wenn ich überhaupt nicht an der Antwort interessiert bin, also nur so viele Anfragen wie möglich so schnell wie möglich an den Server senden möchte, was (falls vorhanden) sollte ich im obigen Beispiel ändern? Vielen Dank !!

    – Guy Avraham

    9. Dezember 2017 um 11:05 Uhr

  • @Guy Avraham Viel Glück, wenn Sie Hilfe bei Ihrem DDoS-Plan erhalten.

    – Walther

    24. Januar 2019 um 8:56 Uhr

  • @Walter – du hast mich erwischt 🙂 Eigentlich wollte ich einen sehr naiven “Stresstest” machen

    – Guy Avraham

    24. Januar 2019 um 14:20 Uhr

Benutzer-Avatar
Marius Stănescu

Ich weiß, dass dies eine alte Frage ist, aber in Python 3.7 können Sie dies mit tun asyncio und 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 __name__ == "__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))

Sie können mehr darüber lesen und ein Beispiel sehen hier.

Threads sind hier absolut nicht die Antwort. Sie bieten sowohl Prozess- als auch Kernel-Engpässe sowie Durchsatzgrenzen, die nicht akzeptabel sind, wenn das Gesamtziel “der schnellste Weg” ist.

Ein kleines Bisschen von twisted und es ist asynchron HTTP Client würde Ihnen viel bessere Ergebnisse liefern.

  • ironfroggy: Ich tendiere zu deinen Gefühlen. Ich habe versucht, meine Lösung mit Threads und Warteschlangen (für automatische Mutexe) zu implementieren, aber können Sie sich vorstellen, wie lange es dauert, eine Warteschlange mit 100.000 Dingen zu füllen? Ich spiele immer noch mit verschiedenen Optionen und Vorschlägen von allen in diesem Thread herum, und vielleicht ist Twisted eine gute Lösung.

    – IgorGanapolsky

    13. April 2010 um 20:30 Uhr

  • Sie können vermeiden, eine Warteschlange mit 100.000 Dingen zu füllen. Verarbeiten Sie einfach die Elemente einzeln aus Ihrer Eingabe und starten Sie dann einen Thread, um die Anfrage zu verarbeiten, die jedem Element entspricht. (Wie ich unten beschreibe, verwenden Sie einen Launcher-Thread, um die HTTP-Anforderungs-Threads zu starten, wenn Ihre Thread-Anzahl unter einem bestimmten Schwellenwert liegt. Lassen Sie die Threads die Ergebnisse in ein Diktat schreiben, das die URL der Antwort zuordnet, oder hängen Sie Tupel an eine Liste an.)

    – Erik Garnison

    13. April 2010 um 20:59 Uhr

  • ironfroggy: Außerdem bin ich neugierig, welche Engpässe Sie bei der Verwendung von Python-Threads gefunden haben? Und wie interagieren Python-Threads mit dem OS-Kernel?

    – Erik Garnison

    13. April 2010 um 21:01 Uhr

  • Stellen Sie sicher, dass Sie den Epoll-Reaktor installieren; Andernfalls verwenden Sie select/poll, und es wird sehr langsam sein. Wenn Sie tatsächlich versuchen, 100.000 Verbindungen gleichzeitig geöffnet zu haben (vorausgesetzt, Ihr Programm ist so geschrieben und die URLs befinden sich auf verschiedenen Servern), müssen Sie Ihr Betriebssystem so einstellen, dass es Ihnen nicht ausgeht von Dateideskriptoren, ephemeren Ports usw. (es ist wahrscheinlich einfacher, einfach sicherzustellen, dass Sie nicht mehr als beispielsweise 10.000 ausstehende Verbindungen auf einmal haben).

    – Mark Nottingham

    15. April 2010 um 2:59 Uhr

  • erikg: du hast eine tolle idee empfohlen. Das beste Ergebnis, das ich mit 200 Fäden erzielen konnte, war jedoch ca. 6 Minuten. Ich bin mir sicher, dass es Möglichkeiten gibt, dies in kürzerer Zeit zu erreichen … Mark N: Wenn ich mich für Twisted entscheide, dann ist Epoll Reactor sicherlich nützlich. Wenn mein Skript jedoch von mehreren Computern ausgeführt wird, würde das nicht die Installation von Twisted auf JEDEM Computer erfordern? Ich weiß nicht, ob ich meinen Chef überzeugen kann, diesen Weg zu gehen …

    – IgorGanapolsky

    27. April 2010 um 18:52 Uhr

Benutzer-Avatar
Akshay Pratap Singh

Verwenden wünsche es ist eine Kombination aus Anfragen + Gevent-Modul .

GRequests ermöglicht es Ihnen, Anfragen mit Gevent zu verwenden, um asynchrone HTTP-Anfragen einfach zu stellen.

Die Verwendung ist einfach:

import grequests

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

Erstellen Sie eine Reihe nicht gesendeter Anfragen:

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

Senden Sie sie alle gleichzeitig:

>>> grequests.map(rs)
[<Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>]

  • ironfroggy: Ich tendiere zu deinen Gefühlen. Ich habe versucht, meine Lösung mit Threads und Warteschlangen (für automatische Mutexe) zu implementieren, aber können Sie sich vorstellen, wie lange es dauert, eine Warteschlange mit 100.000 Dingen zu füllen? Ich spiele immer noch mit verschiedenen Optionen und Vorschlägen von allen in diesem Thread herum, und vielleicht ist Twisted eine gute Lösung.

    – IgorGanapolsky

    13. April 2010 um 20:30 Uhr

  • Sie können vermeiden, eine Warteschlange mit 100.000 Dingen zu füllen. Verarbeiten Sie einfach die Elemente einzeln aus Ihrer Eingabe und starten Sie dann einen Thread, um die Anfrage zu verarbeiten, die jedem Element entspricht. (Wie ich unten beschreibe, verwenden Sie einen Launcher-Thread, um die HTTP-Anforderungs-Threads zu starten, wenn Ihre Thread-Anzahl unter einem bestimmten Schwellenwert liegt. Lassen Sie die Threads die Ergebnisse in ein Diktat schreiben, das die URL der Antwort zuordnet, oder hängen Sie Tupel an eine Liste an.)

    – Erik Garnison

    13. April 2010 um 20:59 Uhr

  • ironfroggy: Außerdem bin ich neugierig, welche Engpässe Sie bei der Verwendung von Python-Threads gefunden haben? Und wie interagieren Python-Threads mit dem OS-Kernel?

    – Erik Garnison

    13. April 2010 um 21:01 Uhr

  • Stellen Sie sicher, dass Sie den Epoll-Reaktor installieren; Andernfalls verwenden Sie select/poll, und es wird sehr langsam sein. Wenn Sie tatsächlich versuchen, 100.000 Verbindungen gleichzeitig geöffnet zu haben (vorausgesetzt, Ihr Programm ist so geschrieben und die URLs befinden sich auf verschiedenen Servern), müssen Sie Ihr Betriebssystem so einstellen, dass es Ihnen nicht ausgeht von Dateideskriptoren, ephemeren Ports usw. (es ist wahrscheinlich einfacher, einfach sicherzustellen, dass Sie nicht mehr als beispielsweise 10.000 ausstehende Verbindungen auf einmal haben).

    – Mark Nottingham

    15. April 2010 um 2:59 Uhr

  • erikg: du hast eine tolle idee empfohlen. Das beste Ergebnis, das ich mit 200 Fäden erzielen konnte, war jedoch ca. 6 Minuten. Ich bin mir sicher, dass es Möglichkeiten gibt, dies in kürzerer Zeit zu erreichen … Mark N: Wenn ich mich für Twisted entscheide, dann ist Epoll Reactor sicherlich nützlich. Wenn mein Skript jedoch von mehreren Computern ausgeführt wird, würde das nicht die Installation von Twisted auf JEDEM Computer erfordern? Ich weiß nicht, ob ich meinen Chef überzeugen kann, diesen Weg zu gehen …

    – IgorGanapolsky

    27. April 2010 um 18:52 Uhr

(Notiz an mich selbst für das nächste Projekt)

Nur Python 3-Lösung verwenden requests. Es ist am einfachsten und es ist schnell, keine Notwendigkeit für Multiprocessing oder komplizierte asynchrone Bibliotheken.

Der wichtigste Aspekt ist die Wiederverwendung von Verbindungen, insbesondere für HTTPS (TLS erfordert einen zusätzlichen Roundtrip zum Öffnen). Beachten Sie, dass eine Verbindung spezifisch für eine Subdomain ist. Wenn Sie viele Seiten auf vielen Domains kratzen, können Sie die Liste der URLs sortieren, um die Wiederverwendung von Verbindungen zu maximieren (es wird effektiv nach Domain sortiert).

Es ist so schnell wie jeder asynchrone Code, wenn genügend Threads vorhanden sind. (Anfragen geben die Python-GIL frei, wenn sie auf die Antwort warten).

[Production grade code with some logging and error handling]

import logging
import requests
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

# source: https://stackoverflow.com/a/68583332/5994461

THREAD_POOL = 16

# This is how to create a reusable connection pool with python requests.
session = requests.Session()
session.mount(
    'https://',
    requests.adapters.HTTPAdapter(pool_maxsize=THREAD_POOL,
                                  max_retries=3,
                                  pool_block=True)
)

def get(url):
    response = session.get(url)
    logging.info("request was completed in %s seconds [%s]", response.elapsed.total_seconds(), response.url)
    if response.status_code != 200:
        logging.error("request failed, error code %s [%s]", response.status_code, response.url)
    if 500 <= response.status_code < 600:
        # server is overloaded? give it a break
        time.sleep(5)
    return response

def download(urls):
    with ThreadPoolExecutor(max_workers=THREAD_POOL) as executor:
        # wrap in a list() to wait for all requests to complete
        for response in list(executor.map(get, urls)):
            if response.status_code == 200:
                print(response.content)

def main():
    logging.basicConfig(
        format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s",
        level=logging.INFO,
        datefmt="%Y-%m-%d %H:%M:%S"
    )

    urls = [
        "https://httpstat.us/200",
        "https://httpstat.us/200",
        "https://httpstat.us/200",
        "https://httpstat.us/404",
        "https://httpstat.us/503"
    ]

    download(urls)

if __name__ == "__main__":
    main()

  • Was meinst du mit ~”URLs sortieren“?

    – IgorGanapolsky

    30. Juli 2021 um 13:44 Uhr

  • Sortieren Sie die Liste der URLs sorted(urls)

    – Benutzer5994461

    30. Juli 2021 um 19:17 Uhr

  • Wenn Sie anfangen, auf 400-Fehler “Anforderung/Cookie zu groß” zu stoßen, arbeiten Sie eine session.cookies.clear() in Ihren Code ein.

    – Grantr

    29. April um 19:35 Uhr

1093010cookie-checkWas ist der schnellste Weg, um 100.000 HTTP-Anfragen in Python zu senden?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy