如何在2023年用uppeteer刮擦网络
#javascript #教程 #puppeteer #webscraping

完整的网络刮擦和爬行教程。了解如何使用Puppeteer启动浏览器,单击按钮并等待操作以及如何从网站中提取数据。从建造基本刮板到大规模爬行。

Puppeteer是用于JavaScript的浏览器自动化库,它使用DevTools协议来编程控制Chromium或Chrome浏览器。在Github上有超过80k的星星,它是无头浏览器自动化的事实上的标准。 Puppeteer用打字稿编写,这很容易从IDE中完成的代码完成。

其他具有类似功能的图书馆是Selenium,它在JavaScript世界以外非常受欢迎,而Playwright是Puppeteer的年轻继父。

相关的

使用Puppeteer,您可以使用(无头)铬或铬打开网站,填充表单,单击按钮,提取数据并通常在使用计算机时执行任何人类的操作。这使Puppeteer成为网络刮擦的真正功能强大的工具,也可以自动化网络上的复杂工作流程。

您不需要熟悉木偶或网络刮擦才能享受本教程,但是对HTML,CSS和JavaScript的了解。

在本教程中,您将学习如何:

  1. 用木偶启动浏览器
  2. 单击按钮并等待操作
  3. 从网站提取数据

该项目

为了展示Puppeteer的基础知识,我们将创建一个简单的刮板,以提取有关GitHub Topics的数据。您将能够选择一个主题,刮板将返回有关此主题标记的存储库的信息。

The page for JavaScript GitHub Topics

我们将使用Puppeteer启动浏览器,打开GitHub主题页面,单击加载更多按钮以显示更多存储库,然后提取以下信息:

  • 所有者
  • 名称
  • url
  • 星数
  • 描述
  • 存储库主题列表

安装

要使用puppeteer,您需要node.js和一个软件包管理器。我们将使用NPM,该NPM已预装为Node.J。您可以通过运行:
来确认它们在计算机上的存在

node -v && npm -v

要充分利用本教程,您需要Node.js版本16或更高版本。如果您缺少node.js或npm或具有不支持的版本,请访问installation tutorial开始。

相关的

现在,我们知道了我们的环境,让我们创建一个新项目并安装木偶。

mkdir puppeteer-scraper && cd puppeteer-scraper
npm init -y
npm i puppeteer

您第一次安装Puppeteer时,它将下载浏览器二进制文件,因此安装可能需要更长的时间。

通过将"type": "module"添加到package.json文件中来完成安装。这将实现现代JavaScript语法。如果您不这样做,则在运行代码时Node.js会抛出SyntaxError: Cannot use import statement outside a moduleLearn more about ECMAScript modules in Node.js

{
    "type": "module",
    "name": "puppeteer-scraper",
    // ... other fields
}

建造一个木偶刮刀

即使您没有以前的刮擦经验,用木偶制作刮板也很容易。如果您了解JavaScript和CSS,那将是一小菜。

在您的项目文件夹中,创建一个名为scraper.js的文件,然后在您喜欢的代码编辑器中打开它。首先,我们将通过运行一个简单的脚本来确认Puppeteer已正确安装并工作。

import puppeteer from 'puppeteer';

// Open the installed Chromium. We use headless: false
// to be able to inspect the browser window.
const browser = await puppeteer.launch({
    headless: false
});

// Open a new page / tab in the browser.
const page = await browser.newPage();

// Tell the tab to navigate to the JavaScript topic page.
await page.goto('https://github.com/topics/javascript');

// Pause for 10 seconds, to see what's going on.
await new Promise(r => setTimeout(r, 10000));

// Turn off the browser to clean up after ourselves.
await browser.close();

现在使用您的代码编辑器或通过在项目文件夹中执行以下命令来运行它。

node scraper.js

如果您看到一个铬窗口打开,并且github主题页面成功加载了,恭喜,您只是用Puppeteer机器人的网络浏览器。

JavaScript GitHub Topics

加载更多存储库

首次打开主题页面时,显示的存储库的数量限制为20。您可以通过单击页面底部的 load 按钮来加载更多。

Load more button at the bottom of the GitHub page

我们需要告诉Puppeteer三件事以加载更多的存储库:

  1. 等待 加载更多按钮以在页面上渲染。
  2. 单击 加载更多按钮。
  3. 等待让存储库加载。

使用Puppeteer,单击按钮非常容易。通过将text/的前缀到您要查找的字符串,Puppeteer会找到包含此字符串的元素并单击它。

await page.click('text/Load more');

但是,重要的是要提到,如果尚未在页面上加载该按钮,Puppeteer将不会自动等待它出现,这可能会产生竞赛条件并打破刮板。让我们扩展代码等待按钮渲染。

const buttonSelector = 'text/Load more';
await page.waitForSelector(buttonSelector);
await page.click(buttonSelector);

单击后,您需要等待存储库加载。如果没有,刮板可以在页面上出现新的存储库之前完成,您会错过这些数据。 koude5允许您在浏览器内执行功能,然后等到函数返回true

await page.waitForFunction(() => {
    const repoCards = document.querySelectorAll('article.border');
    // GitHub displays 20 repositories per page.
    // We wait until there's more than 20.
    return repoCards.length > 20;
});

要找到article.border选择器,我们使用了浏览器DevTools,您可以在大多数浏览器中打开它们,通过右键单击页面上的任何位置并选择 Inspect 。它的意思是:使用border类选择<article>标签。

Chrome DevTools

如果您不熟悉DevTools和CSS选择器,请访问Apify Academy中的Web scraping for beginners course。它是免费的和开源的。

让我们将其插入我们的代码并进行测试运行。我已经删除了早期的评论,以使您更容易找到新的更改。

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
    headless: false
});

const page = await browser.newPage();

await page.goto('https://github.com/topics/javascript');

// Wait for the Load more button to render 
// on the page and click it.
const buttonSelector = 'text/Load more';
await page.waitForSelector(buttonSelector);
await page.click(buttonSelector);

// Tell Puppeteer to keep watching for more than
// 20 repository cards to appear in the page.
await page.waitForFunction(() => {
    const repoCards = document.querySelectorAll('article.border');
    return repoCards.length > 20;
});

await new Promise(r => setTimeout(r, 10000));
await browser.close();

如果您观看运行,则会看到浏览器首先向下滚动并单击加载更多按钮,该按钮将文本更改为加载更多。一两秒钟后,您会看到下一批20个存储库。好!

用木偶提取数据

现在,我们知道如何加载更多存储库,我们将提取所需的数据。为此,我们将使用koude10函数。它告诉浏览器找到某些元素,然后使用这些元素执行JavaScript函数。这是提取代码:

const repos = await page.$$eval('article.border', (repoCards) => {
    return repoCards.map(card => {
        const [user, repo] = card.querySelectorAll('h3 a');
        const stars = card.querySelector('#repo-stars-counter-star')
            .getAttribute('title');
        const description = card.querySelector('div.px-3 > p');
        const topics = card.querySelectorAll('a.topic-tag');

        const toText = (element) => element && element.innerText.trim();
        const parseNumber = (text) => Number(text.replace(/,/g, ''));

        return {
            user: toText(user),
            repo: toText(repo),
            url: repo.href,
            stars: parseNumber(stars),
            description: toText(description),
            topics: Array.from(topics).map((t) => toText(t)),
        };
    });
});

它的工作原理:page.$$eval()找到了我们的存储库并在浏览器中执行提供的功能。我们获得了repoCards,这是所有回购元素中的Array。函数的返回值成为page.$$eval()调用的返回值。多亏了Puppeteer,您可以将数据从浏览器中抽出,然后将它们保存到Node.js中的变量。魔术â

如果您努力理解提取代码本身,请务必查看this guide on working with CSS selectorsthis tutorial on using those selectors to find HTML elements

以及其中包括提取的代码。运行它时,您会看到40个存储库,并将其信息打印到控制台。

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
    headless: false
});

const page = await browser.newPage();

await page.goto('https://github.com/topics/javascript');
const buttonSelector = 'text/Load more';
await page.waitForSelector(buttonSelector);
await page.click(buttonSelector);
await page.waitForFunction(() => {
    const repoCards = document.querySelectorAll('article.border');
    return repoCards.length > 20;
});

// Extract data from the page. Selecting all 'article' elements
// will return all the repository cards we're looking for.
const repos = await page.$$eval('article.border', (repoCards) => {
    return repoCards.map(card => {
        const [user, repo] = card.querySelectorAll('h3 a');
        const stars = card.querySelector('#repo-stars-counter-star')
            .getAttribute('title');
        const description = card.querySelector('div.px-3 > p');
        const topics = card.querySelectorAll('a.topic-tag');

        const toText = (element) => element && element.innerText.trim();
        const parseNumber = (text) => Number(text.replace(/,/g, ''));

        return {
            user: toText(user),
            repo: toText(repo),
            url: repo.href,
            stars: parseNumber(stars),
            description: toText(description),
            topics: Array.from(topics).map((t) => toText(t)),
        };
    });
});


// Print the results 🚀
console.log(`We extracted ${repos.length} repositories.`);
console.dir(repos);

await new Promise(r => setTimeout(r, 10000));
await browser.close();

概括

到目前为止,page.waitForFunction()在浏览器和page.$$eval()中等待操作从浏览器页面提取数据。但是在刮擦一页后,没有真正的刮擦项目完成。刮擦主要用于构建用于数据分析的大型数据集。

相关的

让我们通过将本教程扩展到不仅要刮擦前40个存储库,还要刮擦其中的任何数量来对此进行模拟。为此,我们将不得不重复单击 加载按钮,而不仅仅是一次。

此外,我们将添加每个收集的存储库主分支中的提交数量的刮擦。该号码在主题页面上不可用,因此我们必须单独访问每个存储库页面并从那里获取。我们的刮板将学会爬行。

与木偶爬行

虽然Puppeteer对于控制浏览器绝对是惊人的,但它主要不是网络刮擦工具。有可能与木偶爬行,但请相信我,这很痛苦。您必须打开浏览器,关闭浏览器,打开的选项卡,关闭选项卡,跟踪您已经爬行的内容,失败和需要重试的内容,处理所有错误,因此它们不会撞击您的爬行者,管理内存和CPU,以便太多的打开标签不会使您的机器压倒。这是可行的,但我宁愿专注于爬行,而不是浏览器管理。

舒适的刮擦和用伪装者爬行,最好与另一个图书馆一起完成。该图书馆称为Crawlee,它也是免费的开放源代码,就像木偶一样。 Crawlee包裹木偶和授予访问所有木偶的功能,但也提供useful crawling and scraping tools,例如错误处理,队列管理,存储,代理或指纹或指纹。

Crawlee安装

您可以通过在您之前创建的Puppeteer-Scraper目录中使用NPM将crawlee添加到您的项目中:

npm install crawlee

crawlee将认识到木偶已经已经安装,并且能够立即使用它。要快速测试此问题,让我们创建一个新的文件crawlee.js并在文件中使用以下代码:

// Crawlee works with other libraries like Playwright
// or Cheerio as well. Now we want to work with Puppeteer.
import { PuppeteerCrawler } from 'crawlee';

// PuppeteerCrawler manages browsers and browser tabs.
// You don't have to manually open and close them.
// It also handles navigation (goto), errors and retries.
const crawler = new PuppeteerCrawler({
    // Request handler gives you access to the currently
    // open page. Similar to the pure Puppeteer examples
    // above, we can use it to control the browser's page.
    requestHandler: async ({ page }) => {
        // Get the title of the page just to test things.
        const title = await page.title()
        console.log(title);
    }
})

// Here we start the crawler on the selected URLs.
await crawler.run(['https://github.com/topics/javascript']);

上面的代码使用crawlee的PuppeteerCrawler类来管理Puppeteer并使用它爬网。这次,它只打开一页并获得其标题。对于测试,这已经足够了。

node crawlee.js

执行上述命令后,您会看到Crawlee打印的几行,其中包括以下行。这意味着一切都按预期工作。

javascript · GitHub Topics · GitHub

使无头浏览器的窗户可见

您可能注意到没有打开浏览器窗口。那是因为Crawlee(与Puppeteer相同)默认情况下无头。如果您想查看浏览器中发生的事情,则必须将headless切换到false

// Don't forget to import the sleep helper function.
import { PuppeteerCrawler, sleep } from 'crawlee';

const crawler = new PuppeteerCrawler({
    // Launch all browsers in headless mode.
    headless: false,
    requestHandler: async ({ page }) => {
        const title = await page.title()
        console.log(title);
        // We can use the sleep helper to stop
        // the page from closing too quickly.
        await sleep(10000);
    }
})

await crawler.run(['https://github.com/topics/javascript']);

与木偶滚动

现在,我们知道Crawlee和Puppeteer按预期一起工作,我们可以利用Crawlee的一些工具来帮助我们刮除前100名JavaScript存储库的提交数量。首先,让我们看一下单击加载更多按钮以加载100个存储库。

crawlee的功能正是为此目的。它称为koude23,可用于自动处理具有无限滚动的网站 - 您可以通过简单地滚动加载更多项目的功能,或者使用 加载更多按钮的类似设计。让我们看看它是如何使用的。

import { PuppeteerCrawler, sleep } from 'crawlee';

const crawler = new PuppeteerCrawler({
    headless: false,
    requestHandler: async ({ page, infiniteScroll }) => {
        const title = await page.title()
        console.log(title);

        // The crawler will keep scrolling and ...
        await infiniteScroll({
            // clicking this button, until ...
            buttonSelector: 'text/Load more',
            // this function returns true, which will
            // happen once GitHub has displayed 100 repos.
            stopScrollCallback: async () => {
                const repos = await page.$$('article.border');
                return repos.length >= 100;
            },
        })

        await sleep(10000);
    }
})

await crawler.run(['https://github.com/topics/javascript']);

添加新代码并运行上面的示例后,您应该看到Puppeteer自动单击和滚动,直到页面上可见100个存储库。将其扩展到200或1000个存储库就像更改stopScrollCallback中的数字一样简单。虽然数字非常高,但您可能需要use proxies

将我们的木偶代码添加到Crawlee

如前所述,Crawlee仅包装Puppeteer,因此您可以轻松地重复使用本教程第一部分中编写的刮擦代码。提醒一下,这是我们用来从回购卡中提取数据的刮擦代码。

const repos = await page.$$eval('article.border', (repoCards) => {
    return repoCards.map(card => {
        const [user, repo] = card.querySelectorAll('h3 a');
        const stars = card.querySelector('#repo-stars-counter-star')
            .getAttribute('title');
        const description = card.querySelector('div.px-3 > p');
        const topics = card.querySelectorAll('a.topic-tag');

        const toText = (element) => element && element.innerText.trim();
        const parseNumber = (text) => Number(text.replace(/,/g, ''));

        return {
            user: toText(user),
            repo: toText(repo),
            url: repo.href,
            stars: parseNumber(stars),
            description: toText(description),
            topics: Array.from(topics).map((t) => toText(t)),
        };
    });
});

要在我们的Crawlee crawler中使用它,您只需在infiniteScroll之后粘贴它即可提取所有数据。然后,爬网将结果打印到控制台。爬行者完成工作后,您将看到从100个存储库登录到终端的数据。

import { PuppeteerCrawler, sleep } from 'crawlee';

const crawler = new PuppeteerCrawler({
    headless: false,
    requestHandler: async ({ page, infiniteScroll }) => {
        const title = await page.title()
        console.log(title);

        await infiniteScroll({
            buttonSelector: 'text/Load more',
            stopScrollCallback: async () => {
                const repos = await page.$$('article.border');
                return repos.length >= 100;
            },
        })

        // This is exactly the same code as we used with pure Puppeteer.
        const repos = await page.$$eval('article.border', (repoCards) => {
            return repoCards.map(card => {
                const [user, repo] = card.querySelectorAll('h3 a');
                const stars = card.querySelector('#repo-stars-counter-star')
                    .getAttribute('title');
                const description = card.querySelector('div.px-3 > p');
                const topics = card.querySelectorAll('a.topic-tag');

                const toText = (element) => element && element.innerText.trim();
                const parseNumber = (text) => Number(text.replace(/,/g, ''));

                return {
                    user: toText(user),
                    repo: toText(repo),
                    url: repo.href,
                    stars: parseNumber(stars),
                    description: toText(description),
                    topics: Array.from(topics).map((t) => toText(t)),
                };
            });
        });

        // Print the repos to the console
        // to make sure everything works.
        console.log('Repository count:', repos.length);
        console.dir(repos);

        await sleep(10000);
    }
})

await crawler.run(['https://github.com/topics/javascript']);

现在,您已经提取了主题页面上可用的所有信息,让我们计算提交数量。这些仅在单个存储库页面上可用。这意味着我们必须取您收集的所有链接,用木偶访问它们,然后从其HTML中提取提交计数。

爬行的URL

crawlee为我们提供了一种与PuppeTeer爬网的简便方法,因为它将处理重点,网络错误并为您进行重新验证,而无需牺牲每个单独的页面的完全控制。要将存储库添加到队列中,让我们使用您已经提取的URL。

由于代码很长,我们将始终首先显示新段,然后显示完整的,可运行的示例。

首先,我们需要添加请求类的导入。

import { PuppeteerCrawler, Request } from 'crawlee';

然后,在requestHandler的末尾,我们添加了新代码,该代码在请求队列中添加了更多页面。

// Turn the repository data we extracted into new requests to crawl.
const requests = repos.map(repo => new Request({
    // URL tells Crawlee which page to open
    url: repo.url,
    // labels are helpful for easy identification of requests
    label: 'repository',
    // userData allows us to store any JSON serializable data.
    // It will be kept together with the request and saved
    // persistently, so that no data is lost on errors.
    userData: repo,
}));

// Add the requests to the crawler's queue.
// The crawler will automatically process them.
await crawler.addRequests(requests);

感谢上面的代码,Crawlee将打开所有单个存储库页面,但我们已经创建了一个问题。现在我们有两种页面要处理。初始主题页面,然后是所有存储库页。他们需要不同的逻辑。现在,让我们用一个简单的if语句来解决它,但是在稍后的教程中,我们将使用Router来清洁代码。

在crawlee中,最好通过其分配的label来确定请求。这就是为什么我们将repository标签添加到上一个代码示例中的请求中。

// inside the requestHandler
const title = await page.title()
console.log(title);

// We need to separate the logic for the original
// topic page and for the new repository page.
if (request.label === 'repository') {
    // For now, let's just confirm our crawler works
    // by logging the URLs it visits.
    console.log('Scraping:', request.url);
} else {
    // The original, topic page code goes here.
}

现在,我们确保单个页面类型具有正确的逻辑,让我们更改stopScrollCallback以立即通过将存储库的数量更改为20立即停止滚动。这将使我们在测试运行中更快地获得结果。<

// At the top of the file.
const REPO_COUNT = 20;

// Inside stopScrollCallback
return repos.length >= REPO_COUNT;

太好了。是时候运行爬网机以确认我们正确设置所有内容了。您可以尝试自己对代码进行以上更改,也可以在下面使用完整的可运行代码。

import { PuppeteerCrawler, Request } from 'crawlee';

const REPO_COUNT = 20;

const crawler = new PuppeteerCrawler({
    headless: false,
    requestHandler: async ({ request, page, infiniteScroll }) => {
        const title = await page.title()
        console.log(title);

        if (request.label === 'repository') {
            console.log('Scraping:', request.url);
        } else {
            await infiniteScroll({
                buttonSelector: 'text=Load more',
                stopScrollCallback: async () => {
                    const repos = await page.$$('article.border');
                    return repos.length >= REPO_COUNT;
                },
            });

            const repos = await page.$$eval('article.border', (repoCards) => {
                return repoCards.map(card => {
                    const [user, repo] = card.querySelectorAll('h3 a');
                    const stars = card.querySelector('#repo-stars-counter-star')
                        .getAttribute('title');
                    const description = card.querySelector('div.px-3 > p');
                    const topics = card.querySelectorAll('a.topic-tag');

                    const toText = (element) => element && element.innerText.trim();
                    const parseNumber = (text) => Number(text.replace(/,/g, ''));

                    return {
                        user: toText(user),
                        repo: toText(repo),
                        url: repo.href,
                        stars: parseNumber(stars),
                        description: toText(description),
                        topics: Array.from(topics)
                            .map((t) => toText(t)),
                    };
                });
            });

            console.log('Repository count:', repos.length);
            const requests = repos.map(repo => new Request({
                url: repo.url,
                label: 'repository',
                userData: repo,
            }));

            await crawler.addRequests(requests);
        }
    }
})

await crawler.run(['https://github.com/topics/javascript']);

正如运行上述代码后您会看到的那样,用打开的浏览器窗口刮擦会有些不知所措。我们建议您现在关闭头部模式,仅在需要调试时才将其打开。

// Delete this option in PuppeteerCrawler
headless: false

提取提交数量

我们快到了。缺少的最后一件事是从单个存储库中提取提交数量。为此,我们需要回到浏览器DevTools并查看页面的结构。

Extracting commit counts from DevTools

现在我们知道HTML的外观,唯一要做的就是使用CSS选择器提取它。

将CSS选择器与Puppeteer一起使用

检查页面的HTML后,我们发现可以使用以下CSS选择器隔离提交数量:

span.d-none.d-sm-inline > strong

我们可以使用Puppeteer的page.$eval()函数使用CSS选择器找到任何元素,并使用我们在DevTool中找到的选择器提取其文本。 Puppeteer将在页面中找到我们的元素,并执行获得文本的函数。如前所述,重要的是要等待该元素出现在页面上,以防万一它动态渲染。

const commitCountSelector = 'span.d-none.d-sm-inline > strong';
await page.waitForSelector(commitCountSelector);
const commitText = await page.$eval(commitCountSelector, (el) => el.textContent);

解析提交数量

无论如何,CSS选择器只会使我们走得太远。在节省提交数量之前,我们必须从额外的字符中清理字符串并将其变成数字。

const numberStrings = commitText.match(/\d+/g);
const commitCount = Number(numberStrings.join(''));

最后,我们将所有这些代码组合到我们的if语句的第一部分,这是完整的,可运行的示例。

import { PuppeteerCrawler, Request } from 'crawlee';

const REPO_COUNT = 20;

const crawler = new PuppeteerCrawler({
    requestHandler: async ({ request, page, infiniteScroll }) => {
        const title = await page.title()
        console.log(title);

        if (request.label === 'repository') {
            const commitCountSelector = 'span.d-none.d-sm-inline > strong';
            await page.waitForSelector(commitCountSelector);
            const commitText = await page.$eval(commitCountSelector, (el) => el.textContent);
            const numberStrings = commitText.match(/\d+/g);
            const commitCount = Number(numberStrings.join(''));
            console.log(commitCount);
        } else {
            await infiniteScroll({
                buttonSelector: 'text=Load more',
                stopScrollCallback: async () => {
                    const repos = await page.$$('article.border');
                    return repos.length >= REPO_COUNT;
                },
            });

            const repos = await page.$$eval('article.border', (repoCards) => {
                return repoCards.map(card => {
                    const [user, repo] = card.querySelectorAll('h3 a');
                    const stars = card.querySelector('#repo-stars-counter-star')
                        .getAttribute('title');
                    const description = card.querySelector('div.px-3 > p');
                    const topics = card.querySelectorAll('a.topic-tag');

                    const toText = (element) => element && element.innerText.trim();
                    const parseNumber = (text) => Number(text.replace(/,/g, ''));

                    return {
                        user: toText(user),
                        repo: toText(repo),
                        url: repo.href,
                        stars: parseNumber(stars),
                        description: toText(description),
                        topics: Array.from(topics)
                            .map((t) => toText(t)),
                    };
                });
            });

            console.log('Repository count:', repos.length);
            const requests = repos.map(repo => new Request({
                url: repo.url,
                label: 'repository',
                userData: repo,
            }));

            await crawler.addRequests(requests);
        }
    }
})

await crawler.run(['https://github.com/topics/javascript']);

运行上述代码时,您将看到所有存储库的提交计数登录到控制台。

保存提取的数据

但是,将数据记录到控制台的生产中不是很有用,所以让我们使用Crawlee的Dataset类将刮擦数据保存到磁盘上。

import { PuppeteerCrawler, Request, Dataset } from 'crawlee';

// ... inside requestHandler

await Dataset.pushData({
    ...request.userData,
    commitCount,
});

请记住,我们将有关从主题页面提取的回购的所有信息保存到了请求的userData属性。现在,我们可以轻松地将这些数据与我们的commitCount合并,并将整个对象保存到磁盘上。这将在以下目录中为每个存储库创建一个JSON文件。

./storage/datasets/default

您可以去那里检查文件,您会发现这样的JSON。

{
    "user": "vuejs",
    "repo": "vue",
    "url": "https://github.com/vuejs/vue",
    "stars": 201555,
    "description": "🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.",
    "topics": [
        "javascript",
        "framework",
        "vue",
        "frontend"
    ],
    "label": "repository",
    "commitCount": 3544
}

为了使数据更容易处理,我们可以将其保存到具有一行代码的CSV或一个大JSON中。

// This should be added as the very last
// function call. After await crawler.run()
await Dataset.exportToCSV('repositories');

crawlee将在此位置保存CSV:

./storage/key_value_stores/default/repositories.csv

最终可运行的代码看起来像这样。我们还可以将REPO_COUNT返回100,以获得前100个JavaScript存储库,但这很可能会导致GitHub受到IP率的限制。因此,让我们将其仅增加到40。您可以使用代理来处理费率限制。

import { PuppeteerCrawler, Request, Dataset } from 'crawlee';

const REPO_COUNT = 40;

const crawler = new PuppeteerCrawler({
    requestHandler: async ({ request, page, infiniteScroll }) => {
        const title = await page.title()
        console.log(title);

        if (request.label === 'repository') {
            const commitCountSelector = 'span.d-none.d-sm-inline > strong';
            await page.waitForSelector(commitCountSelector);
            const commitText = await page.$eval(commitCountSelector, (el) => el.textContent);
            const numberStrings = commitText.match(/\d+/g);
            const commitCount = Number(numberStrings.join(''));

            await Dataset.pushData({
                ...request.userData,
                commitCount,
            });
        } else {
            await infiniteScroll({
                buttonSelector: 'text=Load more',
                stopScrollCallback: async () => {
                    const repos = await page.$$('article.border');
                    return repos.length >= REPO_COUNT;
                },
            });

            const repos = await page.$$eval('article.border', (repoCards) => {
                return repoCards.map(card => {
                    const [user, repo] = card.querySelectorAll('h3 a');
                    const stars = card.querySelector('#repo-stars-counter-star')
                        .getAttribute('title');
                    const description = card.querySelector('div.px-3 > p');
                    const topics = card.querySelectorAll('a.topic-tag');

                    const toText = (element) => element && element.innerText.trim();
                    const parseNumber = (text) => Number(text.replace(/,/g, ''));

                    return {
                        user: toText(user),
                        repo: toText(repo),
                        url: repo.href,
                        stars: parseNumber(stars),
                        description: toText(description),
                        topics: Array.from(topics)
                            .map((t) => toText(t)),
                    };
                });
            });

            console.log('Repository count:', repos.length);
            const requests = repos.map(repo => new Request({
                url: repo.url,
                label: 'repository',
                userData: repo,
            }));

            await crawler.addRequests(requests);
        }
    }
})

await crawler.run(['https://github.com/topics/javascript']);
await Dataset.exportToCSV('repositories');

运行此代码时,您会看到爬网将单个页面标题打印到控制台中,并且在完成后,您会在上面显示的位置找到CSV。

部署到云

在教程的这一点上,我将借此机会做一些自我宣传。我是Apify的首席运营官,这是一个云平台,可帮助您轻松有效地开发,运行和维护网络刮刀。它带有许多功能,例如队列储藏和代理,它支持伪造者没有任何额外的配置。您可以运行上述刮板,保存结果并使用功能强大的API控制所有内容,并且可以比AWS或类似的通用云快10倍。

要了解更多信息,请访问our homepage或直接跳到Apify Academy中的Getting Started课程,您可以在其中找到更多的upeTeer和Web上的免费课程。

奖金:路由

早些时候我提到,与将所有内容放入一个功能中相比,使用Puppeteer和Crawlee构造代码的更好方法。在本节中,我们将探讨koude28 crawlee类,以及如何使用它使代码更易于管理。

到目前为止,我们将所有代码保存在一个名为crawlee.js的文件中。我们将添加一个新的文件router.js,并将所有请求处理逻辑移动到其中。多亏了路由器,我们可以将我们在requestHandler中的代码拆分到我们想要的尽可能多的函数中,并且轨道将基于每个Request使用的label自动路由逻辑。

// router.js
import { createPuppeteerRouter, Dataset, Request } from 'crawlee';

// We create a Puppeteer specific router to
// get intellisense and typechecks for our IDE.
export const router = createPuppeteerRouter();

const REPO_COUNT = 40;

router.use(async (ctx) => {
    // This is for middlewares - functions that will be
    // executed on all routes, irrespective of label.
})

router.addHandler('repository', async (ctx) => {
    // This handler will execute for all requests
    // with the 'repository' label.
});

router.addDefaultHandler(async (ctx) => {
    // This handler will execute for requests
    // that don't have a label.
});

然后,我们可以将现有逻辑移至此骨架中,而router.js文件看起来像这样。

// router.js
import { createPuppeteerRouter, Dataset, Request } from 'crawlee';

export const router = createPuppeteerRouter();

const REPO_COUNT = 40;

router.use(async ({ page }) => {
    const title = await page.title()
    console.log(title);
})

router.addHandler('repository', async ({ page, request }) => {
    const commitCountSelector = 'span.d-none.d-sm-inline > strong';
    await page.waitForSelector(commitCountSelector);
    const commitText = await page.$eval(commitCountSelector, (el) => el.textContent);
    const numberStrings = commitText.match(/\d+/g);
    const commitCount = Number(numberStrings.join(''));

    await Dataset.pushData({
        ...request.userData,
        commitCount,
    });
});

router.addDefaultHandler(async ({ page, infiniteScroll, crawler }) => {
    await infiniteScroll({
        buttonSelector: 'text=Load more',
        stopScrollCallback: async () => {
            const repos = await page.$$('article.border');
            return repos.length >= REPO_COUNT;
        },
    });

    const repos = await page.$$eval('article.border', (repoCards) => {
        return repoCards.map(card => {
            const [user, repo] = card.querySelectorAll('h3 a');
            const stars = card.querySelector('#repo-stars-counter-star')
                .getAttribute('title');
            const description = card.querySelector('div.px-3 > p');
            const topics = card.querySelectorAll('a.topic-tag');

            const toText = (element) => element && element.innerText.trim();
            const parseNumber = (text) => Number(text.replace(/,/g, ''));

            return {
                user: toText(user),
                repo: toText(repo),
                url: repo.href,
                stars: parseNumber(stars),
                description: toText(description),
                topics: Array.from(topics)
                    .map((t) => toText(t)),
            };
        });
    });

    console.log('Repository count:', repos.length);
    const requests = repos.map(repo => new Request({
        url: repo.url,
        label: 'repository',
        userData: repo,
    }));

    await crawler.addRequests(requests);
})

如果这对您来说仍然太多,请随时将其拆分。例如,每个文件一条路由。删除了爬行逻辑后,crawlee.js现在非常简短且可读。

// crawlee.js
import { Dataset, PuppeteerCrawler } from 'crawlee';
import { router } from './router.js';

const crawler = new PuppeteerCrawler({
    requestHandler: router
})

await crawler.run(['https://github.com/topics/javascript'])
await Dataset.exportToCSV('repositories');

运行crawlee.js时,它的行为将与拆分之前完全相同,但是由于路由器,该代码将易于阅读和维护。

接下来要学什么?

所以你有。完整的网络刮擦和爬行教程。如果您有兴趣了解更多有关木偶的信息,以及一般的网络刮擦,请访问我们的免费学院课程,在其中我们在其中更详细地详细介绍puppeteer功能,其中包括代码示例和详细说明。

如果您只是刚开始并想学习网络刮擦的基础知识,我们为初学者的网络抓取课程会解释基本概念,并准备好应对更加困难的挑战。

无论如何,快乐刮擦