在数据分析的时候常常需要抓取外部的一些数据作为参考,平常用的比较多的Python工具是requests,今天要做的是针对Python的HTTP请求包做一个简单的盘点。
urllib
urllib 是 Python 标准库中的一个模块,它包含一系列用于处理URL和执行HTTP请求的函数和类。由于它是 Python 的一部分,因此无需额外安装即可使用。使用 urllib 可以执行一般的网络操作,如数据下载、API调用等。
urllib 的优点
- 内置模块:urllib是 Python 的内置模块,不需要额外安装,可以直接使用。
- 无依赖:由于是标准库的一部分,urllib不依赖于外部包,减少了依赖管理的复杂性。
- 功能全面:urllib提供了一系列工具,可以处理URL解析、编码、发送HTTP请求等任务。
- 简单用例简便:对于简单的操作,如发送一个GET或POST请求,urllib的使用非常直观。
urllib 的缺点
- API复杂:与一些第三方库相比,如requests,urllib 的API显得不够简洁和人性化,使用起来可能比较繁琐。
- 缺乏现代特性:urllib没有很好地原生支持一些现代HTTP功能,如连接池管理、多部分编码、WebSockets等。
- 不支持异步:urllib不支持异步操作,而现代Web开发中异步成为了常态。
- 手动处理重定向和重试:urllib不会自动处理HTTP重定向,也没有内置的重试机制。需要用户自行实现。
- 异常处理:urllib的异常处理相对繁琐,需要处理多种异常类型。
- 默认不支持会话:与requests 的Sessions功能相比,urllib 默认不支持连接复用和cookie持久化等会话特性。
由于上述缺点,许多开发者更倾向于使用外部库,如 requests,因为它提供了更简单、更人性化的API,并且拥有更多现代Web开发所需的功能。在处理复杂的HTTP请求,管理Cookies和Sessions,以及执行高级HTTP操作时,requests 能提供更加方便的支持。然而,如果你的项目需要最小化外部依赖,或者只需执行一些基本的HTTP操作,urllib 依然是个不错的选择。
urllib包括以下几个子模块,用以处理各种网络相关的任务:
- request:用来打开和读取 URL。
- error:包含由urllib.request 引发的异常。
- parse:用来解析 URL。
- robotparser:用来解析robots.txt 文件以判断网站爬虫协议。
urllib.request
这个子模块提供了最基本的 HTTP 请求功能。它可以用来GET或POST请求到服务器,并处理请求头、编码、HTTP认证等。
import urllib.request # 发送GET请求 response = urllib.request.urlopen('http://www.example.com') html = response.read() # 发送POST请求 data = urllib.parse.urlencode({'key1': 'value1', 'key2': 'value2'}).encode() response = urllib.request.urlopen('http://www.example.com', data=data) html = response.read()
urllib.error
这个子模块包含了由 urllib.request 引发的异常,比如 URLError 和 HTTPError。
from urllib.request import urlopen from urllib.error import URLError, HTTPError try: response = urlopen('http://www.example.com') except HTTPError as e: print('HTTP错误:', e.code, e.reason) except URLError as e: print('URL错误:', e.reason)
urllib.parse
这个模块提供了一系列用于解析和处理URL的函数。它可以用来分割、解析和组合URL。
from urllib.parse import urlparse, urlunparse, urlencode parsed_url = urlparse('http://www.example.com/test?arg=val') print(parsed_url) query_params = {'arg1': 'value1', 'arg2': 'value2'} encoded_query = urlencode(query_params) print(encoded_query)
urllib.robotparser
urllib.robotparser 用来解析网站的 robots.txt 文件,以判断当前用户代理是否有权限访问网站的某一部分。
from urllib.robotparser import RobotFileParser rp = RobotFileParser() rp.set_url('http://www.example.com/robots.txt') rp.read() can_fetch = rp.can_fetch('*', 'http://www.example.com/test') print(can_fetch)
个人使用感受是除了urllib.parse比较有用外,其他的都有更好的解决方案。
urllib3
urllib3 是 Python 中一个功能强大的,用于HTTP客户端连接的库。它比Python标准库中的urllib提供了更多的功能,并且它是requests库的底层依赖,提供了许多requests所使用的核心功能。
urllib3 的优点
- 连接池管理:urllib3 使用连接池来复用连接,从而减少了建立连接的开销,并提高了性能。
- 线程安全:urllib3 被设计为线程安全的,可以在多线程程序中安全使用。
- 自动内容解码:它可以自动处理来自服务器的压缩数据。
- 重试机制:提供了可配置的重试逻辑,可以在遇到暂时性的问题时自动重试请求。
- 超时配置:支持全局和请求级别的超时设置。
- SSL/TLS验证:支持SSL证书验证和主机名验证,以确保安全连接。
- 支持文件上传:可以处理多种表单数据类型,包括文件上传。
- 支持HTTP/HTTPS代理:可以配置HTTP代理,支持HTTP和HTTPS请求。
urllib3 的缺点
- API较低级:与requests相比,urllib3的API较为低级,可能需要编写更多的代码来实现相同的功能。
- 异常处理繁琐:urllib3可能会抛出一系列不同的异常,用户需要捕捉和处理这些异常。
- 不是标准库:虽然urllib3常常随系统安装,但它不是Python标准库的一部分,可能需要单独安装。
- 缺少人性化特性:urllib3缺少像requests那样简单的JSON编码和解码,认证机制等高级功能。
- 缺乏同步/异步一致性:urllib3是同步的,如果你需要异步支持,可能需要寻找其他库,如aiohttp或者httpx。
- 不自动处理重定向:默认情况下,urllib3不会自动处理重定向,开发者需要手动处理。
尽管有这些缺点,urllib3仍然是一个非常流行和可靠的HTTP库,对于需要精细控制HTTP请求的应用来说,它是一个很好的选择。对于需要更简单API的开发者,可以使用建立在urllib3之上的requests库,以获得更好的开发体验。
urllib3使用示例
# 发送请求 import urllib3 http = urllib3.PoolManager() # 发送GET请求 response = http.request('GET', 'http://www.example.com') # 发送POST请求 response = http.request('POST', 'http://www.example.com', fields={'key': 'value'}) print(response.status) print(response.data.decode('utf-8')) # 处理SSL和HTTPS # 通过指定证书路径来验证HTTPS请求 http = urllib3.PoolManager( cert_reqs='CERT_REQUIRED', ca_certs='/path/to/your/certificate_bundle' ) response = http.request('GET', 'https://www.example.com') # 重试逻辑 from urllib3.util.retry import Retry from urllib3 import PoolManager retries = Retry(connect=5, read=2, redirect=5) http = PoolManager(retries=retries) response = http.request('GET', 'http://www.example.com') # 响应处理 response = http.request('GET', 'http://www.example.com') print(response.status) # 响应状态码 print(response.headers) # 响应头 print(response.data) # 原始响应体
urllib3 是一个强大的HTTP客户端库,特别适用于那些需要细粒度控制HTTP请求的场景。由于其复杂性,对于简单的用例,许多开发人员可能倾向于使用更高级别的 requests 库,它在内部使用 urllib3 但提供了更简单的API。
Requests
requests 是一个使用 Python 编写的优雅且简单的 HTTP 库。它被设计来使得HTTP请求尽可能简单。由于其易用性和人性化的设计,requests 在Python社区中非常受欢迎,并广泛用于各种应用程序中,从Web开发到数据科学再到自动化脚本。
requests 的优点
- 简单易用:requests 的API设计直观易懂,即使是初学者也能快速上手。
- 人性化:提供了许多默认行为,例如自动内容解码、基本/摘要式HTTP认证以及自动处理持久连接等。
- 会话功能:可以使用会话对象 (Session) 实现持久化连接和cookie跨请求保持。
- 支持多种认证形式:支持多种认证机制,包括 OAuth 1/2。
- 自动解码内容:自动处理来自服务器的不同编码方式。
- 支持HTTPS:完全支持 HTTPS,包括 SSL 证书验证。
- 异常处理:拥有清晰的异常处理结构,易于捕获和处理网络请求过程中可能出现的错误。
- 支持多种格式的数据:可以轻松处理多种数据格式,如表单数据、JSON以及文件上传。
- 丰富的高级功能:支持连接超时、重定向历史、代理、流式上传/下载、Chunked 请求等高级HTTP特性。
requests 的缺点
- 同步阻塞:requests 是一个同步库,当发出请求时,程序会阻塞直到收到响应。这意味着在处理大量并发请求时可能效率不高。
- 性能问题:由于其同步阻塞的特性,在高并发或大规模分布式系统中,requests 的性能可能不如异步的HTTP客户端库。
- 外部依赖:requests 不是Python的内置库,需要单独安装,这可能会为一些需要最小化依赖的项目带来不便。
- 不支持异步:requests 本身不支持异步操作,如果你正在编写异步程序,你可能需要使用 aiohttp 等异步HTTP客户端库。
- 资源消耗:在创建没有被会话复用的单独请求时,requests 可能会消耗更多的系统资源,比如不必要地创建新的连接。
尽管存在上述缺点,requests 仍然是一个强大且极其流行的HTTP客户端库,适合于各种不同的开发场景。对于需要更高性能和并发的应用场景,可能需要考虑其他库,如 httpx(支持HTTP/1.1 和 HTTP/2,同步和异步接口)或 aiohttp(异步HTTP客户端/服务端框架)。
相比于Python的标准库 urllib,它的API使用起来更为简洁。
requests使用示例
# 发送GET请求 import requests response = requests.get('http://www.example.com') # 发送POST请求 import requests data = {'key1': 'value1', 'key2': 'value2'} response = requests.post('http://www.example.com', data=data) # 其他HTTP请求方法,除了GET和POST之外,requests 还支持PUT,DELETE,HEAD,OPTIONS等HTTP方法: requests.put('http://www.example.com', data={'key':'value'}) requests.delete('http://www.example.com') requests.head('http://www.example.com') requests.options('http://www.example.com') # 处理响应,requests 可以很方便地处理HTTP响应: response = requests.get('http://www.example.com') print(response.status_code) # 响应状态码 print(response.headers) # 响应头 print(response.text) # 响应内容 print(response.json()) # 如果响应内容是JSON,可以直接转换为Python dict # 处理URL参数,requests 可以很方便地生成带参数的URL: payload = {'key1': 'value1', 'key2': 'value2'} response = requests.get('http://www.example.com', params=payload) # 处理Cookies,requests 也可以轻松处理Cookies: response = requests.get('http://www.example.com') print(response.cookies['example_cookie_name']) # 设置请求头,可以通过字典设置HTTP请求头: headers = {'user-agent': 'my-app/0.0.1'} response = requests.get('http://www.example.com', headers=headers)
requests 库的功能非常丰富,以上只是最基本的用法。如果你想要进行更复杂的HTTP请求,比如处理会话和Cookie、上传文件、设置超时等,requests 库都可以很好地支持。更多可查看:Python网络请求库Requests使用技巧
GRequests
GRequests是一个 Python 库,它允许你使用requests库的接口进行异步HTTP请求。它基于gevent 库,gevent 是一个基于协程的Python网络库,可以轻松处理并发连接。GRequests使得你可以以非阻塞的方式来发起多个HTTP请求,并且在requests的友好接口的基础上提供异步功能。
GRequests 的优点
- 简单的 API:GRequests提供了一个类似于 requests 的 API,使得发送异步请求变得简单。
- 并发请求:可以非常容易地并发执行多个HTTP请求,而不需要复杂的异步编程模型。
- 轻量级并发:GRequests通过 gevent 协程实现并发,这通常比多线程更加轻量级和高效。
- 兼容requests:因为 GRequests 封装了 requests,所以它继承了 requests 的所有特性,包括易用的会话接口、自动解码响应内容、便捷的请求参数传递等。
- 快速开发:对于熟悉requests 的开发者来说,使用 GRequests 开发异步HTTP请求通常比较快速和直观。
GRequests 的缺点
- Monkey Patching:GRequests依赖于 gevent 的猴子补丁(monkey patching)来提供异步能力,这可能会影响全局状态,并且在一些情况下导致难以调试的问题。
- 不是原生异步:GRequests并不使用 Python 的原生 asyncio 库,因此可能不适合所有异步编程场景。
- 不支持 Python 的原生协程:与使用async 和 await 关键字的原生协程代码风格不兼容。
- 可能与其他库冲突:如果其他库也打了猴子补丁,或者对gevent 有特定的不兼容性,可能会出现问题。
- 维护和社区支持:与requests 和其他流行的异步HTTP库相比,GRequests 的维护和社区支持可能没有那么活跃。
- 性能限制:虽然GRequests 适合IO密集型任务,但在CPU密集型任务中,协程可能不如多线程或多进程。
GRequests 是一个有用的库,可以使得使用 requests 风格的代码更加简单地进行异步HTTP操作。但是,由于它不是基于原生的 asyncio 系统构建的,因此可能不是每个项目的最佳选项。在需要高性能异步HTTP客户端时,可以考虑使用 httpx 或 aiohttp 这样的库,这些库直接支持 asyncio 并且设计用于异步操作。
GRequests 使用示例
import grequests urls = [ 'http://www.heroku.com', 'http://python-tablib.org', 'http://httpbin.org', 'http://python-requests.org', 'http://kennethreitz.com' ] # 创建请求集 rs = (grequests.get(u) for u in urls) # 发送并发请求 responses = grequests.map(rs) for response in responses: if response is not None: print(response.url, response.status_code)
以上代码中,grequests.map 函数并发地发送所有的GET请求,并返回一个包含所有响应对象的列表。这些响应对象与 requests 库中的响应对象具有相同的接口。
需要注意的是,由于 GRequests 依赖 gevent,它可能不适合所有的环境。例如,在某些操作系统或者Python解释器中,gevent 的一些特性可能不被支持。此外,如果你的应用程序已经使用了其他形式的异步I/O或并发(如 asyncio 或多线程),那么使用 GRequests 可能会导致问题,因为 gevent 需要打补丁到标准库中才能以非阻塞的方式工作,这可能会与其他并发模型发生冲突。
aiohttp
aiohttp 是一个使用 asyncio 库提供异步网络服务的Python框架。它用于客户端和服务器端的编程,并支持WebSockets和HTTP协议。aiohttp 的HTTP客户端功能是建立在Python的异步和等待概念之上的,这意味着使用 aiohttp 时,你可以在一个非阻塞的方式下执行HTTP请求,并在其它任务同时进行的情况下等待响应。
主要特性
- 异步请求处理:aiohttp客户端支持异步发送HTTP请求,这使得它在处理多个并发连接时非常高效。
- 支持多种HTTP方法:GET、POST、PUT、DELETE等标准HTTP方法都得到支持。
- WebSockets支持:能够处理长连接,如WebSockets。
- 会话管理:提供ClientSession 类来管理和维护一个会话内的所有请求,包括重用连接池和Cookie持久化。
- 流式上传和下载:支持流式上传数据和下载响应内容。
- SSL支持:支持HTTPS连接和SSL证书验证。
使用示例
以下是一个简单的 aiohttp HTTP客户端的使用示例:
import aiohttp import asyncio # 定义异步函数来发送GET请求 async def fetch(session, url): async with session.get(url) as response: return await response.text() # 定义主异步函数 async def main(): # 创建ClientSession async with aiohttp.ClientSession() as session: html = await fetch(session, 'http://python.org') print(html) # 运行异步主函数 asyncio.run(main())
在上述代码中,ClientSession 被用来执行HTTP GET请求。在 async with 块中,session.get 方法是异步的,它将挂起当前任务,直到响应准备就绪,此时会自动处理上下文管理并在结束时关闭连接。response.text() 是一个异步操作,用于获取响应的文本内容。
使用 aiohttp 构建HTTP客户端时,你需要熟悉 asyncio 库,因为所有HTTP请求都是通过 async 和 await 关键字来处理的,确保了整个请求过程是非阻塞的。
aiohttp HTTP客户端功能强大且灵活,适合需要高性能和大量并发HTTP请求的现代异步应用程序。
Tornado
Tornado 是一个Python web框架和异步网络库,它允许Web应用程序非阻塞的方式处理大量开放的连接,因此它非常适合长连接,如长轮询、WebSocket和其他需要与服务器保持长时间连接的应用场景。
Tornado 的 HTTP 客户端组件是这个库的一部分,它支持异步网络访问。Tornado 的异步 HTTP 客户端可以用来执行 HTTP 请求,同时在 I/O 操作时不会阻塞应用程序的执行,从而可以在高并发环境下提高性能。
主要特性
- 异步支持:Tornado 的 HTTP 客户端完全支持异步操作。
- 简洁的用户接口:提供了易于理解和使用的 API。
- 强大的Web功能:除了作为HTTP客户端,Tornado 还提供了完整的Web服务器功能。
- WebSocket支持:Tornado 同时支持WebSocket协议,适用于需要双向通信的Web应用。
使用示例
下面是使用 Tornado HTTP 客户端的一个基本示例:
import tornado.ioloop import tornado.httpclient def handle_request(response): if response.error: print("Error:", response.error) else: print(response.body) # 创建HTTP客户端实例 http_client = tornado.httpclient.AsyncHTTPClient() # 发出异步HTTP请求 http_client.fetch("http://www.google.com", handle_request) # 开始I/O循环 tornado.ioloop.IOLoop.current().start()
在这个例子中,AsyncHTTPClient 实例用于发起异步的 HTTP 请求。fetch 方法接受一个 URL 和一个回调函数 handle_request,当请求完成时,这个回调函数会被调用。然后,我们启动 Tornado 的 I/O 循环来监听事件,并在适当的时候调用回调函数。
Tornado 的 HTTP 客户端通常用于需要处理大量并发连接的场景,它提供了一种机制来有效地执行这些操作而不会导致服务器的性能下降。它非常适合用于构建高性能的微服务、聊天服务器或类似实时数据推送的后端服务。
HTTPX
HTTPX 是一个 Python 的第三方库,用于发送 HTTP/1.1 和 HTTP/2 请求。它提供了同步和异步请求的支持,并与 Python 的异步I/O框架 asyncio 兼容。
HTTPX 的优点
- 同步与异步支持:HTTPX提供了同时支持同步和异步请求的接口,可以根据需要灵活选择。
- HTTP/2 支持:支持最新的 HTTP/2 协议,这有助于提高效率、减少延迟和改善网络性能。
- 类型提示:HTTPX完全支持类型提示,这有助于在使用如 mypy 这样的工具进行静态类型检查时减少错误。
- 高性能:可以利用异步请求来实现高性能的并发 HTTP 请求处理。
- 友好的 API:API 设计友好,易于上手,且与热门的requests 库相似,使得迁移或上手更加容易。
- 全面的功能:支持 multipart 文件上传、JSON 请求/响应、自定义认证和会话等。
- 安全性:集成了 SSL/TLS 加密,提供了证书验证、HTTP代理和客户端证书支持。
- 社区支持:虽然比requests 晚出现,但获得了较好的社区支持和维护。
HTTPX 的缺点
- 较新的库:与requests 这样的成熟项目相比,HTTPX 较新,可能在一些边缘情况下会遇到问题。
- 异步学习曲线:对于不熟悉异步编程的开发者来说,使用HTTPX 的异步功能可能需要更陡峭的学习曲线。
- 性能局限:虽然支持 HTTP/2,但在某些情况下(如大量并发连接)可能不如专门的异步客户端库,例如aiohttp 。
- API 变动:作为一个相对新的库,HTTPX的 API 还在发展中,有时可能会发生变动,这可能影响到依赖它的项目。
- 依赖性:HTTPX作为第三方库,需要单独安装,并且可能会随着项目的发展和维护而变得有些重。
HTTPX 是一个强大的 HTTP 客户端库,它结合了 requests 的易用性和异步编程的性能优势。如果你的项目需要异步处理或者想要利用 HTTP/2 的特性,HTTPX 是一个非常好的选择。不过,如果你的项目不需要异步处理,或者你更习惯于 requests 那样的同步客户端,那么 requests 仍然是一个非常可靠的选择。
HTTPX使用示例
同步请求
import httpx # 发送GET请求 response = httpx.get('https://www.example.com') print(response) # 发送POST请求 response = httpx.post('https://www.example.com/post', data={'key': 'value'}) print(response) # 使用客户端 with httpx.Client() as client: response = client.get('https://www.example.com') print(response)
异步请求
要使用 httpx 执行异步请求,需要配合 async/await 语法以及 httpx.AsyncClient 类。
import httpx import asyncio async def main(): # 使用异步客户端发送GET请求 async with httpx.AsyncClient() as client: response = await client.get('https://www.example.com') print(response) # 运行异步事件循环 asyncio.run(main())
httpx 的API设计十分简洁,易于理解和使用,它是在需要异步处理或者使用HTTP/2协议时的理想选择。尽管 httpx 相对比较新,但它已经在社区中快速获得了广泛的支持和好评。
Uplink
Uplink 是一个用于构建和调用HTTP API的Python库。它旨在提供一种声明式的方法来定义客户端接口,从而使得与RESTful API的交互变得简单和直观。Uplink 使用注解来转换Python函数调用为HTTP请求。
主要特性
- 声明式客户端接口:类似于Java的Retrofit库,你可以通过定义一个接口来描述HTTP请求的细节。
- 支持类型注解:Uplink支持Python的类型注解,使得代码更易于阅读和维护。
- 可插拔:你可以使用不同的客户端来发送请求,如requests、aiohttp 等。
- 可扩展:Uplink提供了中间件机制,允许你定制请求/响应的处理过程。
Uplink 的优点
- 声明式 API 客户端:Uplink允许以声明式的方式建立 API 客户端,使代码更加清晰和易于维护。
- 易于理解的抽象:通过将 API 端点映射到 Python 方法,Uplink提供了一个直观的使用模式。
- 灵活的数据转换:内置的转换器支持多种格式的请求和响应数据,如 JSON、表单数据等,并且可以扩展自定义转换器。
- 类型注解:支持 Python 的类型注解,这有助于 IDE 提供自动完成功能和改善代码的可读性。
- 注解驱动:使用装饰器来指定请求的类型、路径、查询参数和请求体,简化了请求的构建过程。
- 可扩展性:可以通过自定义转换器、中间件等来扩展Uplink。
Uplink 的缺点
- 社区规模和支持:与requests 和其他成熟的 HTTP 客户端库相比,Uplink 的社区相对较小,可能找不到同样多的资源和社区支持。
- 学习曲线:虽然Uplink 的使用模式直观,但如果开发者之前没有使用过类似的库,可能需要一些时间来适应声明式的编程风格。
- 性能考量:由于Uplink 提供了较高层次的抽象,可能会有一些额外的性能开销,尽管对大多数应用来说这不是问题。
- 异步支持:Uplink的异步支持不如一些专门构建在 asyncio 之上的库,如 httpx 或 aiohttp。
较少的内置功能:相较于 requests 和 HTTPX 这样的库,Uplink 可能没有那么多的内置功能和选项,可能需要开发者自行扩展。
Uplink 适用于需要快速构建和消费 RESTful APIs 的场景,尤其是当 API 结构清晰且稳定时。它的声明式和注解驱动的特性使得代码更加易读和易于维护。如果你更倾向于函数式的编程风格,或者需要处理大量异步请求,可能需要考虑使用像 requests、HTTPX 或 aiohttp 这样的库。
使用示例
以下是一个简单的示例,展示了如何使用 Uplink 来定义一个与GitHub API进行交互的客户端接口:
from uplink import Consumer, get, Path, Query # 定义一个接口类继承自Consumer class GitHub(Consumer): # 使用get装饰器来说明这是一个GET请求 @get("/users/{user}/repos") def get_repos(self, user: Path, sort_by: Query("sort")): """获取用户的仓库列表。""" # 创建接口的一个实例 github = GitHub(base_url="https://api.github.com") # 调用方法发送请求 response = github.get_repos("octocat", sort_by="created") # 打印响应内容 print(response.json())
在上述代码中,@get 是一个装饰器,它将一个函数方法与HTTP GET请求相关联。Path 和 Query 是参数注解,它们分别表示URL路径参数和查询参数。
Uplink 的这种方式使得你能够通过定义简洁的Python接口与HTTP API进行交互,而不是手动构建请求和处理响应。这种高级抽象使库非常适用于快速开发和原型制作,尤其是在与RESTful API进行频繁交互的应用中。
由于 Uplink 是建立在如 requests 这样的库之上的,所以它继承了这些库的所有功能,包括自动处理请求和响应、会话管理、支持cookies和重定向等。此外,你还可以利用 Uplink 提供的中间件功能来处理认证、日志记录、请求重试等常见的HTTP相关需求。
Python HTTP请求包总结
Library | Async | Sessions | Proxy support | SSL | HTTP 2 |
aiohttp | ✔️ | ✔️ | ✔️ | ✔️ | – |
GRequests | ✔️ | ✔️ | ✔️ | ✔️ | – |
HTTPX | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
Requests | – | ✔️ | ✔️ | ✔️ | – |
Uplink | – | – | ✔️ | ✔️ | – |
urllib | – | – | ✔️ | ✔️ | – |
urllib3 | – | – | ✔️ | ✔️ | – |