用Python发送100,000个HTTP请求的最快方法是什么?


285

我正在打开一个具有100,000个URL的文件。我需要向每个URL发送一个HTTP请求并打印状态代码。我正在使用Python 2.6,到目前为止,我们研究了Python实现线程/并发性的许多令人困惑的方式。我什至看过python 并发库,但无法弄清楚如何正确编写此程序。有没有人遇到过类似的问题?我想通常我需要知道如何尽快地在Python中执行数千个任务-我想这意味着“同时”。


47
确保只执行HEAD请求(这样就不会下载整个文档)。请参阅:stackoverflow.com/questions/107405/...
Tarnay卡尔曼

5
很好,卡尔米。如果Igor想要的只是请求的状态,那么这10万个请求将执行得非常快。快多了
亚当·克罗斯兰

1
您不需要线程。最有效的方法可能是使用Twisted之类的异步库。
jemfinch

3
这是基于gevent,twisted和asyncio的代码示例(已对1000000个请求进行了测试)
jfs

4
@TarnayKálmán有可能requests.getrequests.head(即,页面请求与头部请求)返回不同的状态代码,所以这不是最佳建议
AlexG

Answers:


201

无捻解决方案:

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更少。


10
@Kalmi,为什么将Queue设置为concurrent*2
马塞尔·威尔逊

8
不要忘记关闭连接 conn.close()。打开太多的http连接可能会在某些时候停止脚本并占用内存。
Aamir Adnan 2014年

4
@hyh,该Queue模块已queue在Python 3中重命名为。这是Python 2代码。
TarnayKálmán2014年

3
如果您想通过持久连接每次与SAME服务器通信,可以走多快?甚至可以跨线程完成此操作,还是每个线程只有一个持久连接?
mdurant 2015年

2
@mptevsion,如果您使用的是CPython,则可以(例如)仅将“打印状态,URL”替换为“ my_global_list.append((status,url))”。由于GIL,(大多数操作)列表在CPython(和某些其他python实现)中是隐式线程安全的,因此这是安全的。
TarnayKálmán'3

54

使用龙卷风异步联网库的解决方案

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

7
该代码使用非阻塞网络I / O,没有任何限制。它可以扩展到成千上万的开放连接。它可以在单个线程中运行,但是比任何线程解决方案都快。结帐非阻塞I / O en.wikipedia.org/wiki/Asynchronous_I/O
mher

1
您能否解释一下全局i变量在这里发生了什么?某种错误检查?
LittleBobbyTables

4
它是确定何时退出ioloop的计数器-完成后就可以退出。
Michael Dorner

1
@AndrewScottEvans它假定你正在使用python 2.7和代理
Dejell

5
@Guy Avraham祝您在ddos计划上获得帮助。
Walter

50

自2010年发布以来,情况发生了很大变化,我还没有尝试所有其他答案,但是尝试了一些答案,我发现使用python3.6对我来说最有效。

我能够每秒获取约150个在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())

1
我只问是因为我不知道,但是可以用async / await代替这种期货吗?
TankorSmash

1
可以,但是我发现上面的方法效果更好。您可以使用aiohttp,但它不是标准库的一部分,并且正在发生很大变化。它确实可以工作,但我还没有发现它也可以工作。当我使用它时,我会得到更高的错误率,并且在我的生命中,我无法使它和并发期货一样好用,尽管从理论上讲,它似乎应该会更好,请参见:stackoverflow.com/questions/45800857/…如果您能正常使用,请发布答案,以便我进行测试。
Glen Thompson

1
这是一个顽固的选择,但我认为将它放在time1 = time.time()for循环的顶部和for循环time2 = time.time()之后很容易。
马特M.18年

我测试了您的代码段,以某种方式执行了两次。难道我做错了什么?还是要运行两次?如果是后一种情况,您还可以帮助我了解它如何触发两次吗?
罗尼

1
它不应运行两次。不知道为什么会这样。
格伦·汤普森

40

线程绝对不是这里的答案。如果总体目标是“最快的方法”,它们将提供进程和内核瓶颈,以及吞吐量限制。

一点点的twisted及其异步HTTP客户端将为您带来更好的结果。


讽刺:我倾向于你的观点。我尝试用线程和队列(用于自动互斥)实现我的解决方案,但是您能想象用100,000个东西填充一个队列需要多长时间?我在此线程上仍在与每个人讨论不同的选项和建议,也许Twisted将是一个很好的解决方案。
IgorGanapolsky 2010年

2
您可以避免在队列中填充10万个内容。只需一次从输入中处理一个项目,然后启动一个线程来处理与每个项目相对应的请求。(如下所述,当您的线程数低于某个阈值时,请使用启动器线程启动HTTP请求线程。使线程将结果写到dict映射URL中以进行响应,或将元组追加到列表中。)
Erik驻军2010年

ironfroggy:另外,我很好奇您使用Python线程发现了哪些瓶颈?Python线程如何与OS内核交互?
艾里克·加里森

确保安装了epoll反应器;否则,您将使用选择/轮询,这会非常慢。另外,如果您实际上要尝试同时打开100,000个连接(假设程序是用这种方式编写的,并且URL在不同的服务器上),则需要调整操作系统,以免耗尽包括文件描述符,临时端口等信息(仅一次确保您没有超过10,000个未完成的连接可能会更容易)。
马克·诺丁汉

erikg:您确实推荐了一个好主意。但是,我用200个线程可以获得的最佳结果是大约。6分钟 我敢肯定有很多方法可以在更短的时间内完成此任务……Mark N:如果我决定采用Twisted,那epoll反应器肯定有用。但是,如果我的脚本将在多台计算机上运行,​​那是否不需要在每台计算机上安装Twisted?我不知道我能否说服老板走那条路……
IgorGanapolsky 2010年

20

我知道这是一个古老的问题,但是在Python 3.7中,您可以使用asyncio和来实现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))

您可以阅读有关它的更多信息,并在此处查看示例。


这类似于C#异步/等待和Kotlin协程吗?
IgorGanapolsky

@IgorGanapolsky,是的,它与C#async / await非常相似。我对Kotlin协程不熟悉。
MariusStănescu,

@sandyp,我不确定它是否有效,但是如果要尝试,则必须将UnixConnector用于aiohttp。在此处阅读更多信息:docs.aiohttp.org/en/stable/client_reference.html#connectors
MariusStănescu19年

谢谢@MariusStănescu。那正是我用的。
sandyp '19

+1用于显示asyncio.gather(* tasks)。这是我使用的一个urls= [fetch(construct_fetch_url(u),idx) for idx, u in enumerate(some_URI_list)] results = await asyncio.gather(*urls)
摘要

19

使用grequests,它是request + Gevent模块的组合。

GRequests允许您将Requests与Gevent一起使用,以轻松进行异步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]>]

7
gevent现在支持python 3
Benjamin Toueg

14
grequests不是正常请求的一部分,并且似乎基本上不需要维护
Thom

8

解决此问题的一种好方法是首先编写获得一个结果所需的代码,然后合并线程代码以并行化应用程序。

在理想情况下,这仅意味着同时启动100,000个线程,这些线程会将其结果输出到字典或列表中以供以后处理,但是实际上,您可以通过这种方式发出多少个并行HTTP请求受到限制。在本地,您可以同时打开多少个套接字,Python解释器允许多少个执行线程受到限制。从远程来看,如果所有请求都针对一台或多台服务器,则同时连接的数量可能会受到限制。这些限制可能导致您必须以一种方式来编写脚本,以便在任何时候仅轮询一小部分网址(如另一位发帖人所述,100可能是一个不错的线程池大小,尽管您可能会发现自己可以成功部署更多)。

您可以按照以下设计模式来解决上述问题:

  1. 启动一个线程以启动新的请求线程,直到当前正在运行的线程(您可以通过threading.active_count()或通过将线程对象推入数据结构来跟踪它们)的数量大于等于您的同时请求的最大数量(例如100) ,然后短暂睡眠。没有更多URL可处理时,该线程应终止。因此,线程将不断唤醒,启动新线程并进入休眠状态,直到完成操作为止。
  2. 让请求线程将其结果存储在某种数据结构中,以供以后检索和输出。如果您要在其中存储结果的结构是C listdictCPython,则可以安全地从线程中添加或插入不带锁的唯一项,但是如果您写入文件或需要更复杂的跨线程数据交互,则应使用a互斥锁,以保护该状态免于腐败

我建议您使用线程模块。您可以使用它来启动和跟踪正在运行的线程。Python的线程支持是裸露的,但是对问题的描述表明它完全可以满足您的需求。

最后,如果你想看到用Python编写的并行网络应用的一个非常简单的应用程序,请ssh.py。这是一个小型库,使用Python线程并行化许多SSH连接。该设计足够接近您的要求,您可能会发现它是很好的资源。


1
erikg:将队列放入等式是否合理(对于互斥锁定)?我怀疑Python的GIL是否适合使用数千个线程。
IgorGanapolsky

为什么需要互斥锁定以防止生成过多线程?我怀疑我误解了这个词。您可以跟踪线程队列中正在运行的线程,在它们完成后将其删除,并添加更多达到上述线程限制的线程。但是在诸如上述问题的简单情况下,您也可以仅观察当前Python进程中的活动线程数,等待其降至阈值以下,然后按所述启动更多线程直至阈值。我猜您可以认为这是一个隐式锁,但是不需要显式锁。
艾里克·加里森

erikg:多个线程不共享状态吗?在O'Reilly的书“ Python for Unix和Linux系统管理”的第305页上,它指出:“ ...使用无队列线程使得它比许多人实际可以处理的更为复杂。始终使用排队是一个更好的主意。如果发现需要使用线程模块,为什么?因为队列模块还减轻了使用互斥锁显式保护数据的需要,因为队列本身已经在内部受到互斥锁的保护。” 再次,我欢迎您对此发表看法。
IgorGanapolsky 2010年

伊戈尔:绝对正确,应该使用锁。我已经修改了帖子以反映这一点。就是说,python的实际经验表明,您不需要锁定从线程中自动修改的数据结构,例如通过list.append或通过添加哈希键。我相信,原因是GIL,它提供了list.append之类的操作,并且具有一定程度的原子性。我目前正在运行测试以验证这一点(使用10k线程将数字0-9999追加到列表中,检查所有追加是否有效)。经过近100次迭代后,测试并未失败。
艾里克·加里森

伊戈尔:有人问我有关这个主题的另一个问题是: stackoverflow.com/questions/2740435/...
埃里克·加里森

7

如果希望获得最佳性能,则可能要考虑使用异步I / O而不是线程。与成千上万个OS线程相关的开销是不平凡的,并且Python解释器中的上下文切换在此之上增加了更多。线程化肯定会完成工作,但是我怀疑异步路由会提供更好的整体性能。

具体来说,我建议在Twisted库(http://www.twistedmatrix.com)中使用异步Web客户端。它具有公认的陡峭的学习曲线,但是一旦您掌握了Twisted的异步编程风格,就很容易使用。

Twisted的异步Web客户端API的HowTo可在以下位置找到:

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


Rakis:我目前正在研究异步和非阻塞I / O。在实施之前,我需要更好地学习它。我想在您的帖子中发表的评论是(至少在我的Linux发行版中)不可能产生“数千个OS线程”。在程序中断之前,Python将允许您产生最大数量的线程。而在我的情况(在CentOS 5)的最大线程数为303
IgorGanapolsky

很高兴知道。我从来没有尝试过一次在多个Python中生成多个代码,但是我希望能够在爆炸之前创建更多代码。
拉基斯

6

一个解法:

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

平时:

bix.hu is ~10 ms away from me
godaddy.com: ~170 ms
google.com: ~30 ms

6
将Twisted用作线程池会忽略您可以从中获得的大多数好处。您应该使用异步HTTP客户端。
Jean-Paul Calderone 2010年

1

使用线程池是一个不错的选择,这将使此操作相当容易。不幸的是,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()

希望这可以帮助。


我建议您像这样为ThreadPool指定q_size:ThreadPool(poolsize,q_size = 1000)这样在内存中就不会有100000个WorkRequest对象。“如果q_size> 0作品的尺寸请求队列的限制和线程池块,当队列已满,它试图把更多的工作请求中(见putRequest方法),除非你也可以使用一个积极的timeout价值putRequest。”
TarnayKálmán2010年

到目前为止,我正在尝试实现线程池解决方案-如建议的那样。但是,我不了解makeRequests函数中的参数列表。什么是some_callable,list_of_args,回调?也许如果我看到一个有帮助的真实代码片段。我很惊讶该库的作者未发布任何示例。
IgorGanapolsky 2010年

some_callable是您所有工作都完成的功能(连接到http服务器)。list_of_args是将传递给some_callabe的参数。回调是在工作线程完成时将调用的函数。它有两个参数,工作对象(不需要真正关心您的自我)和工作人员检索到的结果。
凯文·维斯基亚

1

创建epoll对象,
开放许多客户端TCP套接字,
调整自己的发送缓冲区是有点超过请求头,
发送一个请求头-它应该是即时的,只是放置到一个缓冲区,在注册插座epoll对象,
.pollepollobect,
阅读前3来自每个套接字的字节.poll
将其写入sys.stdout后跟\n(不要刷新),关闭客户端套接字。

限制同时打开的套接字数-创建套接字时处理错误。仅当另一个插座关闭时,才创建一个新的插座。
调整操作系统限制。
尝试分叉到几个(不是很多)进程:这可能有助于更有效地使用CPU。


@IgorGanapolsky一定是。否则我会感到惊讶。但这当然需要实验。
乔治·索维托夫

0

对于您的情况,线程可能会成功,因为您可能会花费大量时间等待响应。标准库中有一些有用的模块(例如Queue)可能会有所帮助。

我之前并行下载文件时也做了类似的事情,这对我来说已经足够了,但是这并不是您正在谈论的范围。

如果您的任务更多地受CPU限制,则可能需要查看多处理模块,该模块将允许您利用更多的CPU /核心/线程(更多的进程不会互相阻塞,因为锁定是按进程进行的)


我唯一要提及的是,产生多个进程可能比产生多个线程更昂贵。此外,通过多个进程与多个线程发送100,000个HTTP请求也没有明显的性能提升。
IgorGanapolsky 2010年

0

考虑使用Windmill,尽管Windmill可能无法执行那么多线程。

您可以在5台计算机上使用手动滚动的Python脚本来完成此操作,每台计算机都使用端口40000-60000连接出站,从而打开100,000个端口连接。

同样,使用线程良好的QA应用程序(例如OpenSTA) 进行示例测试可能会有所帮助,以便了解每个服务器可以处理的数量。

另外,尝试研究仅将简单的Perl与LWP :: ConnCache类一起使用。这样,您可能会获得更多的性能(更多的连接)。


0

这个扭曲的异步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)

-2

最简单的方法是使用Python的内置线程库。它们不是“真实的” /内核线程,它们有问题(例如序列化),但是足够好。您需要一个队列和线程池。一种选择在这里,但是编写自己的东西很简单。您无法并行处理所有100,000个呼叫,但是可以同时触发100个(或大约)呼叫。


7
Python的线程是真实的,例如与Ruby相对。在后台,它们至少在Unix / Linux和Windows上作为本机OS线程实现。也许您指的是GIL,但这并不会使线程的真实性降低……
Eli Bendersky 2010年

2
Eli对Python的线程是正确的,但是Pestilence的观点是您想使用线程池也是正确的。在这种情况下,您要做的最后一件事是尝试同时为每个100K请求启动一个单独的线程。
亚当·克罗斯兰

1
伊戈尔(Igor),您无法明智地在注释中张贴代码段,但是您可以编辑问题并将其添加到注释中。
亚当·克罗斯兰

Pestilence:对于我的解决方案,您会建议多少个队列和每个队列线程数?
IgorGanapolsky 2010年

加上这是一个I / O绑定任务而不是CPU绑定,GIL在很大程度上影响了CPU绑定任务
PirateApp '18
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.