Next.js使用抓取浏览器进行实时数据抓取的预订应用程序

介绍

在不断发展的 Web 开发领域,有效收集、处理和显示外部来源数据的能力变得越来越有价值。无论是市场研究、竞争分析还是客户洞察,网络抓取在释放互联网数据的巨大潜力方面都发挥着至关重要的作用。

这篇博文介绍了构建强大的 Next.js 应用程序的综合指南,该应用程序旨在从领先的旅行搜索引擎之一 Kayak 抓取航班数据。通过利用 Next.js 的强大功能以及 BullMQ、Redis 和 Puppeteer 等现代技术。

图片[1]-Next.js使用抓取浏览器进行实时数据抓取的预订应用程序-哈德森博客

技术栈

特征

  • 🚀 带有 Tailwind CSS 的 Next.js 14 应用程序目录 - 体验由最新 Next.js 14 提供支持的时尚现代的 UI,并使用 Tailwind CSS 进行设计,以实现完美的外观和感觉。
  • 🔗 API 路由和服务器操作 - 深入研究与 Next.js 14 的 API 路由和服务器操作的无缝后端集成,确保高效的数据处理和服务器端逻辑执行。
  • 🕷 使用 Puppeteer Redis 和 BullMQ 进行抓取 - 利用 Puppeteer 的强大功能进行高级 Web 抓取,并使用 Redis 和 BullMQ 管理队列和作业以实现强大的后端操作。
  • 🔑 用于身份验证和授权的 JWT 令牌 - 使用 JWT 令牌保护您的应用程序,为整个平台提供可靠的身份验证和授权方法。
  • 💳 支付网关 Stripe - 集成 Stripe 进行无缝支付处理,为预订旅行、航班和酒店提供安全、轻松的交易。
  • ✈️ 使用 Stripe 支付网关预订旅行、航班和酒店 - 使用我们的 Stripe 支持的支付系统,让您的旅行预订体验变得轻松。
  • 📊 从多个网站抓取实时数据 - 从多个来源抓取实时数据,保持领先,让您的应用程序更新最新信息。
  • 💾 使用 Prisma 将抓取的数据存储在 PostgreSQL 中 - 利用 PostgreSQL 和 Prisma 高效存储和管理抓取的数据,确保可靠性和速度。
  • 🔄 用于状态管理的 Zustand - 通过 Zustand 简化状态逻辑并增强性能,在您的应用程序中享受流畅且可管理的状态管理。
  • 😈 该应用程序的最佳功能 - 使用 Bright Data 的抓取浏览器抓取不可抓取的数据。

Bright Data的抓取浏览器为我们提供了自动验证码解决功能,可以帮助我们抓取不可抓取的数据

第 1 步:设置 Next.js 应用程序

  1. 创建 Next.js 应用程序:首先创建一个新的 Next.js 应用程序(如果您还没有)。您可以通过在终端中运行以下命令来完成此操作:
npx create-next-app@latest booking-app
  1. 导航到您的应用程序目录:更改为您新创建的应用程序目录:
cd booking-app

第2步:安装所需的软件包

您需要安装多个软件包,包括 Redis、BullMQ 和 Puppeteer Core。运行以下命令来安装它们:

npm install ioredis bullmq puppeteer-core
  • ioredis是 Node.js 的强大 Redis 客户端,支持与 Redis 进行通信。
  • bullmq以 Redis 作为后端管理作业和消息队列。
  • puppeteer-core允许您控制外部浏览器以进行抓取。

第3步:设置Redis连接

在合适的目录(例如redis.js)中创建一个文件(例如lib/)来配置 Redis 连接:

// lib/redis.js
import Redis from 'ioredis';

// Use REDIS_URL from environment or fallback to localhost
const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
const connection = new Redis(REDIS_URL);

export { connection };

第4步:配置BullMQ队列

queue.js通过在 Redis 配置所在的同一目录中创建另一个文件(例如 )来设置 BullMQ 队列:

// lib/queue.js
import { Queue } from 'bullmq';
import { connection } from './redis';

export const importQueue = new Queue('importQueue', {
  connection,
  defaultJobOptions: {
    attempts: 2,
    backoff: {
      type: 'exponential',
      delay: 5000,
    },
  },
});

第 5 步:Next.js 仪器设置

Next.js 允许检测,可以在 Next.js 配置中启用。您还需要创建一个用于作业处理的工作文件。

1.在 Next.js 中启用 Instrumentation:将以下内容添加到您的文件中next.config.js以启用 Instrumentation:

// next.config.js
module.exports = {
  experimental: {
    instrumentationHook: true,
  },
};

2.创建用于作业处理的 Worker:在您的应用程序中,创建一个文件 ( instrumentation.js) 来处理作业处理。该工作人员将使用 Puppeteer 来执行抓取任务:

// instrumentation.js
export const register = async () => {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { Worker } = await import('bullmq');
    const puppeteer = await import('puppeteer-core');
    const { connection } = await import('./lib/redis');
    const { importQueue } = await import('./lib/queue');

    new Worker('importQueue', async (job) => {
      // Job processing logic with Puppeteer goes here
    }, {
      connection,
      concurrency: 10,
      removeOnComplete: { count: 1000 },
      removeOnFail: { count: 5000 },
    });
  }
};

第 6 步:设置 Bright Data 的抓取浏览器

在设置 Bright 数据抓取浏览器之前,我们先来谈谈什么是抓取浏览器。

Bright Data 的抓取浏览器是什么?

Bright Data 的抓取浏览器是一款用于自动网页抓取的尖端工具,旨在与 Puppeteer、Playwright 和 Selenium 无缝集成。它提供了一套网站解锁功能,包括代理轮换、验证码解决等,以提高抓取效率。它非常适合需要交互的复杂网络抓取,通过在 Bright Data 的基础设施上托管无限的浏览器会话来实现可扩展性。欲了解更多详情,请访问光明数据

第 1 步:导航至 Bright Data 网站

首先访问Brightdata.com。这是您访问 Bright Data 提供的丰富网络抓取资源和工具的门户。

图片[2]-Next.js使用抓取浏览器进行实时数据抓取的预订应用程序-哈德森博客

第 2 步:创建帐户

访问 Bright Data 网站后,注册并创建一个新帐户。系统将提示您输入基本信息以启动并运行您的帐户。

图片[3]-Next.js使用抓取浏览器进行实时数据抓取的预订应用程序-哈德森博客

第 3 步:选择您的产品

在产品选择页面上,查找代理和抓取基础设施产品。该产品专为满足您的网络抓取需求而设计,提供强大的数据提取工具和功能。

图片[4]-Next.js使用抓取浏览器进行实时数据抓取的预订应用程序-哈德森博客

第 4 步:添加新代理

在“代理和抓取基础设施”页面中,您会找到一个“添加新按钮”。单击此按钮开始将新的抓取浏览器添加到您的工具包的过程。

图片[5]-Next.js使用抓取浏览器进行实时数据抓取的预订应用程序-哈德森博客

第五步:选择抓取浏览器

将出现一个下拉列表,您应该从中选择抓取浏览器选项。这告诉 Bright Data 您打算设置一个新的抓取浏览器环境。

图片[6]-Next.js使用抓取浏览器进行实时数据抓取的预订应用程序-哈德森博客

第 6 步:为您的抓取浏览器命名

为您的新抓取浏览器指定一个唯一的名称。这有助于稍后识别和管理它,特别是如果您计划对不同的抓取项目使用多个浏览器。

图片[7]-Next.js使用抓取浏览器进行实时数据抓取的预订应用程序-哈德森博客

第7步:添加浏览器

命名您的浏览器后,单击“添加”按钮。此操作完成了新的抓取浏览器的创建。

图片[8]-Next.js使用抓取浏览器进行实时数据抓取的预订应用程序-哈德森博客

第 8 步:查看您的抓取浏览器详细信息

添加抓取浏览器后,您将被定向到一个页面,您可以在其中查看新创建的抓取浏览器的所有详细信息。这些信息对于集成和使用至关重要。

图片[9]-Next.js使用抓取浏览器进行实时数据抓取的预订应用程序-哈德森博客

第 9 步:访问代码和集成示例

查找“查看代码和集成示例”按钮。单击此按钮将为您提供如何跨多种编程语言和库集成和使用抓取浏览器的全面视图。对于希望自定义抓取设置的开发人员来说,此资源非常宝贵。

图片[9]-Next.js使用抓取浏览器进行实时数据抓取的预订应用程序-哈德森博客

第 10 步:集成您的抓取浏览器

最后,复制 SRS_WS_ENDPOINT 变量。这是一条关键信息,您需要将其集成到源代码中,以便您的应用程序能够与您刚刚设置的抓取浏览器进行通信。

图片[11]-Next.js使用抓取浏览器进行实时数据抓取的预订应用程序-哈德森博客

通过遵循这些详细步骤,您已在 Bright Data 平台中成功创建了一个抓取浏览器,准备好处理您的网络抓取任务。请记住,Bright Data 提供广泛的文档和支持,帮助您最大限度地提高抓取项目的效率和效果。无论您是在收集市场情报、进行研究还是监控竞争格局,新设置的抓取浏览器都是数据收集库中的强大工具。

第 7 步:使用 Puppeteer 实现抓取逻辑

从我们上次设置用于抓取航班数据的 Next.js 应用程序的地方开始,下一个关键步骤是实现实际的抓取逻辑。此过程涉及利用 Puppeteer 连接到浏览器实例、导航到目标 URL(在我们的示例中为 Kayak)并抓取必要的飞行数据。提供的代码片段概述了实现此目标的复杂方法,与我们之前建立的 BullMQ 工作设置无缝集成。让我们分解这个抓取逻辑的组件,并了解它如何适合我们的应用程序。

建立与浏览器的连接

我们抓取过程的第一步是通过 Puppeteer 建立与浏览器的连接。这是通过利用该方法来实现的puppeteer.connect,该方法使用 WebSocket 端点 ( SBR_WS_ENDPOINT) 连接到现有的浏览器实例。该环境变量应设置为您正在使用的抓取浏览器服务的 WebSocket URL,例如 Bright Data:

const browser = await puppeteer.connect({
  browserWSEndpoint: SBR_WS_ENDPOINT,
});

打开新页面并导航到目标 URL

连接后,我们在浏览器中创建一个新页面并导航到作业数据中指定的目标 URL。此 URL 是我们打算从中抓取航班数据的特定 Kayak 搜索结果页面:

const page = await browser.newPage();
await page.goto(job.data.url);

抓取航班数据

我们逻辑的核心在于从页面中抓取航班数据。我们通过使用page.evaluatePuppeteer 方法来实现这一点,它允许我们在浏览器上下文中运行脚本。在此脚本中,我们等待必要的元素加载,然后继续收集航班信息:

  • 航班选择器:我们以 class 为目标元素.nrc6-wrapper,其中包含航班详细信息。
  • 数据提取:对于每个航班元素,我们提取详细信息,例如航空公司徽标、出发和到达时间、航班持续时间、航空公司名称和价格。出发和到达时间经过清理,以删除最后不必要的数值,确保我们准确捕获时间。
  • 价格处理:价格在删除所有非数字字符后提取为整数,确保其可用于数值运算或比较。

提取的数据被构造成飞行对象数组,每个对象都包含上述详细信息:

const scrappedFlights = await page.evaluate(async () => {
  // Data extraction logic
  const flights = [];
  // Process each flight element
  // ...
  return flights;
});

错误处理和清理

我们的抓取逻辑被包装在一个 try-catch 块中,以在抓取过程中优雅地处理任何潜在的错误。无论结果如何,我们都会确保浏览器在finally块中正确关闭,从而保持资源效率并防止潜在的内存泄漏:

try {
  // Scraping logic
} catch (error) {
  console.log({ error });
} finally {
  await browser.close();
  console.log("Browser closed successfully.");
}

整个代码

const SBR_WS_ENDPOINT = process.env.SBR_WS_ENDPOINT;

export const register = async () => {

  if (process.env.NEXT_RUNTIME === "nodejs") {


    const { Worker } = await import("bullmq");
    const puppeteer = await import("puppeteer");
    const { connection } = await import("./lib/redis");
    const { importQueue } = await import("./lib/queue");

    new Worker(
      "importQueue",
      async (job) => {
        const browser = await puppeteer.connect({
          browserWSEndpoint: SBR_WS_ENDPOINT,
        });

        try {
          const page = await browser.newPage();

          console.log("in flight scraping");
          console.log("Connected! Navigating to " + job.data.url);
          await page.goto(job.data.url);
          console.log("Navigated! Scraping page content...");
          const scrappedFlights = await page.evaluate(async () => {
            await new Promise((resolve) => setTimeout(resolve, 5000));

            const flights = [];

            const flightSelectors = document.querySelectorAll(".nrc6-wrapper");

            flightSelectors.forEach((flightElement) => {
              const airlineLogo = flightElement.querySelector("img")?.src || "";
              const [rawDepartureTime, rawArrivalTime] = (
                flightElement.querySelector(".vmXl")?.innerText || ""
              ).split(" – ");

              // Function to extract time and remove numeric values at the end
              const extractTime = (rawTime: string): string => {
                const timeWithoutNumbers = rawTime
                  .replace(/[0-9+\s]+$/, "")
                  .trim();
                return timeWithoutNumbers;
              };

              const departureTime = extractTime(rawDepartureTime);
              const arrivalTime = extractTime(rawArrivalTime);
              const flightDuration = (
                flightElement.querySelector(".xdW8")?.children[0]?.innerText ||
                ""
              ).trim();

              const airlineName = (
                flightElement.querySelector(".VY2U")?.children[1]?.innerText ||
                ""
              ).trim();

              // Extract price
              const price = parseInt(
                (
                  flightElement.querySelector(".f8F1-price-text")?.innerText ||
                  ""
                )
                  .replace(/[^\d]/g, "")
                  .trim(),
                10
              );

              flights.push({
                airlineLogo,
                departureTime,
                arrivalTime,
                flightDuration,
                airlineName,
                price,
              });
            });

            return flights;
          });
        } catch (error) {
          console.log({ error });
        } finally {
          await browser.close();
          console.log("Browser closed successfully.");
        }
      },
      {
        connection,
        concurrency: 10,
        removeOnComplete: { count: 1000 },
        removeOnFail: { count: 5000 },
      }
    );
  }
};

第8步:航班搜索功能

基于我们的航班数据抓取功能,让我们将全面的航班搜索功能集成到我们的 Next.js 应用程序中。此功能将为用户提供一个动态界面,通过指定出发地、目的地和日期来搜索航班。利用强大的 Next.js 框架以及现代 UI 库和状态管理,我们创建了引人入胜且响应迅速的航班搜索体验。

航班搜索功能的关键组成部分

  1. 动态城市选择:该功能包括源和目的地输入的自动完成功能,由预定义的城市机场代码列表提供支持。当用户输入时,应用程序会过滤并显示匹配的城市,通过更轻松地查找和选择机场来增强用户体验。
  2. 日期选择:用户可以通过日期输入选择预期的航班日期,为计划旅行提供灵活性。
  3. 抓取状态监控:启动抓取作业后,应用程序通过定期 API 调用来监控作业的状态。这种异步检查允许应用程序使用抓取过程的状态更新 UI,确保用户了解进度和结果。

航班搜索组件的完整代码

"use client";
import { useAppStore } from "@/store";
import { USER_API_ROUTES } from "@/utils/api-routes";
import { cityAirportCode } from "@/utils/city-airport-codes";
import { Button, Input, Listbox, ListboxItem } from "@nextui-org/react";
import axios from "axios";
import Image from "next/image";
import { useRouter } from "next/navigation";
import React, { useEffect, useRef, useState } from "react";
import { FaCalendarAlt, FaSearch } from "react-icons/fa";

const SearchFlights = () => {
  const router = useRouter();
  const { setScraping, setScrapingType, setScrappedFlights } = useAppStore();
  const [loadingJobId, setLoadingJobId] = useState<number | undefined>(undefined);
  const [source, setSource] = useState("");
  const [sourceOptions, setSourceOptions] = useState<
    { city: string; code: string; }[]
  >([]);
  const [destination, setDestination] = useState("");
  const [destinationOptions, setDestinationOptions] = useState<
    { city: string; code: string; }[]
  >([]);
  const [flightDate, setFlightDate] = useState(() => {
    const today = new Date();
    return today.toISOString().split("T")[0];
  });

  const handleSourceChange = (query: string) => {
    const matchingCities = Object.entries(cityAirportCode)
      .filter(([, city]) => city.toLowerCase().includes(query.toLowerCase()))
      .map(([code, city]) => ({ code, city }))
      .splice(0, 5);

    setSourceOptions(matchingCities);
  };

  const destinationChange = (query: string) => {
    const matchingCities = Object.entries(cityAirportCode)
      .filter(([, city]) => city.toLowerCase().includes(query.toLowerCase()))
      .map(([code, city]) => ({ code, city }))
      .splice(0, 5);

    setDestinationOptions(matchingCities);
  };

  const startScraping = async () => {
    if (source && destination && flightDate) {
      const data = await axios.get(`${USER_API_ROUTES.FLIGHT_SCRAPE}?source=${source}&destination=${destination}&date=${flightDate}`);
      if (data.data.id) {
        setLoadingJobId(data.data.id);
        setScraping(true);
        setScrapingType("flight");
      }
    }
  };

  useEffect(() => {
    if (loadingJobId) {
      const checkIfJobCompleted = async () => {
        try {
          const response = await axios.get(`${USER_API_ROUTES.FLIGHT_SCRAPE_STATUS}?jobId=${loadingJobId}`);
          if (response.data.status) {
            set

ScrappedFlights(response.data.flights);
            clearInterval(jobIntervalRef.current);
            setScraping(false);
            setScrapingType(undefined);
            router.push(`/flights?data=${flightDate}`);
          }
        } catch (error) {
          console.log(error);
        }
      };
      jobIntervalRef.current = setInterval(checkIfJobCompleted, 3000);
    }

    return () => clearInterval(jobIntervalRef.current);
  }, [loadingJobId]);

  return (
    <div className="h-[90vh] flex items-center justify-center">
      <div className="absolute left-0 top-0 h-[100vh] w-[100vw] max-w-[100vw] overflow-hidden overflow-x-hidden">
        <Image src="/flight-search.png" fill alt="Search" />
      </div>
      <div className="absolute h-[50vh] w-[60vw] flex flex-col gap-5">
        {/* UI and functionality for flight search */}
      </div>
    </div>
  );
};

export default SearchFlights;

第9步:航班搜索页面UI

图片[12]-Next.js使用抓取浏览器进行实时数据抓取的预订应用程序-哈德森博客

显示航班搜索结果

成功抓取飞行数据后,下一个关键步骤是以用户友好的方式将这些结果呈现给用户。Next.js 应用程序中的 Flights 组件就是为此目的而设计的。

"use client";

import { useAppStore } from "@/store";
import { USER_API_ROUTES } from "@/utils/api-routes";
import { Button } from "@nextui-org/react";
import axios from "axios";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
import React from "react";
import { FaChevronLeft } from "react-icons/fa";
import { MdOutlineFlight } from "react-icons/md";

const Flights = () => {
  const router = useRouter();
  const searchParams = useSearchParams();
  const date = searchParams.get("date");
  const { scrappedFlights, userInfo } = useAppStore();
  const getRandomNumber = () => Math.floor(Math.random() * 41);

  const bookFLight = async (flightId: number) => {};

  return (
    <div className="m-10 px-[20vw] min-h-[80vh]">
      <Button
        className="my-5"
        variant="shadow"
        color="primary"
        size="lg"
        onClick={() => router.push("/search-flights")}
      >
        <FaChevronLeft /> Go Back
      </Button>
      <div className="flex-col flex gap-5">
        {scrappedFlights.length === 0 && (
          <div className="flex items-center justify-center py-5 px-10 mt-10 rounded-lg text-red-500 bg-red-100 font-medium">
            No Flights found.
          </div>
        )}
        {scrappedFlights.map((flight: any) => {
          const seatsLeft = getRandomNumber();
          return (
            <div
              key={flight.id}
              className="grid grid-cols-12 border bg-gray-200 rounded-xl font-medium drop-shadow-md"
            >
              <div className="col-span-9 bg-white rounded-l-xl p-10 flex flex-col gap-5">
                <div className="grid grid-cols-4 gap-4">
                  <div className="flex flex-col gap-3 font-medium">
                    <div>
                      <div className="relative w-20 h-16">
                        <Image src={flight.logo} alt="airline name" fill />
                      </div>
                    </div>
                    <div>{flight.name}</div>
                  </div>
                  <div className="col-span-3 flex justify-between">
                    <div className="flex flex-col gap-2">
                      <div className="text-blue-600">From</div>
                      <div>
                        <span className="text-3xl">
                          <strong>{flight.departureTime}</strong>
                        </span>
                      </div>
                      <div>{flight.from}</div>
                    </div>
                    <div className="flex flex-col items-center justify-center gap-2">
                      <div className="bg-violet-100 w-max p-3 text-4xl text-blue-600 rounded-full">
                        <MdOutlineFlight />
                      </div>
                      <div>
                        <span className="text-lg">
                          <strong>Non-stop</strong>
                        </span>
                      </div>
                      <div>{flight.duration}</div>
                    </div>

                    <div className="flex flex-col gap-2">
                      <div className="text-blue-600">To</div>
                      <div>
                        <span className="text-3xl">
                          <strong>{flight.arrivalTime}</strong>
                        </span>
                      </div>
                      <div>{flight.to}</div>
                    </div>
                  </div>
                </div>
                <div className="flex justify-center gap-10 bg-violet-100 p-3 rounded-lg">
                  <div className="flex">
                    <span>Airplane  </span>
                    <span className="text-blue-600 font-semibold">
                      Boeing 787
                    </span>
                  </div>
                  <div className="flex">
                    <span>Travel Class:  </span>
                    <span className="text-blue-600 font-semibold">Economy</span>
                  </div>
                </div>
                <div className="flex justify-between font-medium">
                  <div>
                    Refundable <span className="text-blue-600"> $5 ecash</span>
                  </div>
                  <div
                    className={`${
                      seatsLeft > 20 ? "text-green-500" : "text-red-500"
                    }`}
                  >
                    Only {seatsLeft} Seats Left
                  </div>
                  <div className="cursor-pointer">Flight Details</div>
                </div>
              </div>
              <div className="col-span-3 bg-violet-100 rounded-r-xl h-full flex flex-col items-center justify-center gap-5">
                <div>
                  <div>
                    <span className="line-through font-light">
                      ${flight.price + 140}
                    </span>
                  </div>
                  <div className="flex items-center gap-2">
                    <span className="text-5xl font-bold">${flight.price}</span>
                    <span className="text-blue-600">20% OFF</span>
                  </div>
                </div>
                <Button
                  variant="ghost"
                  radius="full"
                  size="lg"
                  color="primary"
                  onClick={() => {
                    if (userInfo) bookFLight(flight.id);
                  }}
                >
                  {userInfo ? "Book Now" : "Login to Book"}
                </Button>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default Flights;

航班搜索结果

图片[13]-Next.js使用抓取浏览器进行实时数据抓取的预订应用程序-哈德森博客

教程来源:Dev/kishansheth

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=5vbhp91f157x
THE END
喜欢就支持一下吧
点赞11 分享
评论 共1条
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片