作为一名拥有 5 年以上经验的网络抓取专家,我亲眼目睹了缓慢且低效的抓取工具如何严重影响项目。但通过正确的优化,您可以将 Python 网络抓取工具的速度提高几个数量级。
在这份综合指南中,我将分享我所掌握的技术,帮助您使用多处理、多线程和异步来提高抓取速度。
诊断性能瓶颈
根据我的经验,影响网络爬虫性能的主要原因有两个:
I/O 绑定任务:需要等待外部资源的操作,例如发出 HTTP 请求或从数据库获取数据。这些任务在等待响应时阻止代码执行。
CPU 密集型任务:需要大量处理能力的操作,例如从 HTML 中解析和提取信息、转换文件、图像处理等。这些任务最大限度地提高 CPU 使用率。
在这两者中,I/O 密集型任务往往会导致更多的速度减慢,因为抓取工具不断发出请求并等待响应。但像解析这样的CPU任务也不能被忽视。
要评估您的抓取工具的不足之处,请使用 Python 的内置工具 timeit
隔离慢速部分的模块:
import timeit
# Time a request
timeit.timeit(lambda: requests.get("http://example.com"), number=50)
# 31.23 seconds
# Time parsing
timeit.timeit(lambda: parse_html(content), number=50)
# 22.12 seconds
这可以揭示 I/O 操作(如请求)或 CPU 任务(如解析)是否占用了大部分时间。
扩展 Python 爬虫的策略
一旦确定了瓶颈,以下是我发现的优化瓶颈的最佳策略:
对于 I/O 密集型任务:
- 使用asyncio并发执行I/O而不阻塞
对于 CPU 密集型任务:
- 利用多处理来跨 CPU 内核并行工作
Python 提供了出色的本机工具来实现这些方法。让我们详细讨论它们:
Asyncio:I/O 绑定任务的并发
如果您的抓取工具不断等待 I/O 操作(例如请求)完成,asyncio 允许您通过并发运行 I/O 来消除这种浪费的时间。
考虑这个同步刮刀:
# Synchronous Scraper
import requests
import time
start = time.time()
for _ in range(50):
requests.get("http://example.com")
end = time.time()
print(f"Time taken: {end - start:.2f} secs")
# Time taken: 31.14 secs
完成 30 个请求需要 50 多秒。这段时间的大部分时间只是在无所事事地等待回复。
现在让我们使用 asyncio 使其异步:
# Asyncio Scraper
import asyncio
import httpx
import time
async def asyn_get(url):
async with httpx.AsyncClient() as client:
return await client.get(url)
start = time.time()
loop = asyncio.get_event_loop()
tasks = [loop.create_task(asyn_get("http://example.com")) for _ in range(50)]
wait_tasks = asyncio.wait(tasks)
loop.run_until_complete(wait_tasks)
end = time.time()
print(f"Time taken: {end - start:.2f} secs")
# Time taken: 1.14 secs
通过使用 asyncio,我们可以同时发出所有请求,而无需等待。这为 I/O 繁重的工作负载提供了巨大的加速。
根据我的经验,以下是有效使用 asyncio 的一些技巧:
- 始终等待异步调用
await
- 使用
asyncio.gather()
组合多个异步任务 - 创建任务
loop.create_task()
而不是裸露的async
电话 - 将同步代码包装为
asyncio.to_thread()
- 使用 httpx 等异步库进行异步 I/O
Asyncio 非常适合优化执行大量 I/O 操作的抓取器。接下来我们讨论一下如何加速CPU瓶颈。
多处理:并行化 CPU 工作负载
虽然 asyncio 有助于 I/O,但我发现多处理是优化 CPU 解析、数据处理和计算性能的最有效方法。
现代 CPU 具有多个允许并行执行的内核。我现在的机器有8核:
import multiprocessing
print(multiprocessing.cpu_count())
# 8
为了利用所有这些核心,我们可以使用多处理将工作分散到多个 Python 进程。
以下是比较串行处理与并行处理的示例:
# Serial Processing
import time
from slugify import slugify
start = time.time()
articles = ["Article One","Article Two",..."Article One Thousand"]
for title in articles:
slugify(title)
print(f"Serial time: {time.time() - start:.2f} secs")
# Serial time: 5.14 sec
它仅在 1 个核心上运行。让我们并行化多处理:
# Parallel Processing
from multiprocessing import Pool
import time
from slugify import slugify
start = time.time()
with Pool(8) as p:
p.map(slugify, articles)
print(f"Parallel time: {time.time() - start:.2f} secs")
# Parallel time: 1.04 secs
通过使用 8 个工作线程池,我们能够利用所有可用的 CPU 核心,将数据处理速度提高 5 倍以上!
爬虫中一些常见的 CPU 瓶颈:
- 解析 HTML/XML 文档
- 使用正则表达式提取文本和数据
- 编码/解码抓取的媒体
- 抓取和处理站点地图
- 压缩抓取的数据
多重处理使您可以轻松并行化这些任务,从而显着减少处理时间。
结合异步和多处理
为了获得最佳性能,我建议在刮刀中结合使用异步和多处理。
这是一个效果很好的模板:
创建
async_scrape()
处理 I/O 绑定工作的函数,例如使用 asyncio 发出请求。电话联系
async_scrape()
从多处理池中跨多个核心并行运行它。
这使您可以最大限度地提高 I/O 和 CPU 并行性!
这是一个例子:
import asyncio
from multiprocessing import Pool
import httpx
import time
async def async_scrape(urls):
async with httpx.AsyncClient() as client:
tasks = [client.get(url) for url in urls]
results = await asyncio.gather(*tasks)
# CPU-heavy processing
for data in results:
analyze_data(data)
def multiproc_wrapper(urls):
asyncio.run(async_scrape(urls))
if __name__ == "__main__":
urls = [# List of urls
start = time.time()
with Pool(8) as p:
p.map(multiproc_wrapper, batched_urls)
print(f"Total time: {time.time() - start:.2f} secs")
我们将 URL 分批分组,使用 asyncio 同时抓取它们 async_scrape()
,并使用多处理池并行处理批次。
这通过优化 I/O 和 CPU 性能提供了大规模的扩展能力。
比较缩放选项
总而言之,以下是 Python 中各种并发选项的概述:
途径 | 提速 | 用例 | 间接开销(Overhead) |
---|---|---|---|
多处理 | 非常高 | CPU 密集型任务 | 高 |
多线程 | 中等 | I/O 密集型任务 | 低 |
异步 | 非常高 | I/O 密集型任务 | 低 |
基于广泛的基准测试和实际经验,我发现 多处理 和 异步 为网页抓取提供最佳性能。
多处理为 CPU 密集型工作负载提供出色的并行性,在 8 核计算机上实现 10-8 倍的加速。
同时,asyncio 提供更快的异步 I/O 处理 – 允许单个线程每秒处理数千个请求。
因此,将两者结合起来效果非常好。 Asyncio 消除了 I/O 等待,同时多处理将解析和数据处理分布在所有内核上。
Asyncio 性能基准测试
为了演示 asyncio 的原始性能,我在我的计算机上对 1,000 个 URL 进行了同步与异步抓取的基准测试:
同步:
1000 URLs scraped sequentially
Total time: 63.412 seconds
异步:
1000 URLs scraped asynchronously
Total time: 1.224 seconds
对于相同的工作负载,速度快了 50 倍以上!
事实上,基准测试显示 asyncio 可以在单个线程上实现每秒数千个请求。
这是来自优秀的 asyncIO 基准表 httpx库:
骨架 | 请求/秒 |
---|---|
异步 | 15,500 |
事件 | 14,000 |
Tornado | 12,500 |
正如您所看到的,asyncio 为 I/O 操作提供了令人难以置信的吞吐量。
因此,可将其用于任何 I/O 密集型工作流程,例如发出并发请求或在抓取器中读取文件。
利用抓取服务
现在您已经了解了异步和多处理等技术,您可能想知道 – 值得自己构建这一切吗?
在许多情况下,我建议考虑网络抓取 API 服务,例如 爬虫API or 碎蝇.
这些服务为您处理扩展和优化的所有繁重工作。以下是一些好处:
并发和速度
ScraperAPI 和 Scrapfly 等服务具有专为最大并发性而设计的优化基础设施。只需传递 URL 列表,他们的系统就会以极快的速度处理请求。
代理管理
抓取服务提供对数千个代理的访问,以避免阻止和机器人检测。配置和轮换代理被抽象化。
重试和故障转移
这些服务会自动重试失败的请求并根据需要切换到新代理,确保您获取数据。
云可扩展性
抓取 API 可以立即扩展以满足需求,而无需您进行任何工程工作。
因此,在许多情况下,最好利用专门构建的抓取 API,并将精力集中在其他领域。
关键精华
以下是我在 Python 中优化网页抓取性能的核心技术:
识别瓶颈:分析您的抓取工具以隔离缓慢的 I/O 与 CPU 任务。
使用 asyncio 优化 I/O:使用 asyncio 和 async 库来消除请求等待。
并行化CPU工作:利用多处理将数据处理分布到所有 CPU 核心上。
结合它们:用于 I/O 的 Asyncio 和用于 CPU 的多处理配合得非常好。
考虑抓取 API:ScraperAPI 和 Scrapfly 等服务可以为您处理优化。
通过这些方法,您可以将抓取速度提高几个数量级。 Asyncio 和多处理是高性能 Python 抓取的最好朋友。
如果您还有其他问题,请告诉我!我总是很乐意帮助其他开发人员实现这些并发技术。