网页抓取中的分页指南

本文探讨了常见的分页技术,并提供了 Python 代码示例,帮助您更有效地抓取数据。
6 min read
网页抓取中的分页指南

当你在抓取网页时,你经常会遇到分页的情况,即内容分布在多个页面上。处理这种分页可能具有挑战性,因为不同的网站使用不同的分页技术。

在本文中,我将解释常见的分页技术,并通过一个实用的代码示例展示如何处理它们。

什么是分页?

像电子商务平台、招聘网站和社交媒体等网站使用分页来管理大量数据。如果将所有内容显示在一个页面上,会显著增加加载时间并消耗过多内存。分页将内容分布在多个页面上,并提供诸如“下一页”、页码或滚动时自动加载的导航选项。这使得浏览更快、更有组织。

分页类型

分页的复杂性各不相同,从简单的数字分页到更高级的技术,如无限滚动或动态内容加载。根据我的经验,我遇到了三种主要的分页类型,我认为它们是网站上最常用的:

  • 数字分页:用户通过数字链接导航到不同的页面。
  • 点击加载分页:用户点击一个按钮(例如“加载更多”)来加载更多内容。
  • 无限滚动:用户向下滚动页面时内容自动加载。

让我们更详细地探讨一下每种分页方式!

数字分页

这是最常见的分页技术,通常称为“下一页和上一页分页”、“箭头分页”或“基于 URL 的分页”。尽管名称不同,但核心思想相同——页面通过数字链接进行连接。你可以通过更改 URL 中的页码来导航。要知道何时停止分页,你可以检查“下一页”按钮是否被禁用或是否没有新数据可用。

通常看起来是这样的:

网页抓取中的分页截图 - 示例数字分页

`让我们举个例子!我们将浏览网站 Scrapethesite 上的所有页面。该网站的分页栏共有 24 页。

网页抓取中的分页截图 - Scrapethesite 分页

你会注意到,当你点击“>>”按钮时,URL 会发生如下变化:

现在,看看这个“下一页”按钮的 HTML。它是一个带有 href 属性的锚标签(<a>),链接到下一页。aria-label 属性显示“下一页”按钮仍然是活动的。
当没有更多页面时,aria-label 将缺失,显示分页的结束。

网页抓取中的分页截图 - Scrapethesite 分页

让我们从编写一个基本的网页抓取器开始,导航这些页面。首先,通过安装所需的包来设置你的环境。有关使用 Python 进行网页抓取的详细指南,你可以查看深入的博客文章 这里

pip install requests beautifulsoup4 lxml

以下是分页浏览每个页面的代码:

import requests
from bs4 import BeautifulSoup

base_url = "https://www.scrapethissite.com/pages/forms/?page_num="

# Start with page 1
page_num = 1

while True:
    url = f"{base_url}{page_num}"
    response = requests.get(url)
    soup = BeautifulSoup(response.content, "lxml")

    print(f"Currently on page: {page_num}")

    # Check if 'Next' button exists
    next_button = soup.find("a", {"aria-label": "Next"})

    if next_button:
        # Move to the next page
        page_num += 1
    else:
        # No more pages, exit loop
        print("Reached the last page.")
        break

这段代码通过检查是否存在带有 aria-label="Next" 的“下一页”按钮来导航页面。如果按钮存在,它会增加 page_num 并使用更新后的 URL 发送新的请求。循环将继续,直到不再找到“下一页”按钮,表明已经到达最后一页。

运行代码,你会看到我们已成功浏览了所有页面。

网页抓取中的分页截图 - 数字分页输出

一些网站的“下一页”按钮不会更改 URL,但仍在同一页面上加载新内容。在这种情况下,传统的网页抓取方法可能效果不佳。像 Selenium 或 Playwright 这样的工具更适合,因为它们可以与页面交互并模拟点击按钮等操作,以检索动态加载的内容。有关使用 Selenium 进行此类任务的更多信息,你可以阅读详细指南 这里

当你尝试抓取 NGINX 博客页面时,你会遇到类似的情况。

网页抓取中的分页截图 - NGINX 分页

让我们使用 Playwright 来处理动态加载的内容。如果你是 Playwright 新手,可以查看这个有用的 入门指南

现在,在编写代码之前,运行以下命令在你的机器上设置 Playwright:

pip install playwright
playwright install

以下是代码:

import asyncio
from playwright.async_api import async_playwright

# Define an asynchronous function
async def scrape_nginx_blog():
    async with async_playwright() as p:
        # Launch a Chromium browser instance in headless mode
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()

        # Navigate to the NGINX blog page
        await page.goto("https://www.f5.com/company/blog/nginx")

        page_num = 1
        while True:
            print(f"Currently on page {page_num}")

            # Locate the 'Next' button using a button locator with value "next"
            next_button = page.locator('button[value="next"]')

            # Check if the 'Next' button is enabled
            if await next_button.is_enabled():
                await next_button.click()  # Click the 'Next' button to go to the next page
                await page.wait_for_timeout(
                    2000
                )  # Wait for 2 seconds to allow new content to load
                page_num += 1
            else:
                print("No more pages. Scraping finished.")
                break  # Exit the loop if no more pages are available

        await browser.close()  # Close the browser


# Run the asynchronous scraping function
asyncio.run(scrape_nginx_blog())

该代码使用异步 Playwright 浏览所有页面。它进入一个循环,检查“下一页”按钮是否存在。如果按钮被启用,它会点击以转到下一页并等待内容加载。这个过程会重复,直到没有更多页面可用。最后,抓取完成后,浏览器将被关闭。

运行代码,你会看到我们已成功浏览了所有页面。

网页抓取中的分页截图 - 数字分页 NGINX 输出

点击加载分页

在许多网站上,你可能见过“加载更多”、“显示更多”或“查看更多”等按钮。这些都是点击加载分页的例子,通常用于现代网站。这些按钮通过 JavaScript 动态加载内容。这里的关键挑战是模拟用户交互——自动化点击按钮以加载更多内容的过程。

Bright Data 博客 部分为例。当你访问并向下滚动时,你会注意到一个“查看更多”按钮,点击它会加载博客文章。

网页抓取中的分页截图 - Bright Data 加载更多

你可以使用 Selenium 或 Playwright 等工具,通过反复点击“加载更多”按钮,直到没有更多内容可用,来自动化这个过程。让我们看看如何使用 Playwright 轻松处理这个问题。

import asyncio
from playwright.async_api import async_playwright


async def scrape_brightdata_blog():
    async with async_playwright() as p:

        # Launch a headless browser
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()

        # Navigate to the Bright Data blog
        await page.goto("https://brightdata.com/blog")

        page_num = 1

        while True:
            print(f"Currently on page {page_num}")

            # Locate the "View More" button
            view_more_button = page.locator("button.load_more_btn")

            # Check if the button is visible and enabled
            if (
                await view_more_button.count() > 0
                and await view_more_button.is_visible()
            ):
                await view_more_button.click()
                await page.wait_for_timeout(2000)
                page_num += 1
            else:
                print("No more pages to load. Scraping finished.")
                break

        # Close the browser
        await browser.close()


# Run the scraping function
asyncio.run(scrape_brightdata_blog())

该代码使用 CSS 选择器 button.load_more_btn 定位“查看更多”按钮。然后,它通过使用 count() > 0is_visible() 检查按钮是否存在且可见。如果按钮可见,它使用 click() 方法与其交互,并等待 2 秒以允许新内容加载。这个过程在循环中重复,直到按钮不再可见。

运行代码,你会看到我们已成功浏览了所有页面。

网页抓取中的分页截图 - 加载更多分页输出

我们已成功从 Bright Data 博客部分抓取了所有 52 页。这表明该网站共有 52 页,我们是在抓取过程后才发现的。然而,在抓取之前知道总页数是可能的。

要做到这一点,打开开发者工具,导航到“网络”选项卡,并通过选择“Fetch/XHR”来筛选请求。然后,再次点击“查看更多”按钮,你会注意到触发了一个 AJAX 请求。

网页抓取中的分页截图 - Bright Data 博客

点击这个请求并导航到“预览”部分,你会看到最大页数是 52。然后,前往“负载”部分,你会发现每页有 6 篇博客文章,我们当前在第 3 页。

网页抓取中的分页截图 - Bright Data 博客部分

这太棒了!

无限滚动分页

许多网站现在使用无限滚动,而不是“上一页/下一页”按钮,这通过消除点击多个页面的需求来改善用户体验。这种技术会在用户向下滚动时自动加载新内容。然而,它为网页抓取器带来了独特的挑战,因为它需要监控 DOM 变化和处理 AJAX 请求。

让我们举一个现实生活中的例子。当你访问 Nike 网站时,你会注意到鞋子在你向下滚动时会自动加载。每次滚动时,一个加载图标会短暂出现,转眼间,更多鞋子就会显示出来,如下图所示:

网页抓取中的分页截图 - Nike 无限滚动

当你点击请求 (d9a5bc) 时,你可以在“响应”选项卡中找到当前页面的所有数据。

现在,要处理分页,你需要不断向下滚动页面,直到到达页面底部。随着滚动,浏览器会发出许多请求,但这些 Fetch/XHR 请求中只有一些会包含你需要的实际数据。

以下是处理分页并提取鞋子标题的代码:

import asyncio
from urllib.parse import parse_qs, urlparse
from playwright.async_api import async_playwright


async def scroll_to_bottom(page) -> None:
    """Scroll to the bottom of the page until no more content is loaded."""
    last_height = await page.evaluate("document.body.scrollHeight")
    scroll_count = 0
    while True:
        # Scroll down
        await page.evaluate("window.scrollTo(0, document.body.scrollHeight);")
        await asyncio.sleep(2)  # Wait for new content to load

        scroll_count += 1
        print(f"Scroll iteration: {scroll_count}")

        # Check if scroll height has changed
        new_height = await page.evaluate("document.body.scrollHeight")
        if new_height == last_height:
            print("Reached the bottom of the page.")
            break  # Exit if no new content is loaded
        last_height = new_height


async def extract_product_data(response, extracted_products) -> None:
    """Extract product data from the response."""
    parsed_url = urlparse(response.url)
    query_params = parse_qs(parsed_url.query)

    if "queryType" in query_params and query_params["queryType"][0] == "PRODUCTS":
        data = await response.json()
        for grouping in data.get("productGroupings", []):
            for product in grouping.get("products", []):
                title = product.get("copy", {}).get("title")
                extracted_products.append({"title": title})


async def scrape_shoes(target_url: str) -> None:
    async with async_playwright() as playwright:
        browser = await playwright.chromium.launch(headless=True)
        page = await browser.new_page()
        extracted_products = []

        # Set up listener for product data responses
        page.on(
            "response",
            lambda response: extract_product_data(
                response, extracted_products),
        )

        # Navigate to the page and scroll to the bottom
        print("Navigating to the page...")
        await page.goto(target_url, wait_until="domcontentloaded")
        await asyncio.sleep(2)
        await scroll_to_bottom(page)

        # Save product titles to a text file
        with open("product_titles.txt", "w") as title_file:
            for product in extracted_products:
                title_file.write(product["title"] + "\n")
        print(f"Scraping completed!")
        await browser.close()


if __name__ == "__main__":
    asyncio.run(
        scrape_shoes(
            "https://www.nike.com/in/w/mens-running-shoes-37v7jznik1zy7ok")
    )

在代码中,scroll_to_bottom 函数不断滚动到页面底部以加载更多内容。它首先记录当前的滚动高度,然后反复向下滚动。每次滚动后,它会检查新的滚动高度是否与最后记录的高度不同。如果高度保持不变,它会得出没有更多内容被加载的结论并退出循环。这种方法确保在抓取过程继续之前,所有可用的产品都已完全加载。

当你运行代码时,会发生以下情况:

网页抓取中的分页截图 - 无限滚动输出

代码成功执行后,将创建一个包含所有 Nike 鞋子标题的新文本文件。

网页抓取中的分页截图 - 文本文件

分页中的挑战

在处理分页内容时,被 封锁 的风险增加,一些网站可能会在仅一页后就封锁你。例如,如果你试图 抓取 Glassdoor,你可能会遇到各种 网页抓取挑战,其中之一就是我所经历的 Cloudflare CAPTCHA 挑战。

Glassdoor Cloudflare CAPTCHA

让我们向 Glassdoor 页面发起请求,看看会发生什么。

import requests

url = "https://www.glassdoor.com/"
response = requests.get(url)
print(f"Status code: {response.status_code}")

结果是一个 403 状态码

这表明 Glassdoor 已检测到你的请求来自机器人或抓取器,导致 CAPTCHA 挑战。如果你继续发送多个请求,你的 IP 可能会立即被封锁。

要绕过这些封锁并有效地提取所需数据,你可以使用 Python Requests 中的代理避免 IP 封锁,或通过 轮换用户代理 模仿真实浏览器。然而,重要的是要注意,这些方法都不能保证避免高级机器人检测。

那么,最终的解决方案是什么?让我们接下来深入探讨一下!

集成 Bright Data 解决方案

Bright Data 是绕过复杂反机器人措施的优秀解决方案。它只需几行代码即可无缝集成到你的项目中,并提供一系列针对任何高级反机器人机制的解决方案。

其中一个解决方案是 网页抓取器 API,它通过自动处理 IP 轮换和 CAPTCHA 解决 简化了从任何网站提取数据。这使你能够专注于数据分析,而不是数据检索的复杂细节。

例如,在我们的案例中,当尝试绕过 Glassdoor 上的 CAPTCHA 时遇到了挑战。为了解决这个问题,你可以使用 Bright Data 的 Glassdoor 抓取器 API,它专门设计用于绕过这些障碍并无缝地从网站提取数据。

要开始使用 Glassdoor 抓取器 API,请按照以下步骤操作:

首先,创建一个账户。访问 Bright Data 网站,点击 开始免费试用,并按照注册说明操作。登录后,你将被重定向到你的 仪表板,在那里你将获得一些免费积分。

现在,进入 Web Scraper API 部分,在 B2B 数据类别下选择 Glassdoor。你会找到各种数据收集选项,例如通过 URL 收集公司信息或通过 URL 收集职位列表。

Bright Data 仪表板上的网页抓取器 API

在“Glassdoor 公司概述信息”下,获取你的 API 令牌并复制你的数据集 ID(例如,gd_l7j0bx501ockwldaqf)。

获取 API 令牌和数据集 ID

现在,以下是一个简单的代码片段,展示如何通过提供 URL、API 令牌和数据集 ID 来提取公司数据。

import requests
import json

def trigger_dataset(api_token, dataset_id, company_url):
    """
    Triggers a dataset using the BrightData API.

    Args:
    api_token (str): The API token for authentication.
    dataset_id (str): The dataset ID to trigger.
    company_url (str): The URL of the company page to analyze.

    Returns:
    dict: The JSON response from the API.
    """
    headers = {
        "Authorization": f"Bearer {api_token}",
        "Content-Type": "application/json",
    }
    payload = json.dumps([{"url": company_url}])
    response = requests.post(
        "https://api.brightdata.com/datasets/v3/trigger",
        headers=headers,
        params={"dataset_id": dataset_id},
        data=payload,
    )
    return response.json()

api_token = "API_Token"
dataset_id = "DATASET_ID"
company_url = "https://www.glassdoor.com/"
response_data = trigger_dataset(api_token, dataset_id, company_url)
print(response_data)

运行代码后,你将收到如下所示的快照 ID:

快照 ID

使用快照 ID 来检索公司的实际数据。在终端中运行以下命令。对于 Windows,使用:

curl.exe -H "Authorization: Bearer API_TOKEN" \
"https://api.brightdata.com/datasets/v3/snapshot/s_m0v14wn11w6tcxfih8?format=json"

对于 Linux

curl -H "Authorization: Bearer API_TOKEN" \
"https://api.brightdata.com/datasets/v3/snapshot/s_m0v14wn11w6tcxfih8?format=json"

运行命令后,你将获得所需的数据。

最终所需数据

就这么简单!

同样,你可以通过修改代码从 Glassdoor 提取各种类型的数据。我已经解释了一种方法,但还有五种其他方法。因此,我建议探索这些选项来抓取你想要的数据。每种方法都针对特定的数据需求,帮助你获取所需的精确数据。

结论

本文讨论了现代网站常用的各种分页方法,如数字分页、“加载更多”按钮和无限滚动。还提供了有效实施这些分页技术的代码示例。然而,尽管处理分页是网页抓取的一部分,但克服反机器人检测是一个重大挑战。

逃避高级反机器人检测可能相当复杂,且成功率各不相同。Bright Data 的工具提供了一种简化且具有成本效益的解决方案,包括 Web UnlockerScraping BrowserWeb Scraper APIs,满足你所有的网页抓取需求。只需几行代码,你就可以实现更高的成功率,而无需麻烦地管理复杂的反机器人措施。

完全不想参与抓取过程?看看我们的 数据集市场

今天就注册,享受免费试用。