使用 Ray 并行化加速你的网络爬虫#

try-anyscale-quickstart

在本例中,我们将快速演示如何使用 Ray Tasks 构建一个简单的 Python 网络爬虫,并通过少量代码修改实现并行化。

要在你的机器上本地运行此示例,请首先使用以下命令安装 raybeautifulsoup

pip install "beautifulsoup4==4.11.1" "ray>=2.2.0"

首先,我们将定义一个名为 find_links 的函数,该函数接受一个起始页面 (start_url) 作为抓取目标,并以 Ray 文档作为这样的一个起始点。我们的爬虫简单地从该起始 URL 中提取所有包含指定的 base_url 的可用链接(例如,在我们的例子中,我们只希望跟踪 https://docs.rayai.org.cn 上的链接,而非任何外部链接)。随后,find_links 函数将使用我们通过这种方式找到的所有链接进行递归调用,直到达到某个深度。

为了从网站的 HTML 元素中提取链接,我们定义了一个名为 extract_links 的辅助函数,它负责正确处理相对 URL,并限制从网站返回的链接数量 (max_results),以便更容易控制爬虫的运行时间。

这是完整的实现

import requests
from bs4 import BeautifulSoup

def extract_links(elements, base_url, max_results=100):
    links = []
    for e in elements:
        url = e["href"]
        if "https://" not in url:
            url = base_url + url
        if base_url in url:
            links.append(url)
    return set(links[:max_results])


def find_links(start_url, base_url, depth=2):
    if depth == 0:
        return set()

    page = requests.get(start_url)
    soup = BeautifulSoup(page.content, "html.parser")
    elements = soup.find_all("a", href=True)
    links = extract_links(elements, base_url)

    for url in links:
        new_links = find_links(url, base_url, depth-1)
        links = links.union(new_links)
    return links

让我们定义一个起始 URL 和基础 URL,并将 Ray 文档爬取到深度为 2

base = "https://docs.rayai.org.cn/en/latest/"
docs = base + "index.html"
%time len(find_links(docs, base))
CPU times: user 19.3 s, sys: 340 ms, total: 19.7 s
Wall time: 25.8 s
591

如你所见,像这样递归爬取文档根目录总共返回 591 个页面,耗时大约 25 秒。

页面爬取可以通过多种方式并行化。可能最简单的方式是简单地从多个起始 URL 开始,并为每个 URL 并行调用 find_links。我们可以通过 Ray Tasks 以直接的方式实现这一点。我们只需使用 ray.remote 装饰器将 find_links 函数包装成一个名为 find_links_task 的任务,如下所示:

import ray

@ray.remote
def find_links_task(start_url, base_url, depth=2):
    return find_links(start_url, base_url, depth)

要使用此任务启动并行调用,你只需使用 find_links_tasks.remote(...) 代替直接调用底层 Python 函数即可。

以下是如何并行运行六个爬虫:前三个(重复地)再次爬取 docs.ray.io,另外三个则分别爬取 Ray RLlib、Tune 和 Serve 库的主要入口点。

links = [find_links_task.remote(f"{base}{lib}/index.html", base)
         for lib in ["", "", "", "rllib", "tune", "serve"]]
%time for res in ray.get(links): print(len(res))
591
591
105
204
105
CPU times: user 65.5 ms, sys: 47.8 ms, total: 113 ms
Wall time: 27.2 s

此次并行运行爬取了大约四倍数量的页面,耗时与最初的串行运行大致相同。请注意,在计时运行中使用了 ray.get 来从 Ray 获取结果(通过 get 解析远程调用承诺)。

当然,还有更智能的方法来创建爬虫并高效地并行化它,本示例为你提供了一个起点。