web-dev-qa-db-ja.com

Pythonで100,000個のHTTPリクエストを送信する最速の方法は何ですか?

100,000個のURLを持つファイルを開いています。各URLにHTTPリクエストを送信し、ステータスコードを印刷する必要があります。私はPython 2.6を使用していますが、これまでPythonがスレッド化/並行性を実装する多くの紛らわしい方法を見てきました。 python concurrence ライブラリも見てきましたが、このプログラムを正しく書く方法がわかりません。誰かが同様の問題に遭遇しましたか?一般的に、Pythonで可能な限り高速に何千ものタスクを実行する方法を知る必要があると思います。これは「同時に」を意味すると思います。

228
IgorGanapolsky

ツイストレスソリューション:

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)

これは、ツイストソリューションよりもわずかに高速であり、使用するCPUが少なくなります。

179
Tarnay Kálmán

tornado 非同期ネットワークライブラリを使用したソリューション

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

ここではスレッドは絶対に答えではありません。プロセスとカーネルの両方のボトルネック、および全体的な目標が「最速の方法」である場合に許容できないスループット制限を提供します。

少しのtwistedとその非同期HTTPクライアントを使用すると、より良い結果が得られます。

34
ironfroggy

これが投稿された2010年以降、状況はかなり変化しており、他のすべての答えを試していませんが、いくつか試しましたが、python3.6を使用して私にとってこれが最適に機能することがわかりました。

AWSで実行されている1秒あたり約150個の一意のドメインを取得できました。

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())
29
Glen Thompson

grequests を使用します。これはリクエストとGeventモジュールの組み合わせです。

GRequestsを使用すると、GeventでRequestsを使用して、非同期HTTP要求を簡単に作成できます。

使い方は簡単です:

import grequests

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

未送信のリクエストのセットを作成します。

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

すべて同時に送信する:

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

この問題を解決する良い方法は、最初に1つの結果を得るために必要なコードを記述し、次にスレッド化コードを組み込んでアプリケーションを並列化することです。

完璧な世界では、これは結果を辞書またはリストに出力して後で処理する100,000スレッドを同時に開始することを意味しますが、実際にはこの方法で発行できる並行HTTPリクエストの数に制限があります。ローカルでは、同時に開くことができるソケットの数、Pythonインタープリターが許可する実行スレッドの数に制限があります。リモートでは、すべての要求が1つまたは多くのサーバーに対するものである場合、同時接続の数が制限される場合があります。これらの制限により、URLのごく一部のみを一度にポーリングするような方法でスクリプトを記述することがおそらく必要になります(別の投稿者が述べたように、100はおそらくまともなスレッドプールサイズです。ただし、より多くを正常に展開できます)。

この設計パターンに従って、上記の問題を解決できます。

  1. 現在実行中のスレッドの数(threading.active_count()またはスレッドオブジェクトをデータ構造にプッシュすることで追跡できる)の数が同時要求の最大数(たとえば100)になるまで、新しい要求スレッドを起動するスレッドを開始します、その後、短いタイムアウトの間スリープします。このスレッドは、処理するURLがなくなると終了するはずです。したがって、スレッドは目覚め、新しいスレッドを起動し、終了するまでスリープし続けます。
  2. 後で取得して出力できるように、リクエストスレッドに結果を何らかのデータ構造に保存させます。結果を保存する構造がCPythonのlistまたはdictである場合、 ロックなしでスレッドから一意のアイテムを安全に追加または挿入 できますが、ファイルに書き込むか、より複雑な場合はクロススレッドデータの相互作用この状態を破損から保護するには、相互排他ロックを使用する必要があります

threading モジュールを使用することをお勧めします。これを使用して、実行中のスレッドを起動および追跡できます。 Pythonのスレッドサポートはむき出しになっていますが、問題の説明は、ニーズに完全に対応していることを示唆しています。

最後に、Pythonで記述された並列ネットワークアプリケーションの非常に単純なアプリケーションを確認したい場合は、 ssh.py を確認してください。 Pythonスレッディングを使用して多くのSSH接続を並列化する小さなライブラリです。設計は要件に十分に近いため、適切なリソースであることがわかります。

7
Erik Garrison

可能な限り最高のパフォーマンスを得たい場合は、スレッドではなく非同期I/Oの使用を検討することをお勧めします。数千のOSスレッドに関連するオーバーヘッドは些細なものではなく、Pythonインタープリター内のコンテキスト切り替えにより、さらに多くのスレッドが追加されます。スレッド化は確かに仕事を成し遂げますが、非同期ルートは全体的なパフォーマンスを向上させると思います。

具体的には、Twistedライブラリ( http://www.twistedmatrix.com )で非同期Webクライアントを提案します。明らかに急な学習曲線を持っていますが、Twistedのスタイルの非同期プログラミングをうまく理解すれば、非常に使いやすくなります。

Twistedの非同期WebクライアントAPIのHowToは次の場所から入手できます

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

7
Rakis

解決策:

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

テスト時間:

[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

これは古い質問ですが、Python 3.7では、asyncioaiohttpを使用してこれを行うことができます。

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

詳細については、例を参照してください here

2

epollオブジェクトを作成し、
多くのクライアントTCPソケットを開き、
送信バッファを調整して、リクエストヘッダーより少し大きくします。
リクエストヘッダーを送信します。すぐにバッファに配置し、epollオブジェクトにソケットを登録し、
do .poll on epoll obect、
.pollの各ソケットから最初の3バイトを読み取り、
sys.stdoutに続いて\n(フラッシュしない)に書き込み、クライアントソケットを閉じます。

同時に開くソケットの数を制限します—ソケットの作成時にエラーを処理します。別のソケットが閉じている場合にのみ、新しいソケットを作成します。
OSの制限を調整します。
少数(多くはない)のプロセスに分岐してみてください。これにより、CPUをもう少し効率的に使用できる場合があります。

1
George Sovetov

スレッドプール を使用することは適切なオプションであり、これはかなり簡単になります。残念ながら、pythonには、スレッドプールを非常に簡単にする標準ライブラリがありません。しかし、ここにあなたが始めるべき適切なライブラリがあります: http://www.chrisarndt.de/projects/threadpool/

彼らのサイトからのコード例:

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

お役に立てれば。

1
Kevin Wiskia

このねじれた非同期Webクライアントは非常に高速です。

#!/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ᵩ

あなたの場合、おそらく応答を待つことにほとんどの時間を費やしているので、スレッド化はおそらくトリックを行うでしょう。標準ライブラリには Queue などの役立つモジュールがあります。

以前にファイルの並列ダウンロードで同様のことをしましたが、それで十分でしたが、あなたが話している規模ではありませんでした。

タスクがよりCPUにバインドされている場合は、 multiprocessing モジュールを確認することをお勧めします。これにより、より多くのCPU /コア/スレッド(ロックはプロセスごとです)

0
Mattias Nilsson

Windmill を使用することを検討してください。ただし、おそらくWindmillはそれほど多くのスレッドを実行できません。

5台のマシンで手動でPythonスクリプトを実行して、それぞれがポート40000〜60000を使用してアウトバウンドに接続し、100,000のポート接続を開くことができます。

また、各サーバーがどれだけ処理できるかを知るために、 OpenSTA などの適切にスレッド化されたQAアプリでサンプルテストを行うと役立つ場合があります。

また、LWP :: ConnCacheクラスで単純なPerlを使用するだけで調べてみてください。そうすれば、おそらくより多くのパフォーマンス(より多くの接続)が得られます。

0
djangofan