如何用最快的方式发送 10 万个 HTTP 请求

开发 架构
假如有一个文件,里面有 10 万个 url,需要对每个 url 发送 http 请求,并打印请求结果的状态码,如何编写代码尽可能快的完成这些任务呢?

[[419539]]

假如有一个文件,里面有 10 万个 url,需要对每个 url 发送 http 请求,并打印请求结果的状态码,如何编写代码尽可能快的完成这些任务呢?

Python 并发编程有很多方法,多线程的标准库 threading,concurrency,协程 asyncio,当然还有 grequests 这种异步库,每一个都可以实现上述需求,下面一一用代码实现一下,本文的代码可以直接运行,给你以后的并发编程作为参考:

队列+多线程

定义一个大小为 400 的队列,然后开启 200 个线程,每个线程都是不断的从队列中获取 url 并访问。

主线程读取文件中的 url 放入队列中,然后等待队列中所有的元素都被接收和处理完毕。代码如下:

  1. from threading import Thread 
  2. import sys 
  3. from queue import Queue 
  4. import requests 
  5.  
  6. concurrent = 200 
  7.  
  8.  
  9. def doWork(): 
  10.     while True
  11.         url = q.get() 
  12.         status, url = getStatus(url) 
  13.         doSomethingWithResult(status, url) 
  14.         q.task_done() 
  15.  
  16.  
  17. def getStatus(ourl): 
  18.     try: 
  19.         res = requests.get(ourl) 
  20.         return res.status_code, ourl 
  21.     except
  22.         return "error", ourl 
  23.  
  24.  
  25. def doSomethingWithResult(status, url): 
  26.     print(status, url) 
  27.  
  28.  
  29. q = Queue(concurrent * 2) 
  30. for i in range(concurrent): 
  31.     t = Thread(target=doWork) 
  32.     t.daemon = True 
  33.     t.start() 
  34.  
  35. try: 
  36.     for url in open("urllist.txt"): 
  37.         q.put(url.strip()) 
  38.     q.join() 
  39. except KeyboardInterrupt: 
  40.     sys.exit(1) 

运行结果如下:

有没有 get 到新技能?

线程池

如果你使用线程池,推荐使用更高级的 concurrent.futures 库:

  1. import concurrent.futures 
  2. import requests 
  3.  
  4. out = [] 
  5. CONNECTIONS = 100 
  6. TIMEOUT = 5 
  7.  
  8. urls = [] 
  9. with open("urllist.txt"as reader: 
  10.     for url in reader: 
  11.         urls.append(url.strip()) 
  12.  
  13. def load_url(url, timeout): 
  14.     ans = requests.get(url, timeout=timeout) 
  15.     return ans.status_code 
  16.  
  17. with concurrent.futures.ThreadPoolExecutor(max_workers=CONNECTIONS) as executor: 
  18.     future_to_url = (executor.submit(load_url, url, TIMEOUT) for url in urls) 
  19.     for future in concurrent.futures.as_completed(future_to_url): 
  20.         try: 
  21.             data = future.result() 
  22.         except Exception as exc: 
  23.             data = str(type(exc)) 
  24.         finally: 
  25.             out.append(data) 
  26.             print(data) 

协程 + aiohttp

协程也是并发非常常用的工具了:

  1. import asyncio 
  2. from aiohttp import ClientSession, ClientConnectorError 
  3.  
  4. async def fetch_html(url: str, session: ClientSession, **kwargs) -> tuple: 
  5.     try: 
  6.         resp = await session.request(method="GET", url=url, **kwargs) 
  7.     except ClientConnectorError: 
  8.         return (url, 404) 
  9.     return (url, resp.status) 
  10.  
  11. async def make_requests(urls: set, **kwargs) -> None: 
  12.     async with ClientSession() as session: 
  13.         tasks = [] 
  14.         for url in urls: 
  15.             tasks.append( 
  16.                 fetch_html(url=url, session=session, **kwargs) 
  17.             ) 
  18.         results = await asyncio.gather(*tasks) 
  19.  
  20.     for result in results: 
  21.         print(f'{result[1]} - {str(result[0])}'
  22.  
  23. if __name__ == "__main__"
  24.     import sys 
  25.     assert sys.version_info >= (3, 7), "Script requires Python 3.7+." 
  26.     with open("urllist.txt"as infile: 
  27.         urls = set(map(str.strip, infile)) 
  28.     asyncio.run(make_requests(urls=urls)) 

grequests[1]

这是个第三方库,目前有 3.8K 个星,就是 Requests + Gevent[2],让异步 http 请求变得更加简单。Gevent 的本质还是协程。

使用前:

  1. pip install grequests 

使用起来那是相当的简单:

  1. import grequests 
  2.  
  3. urls = [] 
  4. with open("urllist.txt"as reader: 
  5.     for url in reader: 
  6.         urls.append(url.strip()) 
  7.  
  8. rs = (grequests.get(u) for u in urls) 
  9.  
  10. for result in grequests.map(rs): 
  11.     print(result.status_code, result.url) 

注意 grequests.map(rs) 是并发执行的。运行结果如下:

也可以加入异常处理:

  1. >>> def exception_handler(request, exception): 
  2. ...    print("Request failed"
  3.  
  4. >>> reqs = [ 
  5. ...    grequests.get('http://httpbin.org/delay/1', timeout=0.001), 
  6. ...    grequests.get('http://fakedomain/'), 
  7. ...    grequests.get('http://httpbin.org/status/500')] 
  8. >>> grequests.map(reqs, exception_handler=exception_handler) 
  9. Request failed 
  10. Request failed 
  11. [None, None, <Response [500]>] 

最后的话 

今天分享了并发 http 请求的几种实现方式,有人说异步(协程)性能比多线程好,其实要分场景看的,没有一种方法适用所有的场景,笔者就曾做过一个实验,也是请求 url,当并发数量超过 500 时,协程明显变慢。

 

责任编辑:武晓燕 来源: Python七号
相关推荐

2017-09-15 09:43:59

Go语言web请求开发

2022-03-24 14:49:57

HTTP前端

2023-11-27 08:57:24

GoGET

2022-07-03 17:55:53

HTTP页面浏览器

2021-07-27 14:50:15

axiosHTTP前端

2022-11-22 08:41:22

curlDELETELinux

2010-09-08 15:35:35

2022-04-14 20:43:24

JavaScript原型链

2011-08-03 14:33:54

IOS4.2 HTTP 请求

2023-09-14 08:16:51

2021-03-06 09:54:22

PythonHTTP请求头

2021-11-06 19:43:34

Python微信服务器

2022-06-21 15:00:01

Python语言循环方式

2018-02-24 16:15:03

PythonHTTP

2020-05-27 11:31:43

Python 开发程序员

2023-09-19 22:41:30

控制器HTTP

2018-01-04 09:46:48

PHPHTTP

2013-03-22 17:34:07

BlackBerry1BB10黑莓

2017-04-24 14:39:01

PythonHTTP语言

2017-08-31 15:20:03

PythonPython3HTTP
点赞
收藏

51CTO技术栈公众号