跳到内容

网页抓取速度:进程、线程和异步

作为一名拥有 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 文档
  • 使用正则表达式提取文本和数据
  • 编码/解码抓取的媒体
  • 抓取和处理站点地图
  • 压缩抓取的数据

多重处理使您可以轻松并行化这些任务,从而显着减少处理时间。

结合异步和多处理

为了获得最佳性能,我建议在刮刀中结合使用异步和多处理。

这是一个效果很好的模板:

  1. 创建 async_scrape() 处理 I/O 绑定工作的函数,例如使用 asyncio 发出请求。

  2. 电话联系 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
Tornado12,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 抓取的最好朋友。

如果您还有其他问题,请告诉我!我总是很乐意帮助其他开发人员实现这些并发技术。

标签:

加入谈话

您的电邮地址不会被公开。 必填带 *