发布于 2026-01-06 9 阅读
0

构建SaaS应用:进阶篇

构建SaaS应用:进阶篇

这是关于构建您自己的 SaaS 应用系列文章的第一篇。我们将一步一步地讲解构建一个真正的产品所需的步骤:收款、系统监控、用户管理等等。

那么我们要打造什么样的产品呢?

我们将构建一个功能齐全(即使功能有限)的谷歌排名跟踪器。

输入域名和一些关键词,这款应用就能追踪其在谷歌搜索上的表现。这个想法有商业意义吗?可能没有!但它很有趣,而且确实有用,是我们力所能及的,你可以根据自己的喜好进行扩展。我们会在此过程中讲解构建 SaaS 应用的所有基础知识。

您可以在 GitHub 上找到完整的代码。

目录

构建谷歌搜索爬虫

抓取谷歌搜索结果是这个应用程序的核心。虽然我们可以从几乎任何地方开始构建,但我认为从抓取器本身入手是合理的。

爬虫程序应该接收搜索查询并加载多页搜索结果。然后,爬虫程序会将这些结果返回给我们的应用程序。听起来很简单!但实际上,过程中可能会出现很多问题。为了避免收到不满客户的投诉邮件,我们需要编写大量代码来处理各种故障。

在 AWS 实例上设置 Puppeteer

我们将使用Puppeteer来进行网页抓取。Puppeteer 提供了一个 JavaScript API,用于远程控制 Chromium 浏览器会话。最棒的是,浏览器可以在没有桌面环境的情况下运行(无头模式),因此我们的代码可以在云服务器上独立执行。在本教程中,我们将从 AWS 上的 Ubuntu 18.04 实例开始,逐步完成 Puppeteer 所需的所有依赖项的安装。

tc2.medium为这个项目使用了一台 EC2 实例。它配备了 2 个虚拟 CPU 和 4GB 内存,因此性能足以运行 Puppeteer,以及我们之后要添加的其他程序。Ubuntu 18.04 是一个不错的选择。

Chromium 自带 Puppeteer,但在开始之前,我们需要安装一系列必要的系统库。幸运的是,我们只需一行命令就能完成所有这些安装。

sudo apt-get install -y ca-certificates fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils
Enter fullscreen mode Exit fullscreen mode

Chromium 依赖项安装完毕后,我们就可以开始设置 Node v14 了。最简单的方法是通过下载安装脚本,该脚本会告诉我们的包管理器如何找到 Node v14,而不是它目前指向的旧版本。

curl -sL https://deb.nodesource.com/setup_14.x -o nodesource_setup.sh
bash nodesource_setup.sh
apt-get install -y nodejs
Enter fullscreen mode Exit fullscreen mode

至此,我们已经安装了Node和Chromium。接下来,我们将创建一个package.json文件,以便使用NPM安装项目依赖项(例如Puppeteer)。

{
    "name": "agent-function",
    "version": "0.0.1",
    "dependencies": {
        "axios": "^0.19.2", // For communicating with the app server.
        "puppeteer": "10.0.0",
        "puppeteer-extra": "3.1.8",
        "puppeteer-extra-plugin-stealth": "2.7.8"
    }
}
Enter fullscreen mode Exit fullscreen mode

运行完成后npm install,您应该已经准备好所有必要的组件。让我们使用一个非常简单的 Node 脚本来验证 Puppeteer 是否已安装并正常运行。

const puppeteer = require("puppeteer-extra");

async function crawl() {
    console.log("It worked!!!");
}

puppeteer
    .launch({
        headless: true,
        executablePath:
            "./node_modules/puppeteer/.local-chromium/linux-884014/chrome-linux/chrome",
        ignoreHTTPSErrors: true,
        args: [
            "--start-fullscreen",
            "--no-sandbox",
            "--disable-setuid-sandbox"
        ]
    })
    .then(crawl)
    .catch(error => {
        console.error(error);
        process.exit();
    });
Enter fullscreen mode Exit fullscreen mode

请注意配置对象中的 headless 键。这意味着 Chromium 将以无图形用户界面 (GUI) 的方式启动,这正是我们在 EC2 服务器上运行时所需要的。如果一切顺利,It worked!!!执行此脚本后您应该会在控制台中看到输出。

发出简单的谷歌搜索请求

既然我们已经确认所有软件都已正确安装,接下来应该先进行一次简单的谷歌搜索。目前我们暂不进行任何实际的网页抓取操作。我们的目标很简单,就是在搜索栏中输入搜索词,加载谷歌搜索结果,然后截屏验证搜索功能是否正常。

这是更新后的爬虫函数,使其能够执行我刚才描述的操作。

async function crawl(browser) {
    const page = await browser.newPage();
    await page.goto("https://www.google.com/?hl=en");

    // Find an input with the name 'q' and type the search query into it, while 
    // pausing 100ms between keystrokes.
    const inputHandle = await page.waitForXPath("//input[@name = 'q']");
    await inputHandle.type("puppeteer", { delay: 100 });

    await page.keyboard.press("Enter");
    await page.waitForNavigation();

    await page.screenshot({ path: "./screenshot.png" });
    await browser.close();
}
Enter fullscreen mode Exit fullscreen mode

Puppeteer 加载 Google 搜索页面(添加hl=en以请求英文版本),输入搜索查询,然后按回车键。

waitForNavigation方法会暂停脚本,直到浏览器发出加载事件(即页面及其所有资源,例如 CSS 和图像,都已加载完毕)。这一点很重要,因为我们希望在截屏之前等待结果可见。

简单搜索结果

screenshot.png希望运行脚本后您能看到类似的结果。

使用代理网络进行爬虫请求

然而,即使你的第一次请求成功了,最终很可能还是会遇到验证码。如果你从同一个IP地址发送太多请求,这种情况几乎不可避免。

已屏蔽

解决方案是通过代理网络路由请求,以避免触发验证码拦截。爬虫程序偶尔会被拦截,但如果运气好的话,大部分请求都能成功通过。

代理服务器种类繁多,供应商也数不胜数。对于像这样的网络爬虫项目,主要有三种选择。

  • 通过 Proxyall 之类的服务购买单个 IP 地址或多个 IP 地址。这是最经济的选择。我购买了 5 个 IP 地址,每月大约 5 美元。
  • 数据中心代理提供大量的IP地址,但按带宽收费。例如,Smartproxy提供100GB的带宽,价格为100美元。然而,其中许多IP地址已被屏蔽。
  • 住宅代理也提供大量的 IP 地址,但这些地址来自住宅或移动 ISP,因此遇到验证码的频率较低。但缺点是价格较高。Smartproxy 5GB 的数据传输费用为 75 美元。

如果你的爬虫运行速度很慢,请求频率也很低,那么你或许可以不用代理。但我实际上想追踪自己网站的排名,所以使用几个专用的 IP 地址就显得很合理了。

使用 Puppeteer 通过代理网络而非默认网络发送请求非常简单。启动参数列表接受一个proxy-server值。

puppeteer
    .launch({
        headless: false,
        executablePath:
            "./node_modules/puppeteer/.local-chromium/linux-884014/chrome-linux/chrome",
        ignoreHTTPSErrors: true,
        args: [
            `--proxy-server=${proxyUrl}`, // Specifying a proxy URL.
            "--start-fullscreen",
            "--no-sandbox",
            "--disable-setuid-sandbox"
        ]
    })
Enter fullscreen mode Exit fullscreen mode

可能proxyUrl类似于这样http://gate.dc.smartproxy.com:20000。大多数代理配置都需要用户名和密码,除非您使用 IP 白名单作为身份验证方法。在发出任何请求之前,您需要使用该用户名/密码组合进行身份验证。

async function crawl(browser) {
    const page = await browser.newPage();
    await page.authenticate({ username, password });
    await page.goto("https://www.google.com/?hl=en");
}
Enter fullscreen mode Exit fullscreen mode

任何被大量使用的爬虫程序仍然会遇到被屏蔽的情况,但只要我们构建良好的错误处理机制,一个好的代理就能使这个过程可持续下去。

收集搜索结果

现在我们来看看实际的网页抓取部分。这款应用的总体目标是追踪排名,但为了简化操作,抓取器并不关心任何特定的网站或域名。它只是简单地将链接列表(按照页面上的顺序!)返回给应用服务器。

为此,我们将使用 XPath 来选择页面上的正确元素。在复杂的网页抓取场景中,CSS 选择器通常不够用。在这种情况下,Google 没有提供任何简单的 ID 或类名供我们识别正确的链接。我们需要结合类名和标签结构来提取正确的链接集。

这段代码将提取链接,并按预定次数点击“下一步”按钮,或者直到没有“下一步”按钮为止。

let rankData = [];
while (pages) {
    // Find the search result links -- they are children of div elements
    // that have a class of 'g', while the links themselves must also
    // have an H3 tag as a child.
    const results = await page.$x("//div[@class = 'g']//a[h3]");

    // Extract the links from the tags using a call to 'evaluate', which
    // will execute the function in the context of the browser (i.e. not
    // within the current Node process).
    const links = await page.evaluate(
        (...results) => results.map(link => link.href),
        ...results
    );

    const [next] = await page.$x(
        "//div[@role = 'navigation']//a[descendant::span[contains(text(), 'Next')]]"
    );

    rankData = rankData.concat(links);

    if (!next) {
        break;
    }

    await next.click();
    await page.waitForNavigation();

    pages--;
}
Enter fullscreen mode Exit fullscreen mode

现在我们有了搜索结果,如何将它们从 Node 进程中取出并保存到某个地方呢?

有很多方法可以实现这一点,但我选择让应用程序提供一个可供爬虫使用的 API,以便爬虫可以通过 POST 请求发送结果。Axios 库让这一切变得非常简单,所以我在这里分享一下它的具体实现方式。

    axios
        .post(`http://172.17.0.1/api/keywords/${keywordID}/callback/`, {
            secret_key: secretKey,
            proxy_id: proxyID,
            results: rankData,
            blocked: blocked,
            error: ""
        })
        .then(() => {
            console.log("Successfully returned ranking data.");
        });
Enter fullscreen mode Exit fullscreen mode

暂时不用担心这里的 ` blockedor`error变量。我们稍后会讨论错误处理。这里最重要的是 ` rankDataresearch` 变量,它指向包含所有搜索结果链接的列表。

爬虫程序错误处理

处理意外情况在任何类型的编程中都至关重要,对于网页爬虫来说尤其如此。很多环节都可能出错:遇到验证码、代理连接失败、XPath 过期、网络不稳定等等。

部分错误处理将在稍后实现,因为在爬虫代码本身中我们能做的有限。应用程序需要足够智能,能够判断何时应该重试,或者是否应该停用某个代理 IP 地址,因为它被屏蔽的频率过高。

如果你还记得之前的内容,抓取器会返回一个blocked值。让我们来看看如何判断抓取器是否被阻塞。

    let blocked = false;

    try {
        const [captcha] = await page.$x("//form[@id = 'captcha-form']");
        if (captcha) {
            console.log("Agent encountered a CAPTCHA");
            blocked = true;
        }
    } catch (e) {}
Enter fullscreen mode Exit fullscreen mode

这段代码的作用是查找是否存在具有指定 ID 的表单,如果存在,captcha-form则将值设置blocked为 true。稍后我们将看到,如果代理 IP 被报告为被阻止的次数过多,应用程序将不再使用该 IP 地址。

接下来会发生什么?

希望您喜欢本SaaS应用系列的第一部分!接下来,我将讲解如何使用Docker配置NGINX、Flask和Postgres,以便我们的爬虫程序可以调用API。您可以在GitHub上找到项目的完整代码。

文章来源:https://dev.to/zchtodd_79/building-a-saas-app-beyond-the-basics-1jh