Puppeteer和NodeJS抓取网页

如何使用Puppeteer和NodeJS抓取网页

Web 抓取和爬行是从 Web 中自动提取大量数据的过程。数据提取正在兴起,但大多数网站不通过 API 提供数据。按照本教程学习如何使用 Puppeteer 在 NodeJS 中进行网络抓取并提取该信息。

无头浏览器正在蓬勃发展,因为反机器人系统很普遍并且可供任何人使用。使用Axios 等静态抓取解决方案绕过防御软件几乎是不可能的。这就是使用 Puppeteer 进行网页抓取的用武之地。

另一个主要优势是从使用 JavaScript 呈现的网站中提取内容,称为动态抓取

您可能知道,Puppeteer是一个 Node 库,它提供高级 API 以通过DevTools Protocol控制 Chrome 或 Chromium 。它允许我们以编程方式使用无头浏览器浏览 Internet。专为测试而设计,我们将了解如何使用 Puppeteer 进行网页抓取。

Puppeteer 允许您执行几乎所有您可以在浏览器中手动执行的操作。访问页面、单击链接、提交表单、截取屏幕截图等等。

Puppeteer 可以用于网页抓取吗?

当然,网页抓取是 Puppeteer 的第二大最常见用途!😹

应用这些特性加上提取内容的能力,我们开始了解如何构建爬虫和解析器。我们可以告诉 Puppeteer 访问我们的目标页面,选择一些元素,并从中提取数据。然后,解析页面上所有可用的链接并将它们添加到抓取中。

听起来像网络抓取工具吗?让我们开始吧!

使用 Puppeteer 进行网页抓取有哪些优势?

Axios 和 Cheerio是使用 Javascript 进行网页抓取的绝佳选择。这种方法有两个问题:抓取动态内容和反抓取软件。稍后我们将看到如何避免和绕过反机器人系统。

由于 Puppeteer 是无头浏览器,因此它对动态内容没有问题。它将加载目标页面并运行页面上的 Javascript。也许触发 XHR 请求以获得额外的内容。您将无法使用静态刮板提取它。或者单页应用程序 (SPA),其中初始 HTML 几乎没有数据。

它还将渲染图像并允许您截取屏幕截图。您可以对脚本进行编程以转到特定页面并在每天的同一时间截取屏幕截图。然后分析它们以获得竞争优势。选择是无穷无尽的!

先决条件

要使代码正常工作,您需要安装Node(或nvm)和 npm。有些系统已经预装了它。之后,通过运行安装所有必需的库npm install。它将创建一个包含所有依赖项的 package.json 文件。

npm install puppeteer

该代码在 Node v16 中运行,但您始终可以检查每个功能的兼容性

你如何使用 Puppeteer 来抓取网站?

安装 Puppeteer 后,您就可以开始抓取了!打开你最喜欢的编辑器,创建一个新文件 – index.js– 并添加以下代码:

const puppeteer = require('puppeteer'); 
 
(async () => { 
    // Initiate the browser 
    const browser = await puppeteer.launch(); 
     
    // Create a new page with the default browser context 
    const page = await browser.newPage(); 
 
    // Go to the target website 
    await page.goto('https://example.com'); 
 
    // Get pages HTML content 
    const content = await page.content(); 
    console.log(content); 
 
    // Closes the browser and all of its pages 
    await browser.close(); 
})();

您可以使用node test.js. 它将打印示例页面的 HTML 内容,<title>Example Domain</title>例如包含 。

使用 Puppeteer 选择节点

对于大多数用例来说,打印整个页面可能不是一个好的解决方案。我们最好选择页面的部分内容并访问它们的内容或属性

example-org-annotated

正如我们在上面看到的,我们可以从突出显示的节点中提取相关数据。为此,我们将使用CSS 选择器。Puppeteer Page API公开了访问页面的方法,就像用户浏览一样。

  • $(selector)就像document.querySelector,会找到一个元素。
  • $$(selector)executes document.querySelectorAll,找到所有匹配的节点。
  • $x(expression)计算 XPath 表达式,这对于在页面或节点上查找文本很有用。
  • evaluate(pageFunction, args)将在浏览器上执行任何 Javascript 指令并返回结果。

还有很多。它为我们提供了所需的所有灵活性。

我们现在将获得提到的节点。包含链接的h1标题和a标签属性的 。href

await page.goto('https://example.com'); 
 
// Get the node and extract the text 
const titleNode = await page.$('h1'); 
const title = await page.evaluate(el => el.innerText, titleNode); 
 
// We can do both actions with one command 
// In this case, extract the href attribute instead of the text 
const link = await page.$eval('a', anchor => anchor.getAttribute('href')); 
 
console.log({ title, link });

输出将是这样的:

{ 
    title: 'Example Domain', 
    link: 'https://www.iana.org/domains/example' 
}

正如我们在代码片段中看到的那样,有几种方法可以实现该结果。Puppeteer 公开了几个函数,允许您自定义数据提取

所有信息在第一次加载时就已经存在,但我们如何才能获得动态内容呢?

等待内容加载或出现

我们如何抓取不存在的数据?它可能在脚本上(React 使用 JSON 对象)或在对服务器的 XHR 请求之后。Puppeteer 允许我们等待内容。它可能会等待网络状态或元素可见。这里我们提到了一些,但是,同样,还有更多。查看页面 API 文档以获取更多信息。

  • waitForNetworkIdle停止脚本直到网络空闲。
  • waitForSelectorselector暂停,直到出现匹配的节点。
  • waitForNavigation等待浏览器导航到新的 URL。
  • waitForTimeout休眠数毫秒,但现在已过时且不推荐使用。

我们现在将切换目标网站并转到 YouTube 视频。例如,评论是在 XHR 请求后异步加载的。这意味着我们必须等待该内容出现

youtube_recommended_videos

您可以在下面看到如何使用waitForNetworkIdle.

(async () => { 
    const browser = await puppeteer.launch(); 
    const page = await browser.newPage(); 
    await page.goto('https://www.youtube.com/watch?v=tmNXKqeUtJM'); 
 
    const videosTitleSelector = '#items h3 #video-title'; 
    await page.waitForSelector(videosTitleSelector); 
    const titles = await page.$$eval( 
        videosTitleSelector, 
        titles => titles.map(title => title.innerText) 
    ); 
    console.log(titles ); 
 
    // [ 
    //	 'Why Black Holes Could Delete The Universe – The Information Paradox', 
    //	 'Why All The Planets Are On The Same Orbital Plane', 
    //	 ... 
    // ] 
 
    await browser.close(); 
})();

我们也可以使用waitForNetworkIdle. 输出将是相似的,但背后的解释不同。第二个选项将暂停执行,直到网络空闲。在页面充满项目、加载缓慢或流式传输内容等的某些情况下,它可能不起作用。

waitForSelector,相比之下,只会检查至少有一个评论标题存在。当出现这样的节点时,它会返回并继续执行。

既然我们可以抓取数据,那么我们如何抓取新页面呢?

如何使用 Puppeteer 抓取多个页面?

我们首先需要链接!我们无法仅使用我们的种子 URL 来构建网络爬虫。假设我们只对相关视频感兴趣。上一节中显示的那些。但是现在,我们应该抓取链接而不是标题

page.waitForSelector('#items h3 #video-title'); 
 
const videoLinks = await page.$$eval( 
    '#items .details a', 
    links => links.map(link => link.href) 
); 
console.log(videoLinks); 
 
// [ 
//	'https://www.youtube.com/watch?v=ceFl7NlpykQ', 
//	'https://www.youtube.com/watch?v=d3zTfXvYZ9s', 
//	'https://www.youtube.com/watch?v=DxQK1WDYI_k', 
//	'https://www.youtube.com/watch?v=jSMZoLjB9JE', 
// ]

伟大的!您现在从一个页面变成了 20 个页面。下一步是导航到这些页面并继续提取数据和更多链接。

我们已经介绍了Javascript 中的网络爬虫主题,这里不再赘述。好吧!快速回顾一下我们所看到的基础知识:

  1. 安装并运行 Puppeteer。
  2. 使用选择器抓取数据。
  3. 从 HTML 中提取链接。
  4. 抓取新链接。
  5. 从#2 开始重复。

其他 Puppeteer 功能

现在我们已经介绍了使用 Puppeteer 进行网页抓取的基础知识,让我们来看看其他功能。

如何使用 Puppeteer 截取屏幕截图?

如果你能看到刮板在做什么,那就太好了,对吧?

您始终可以使用puppeteer.launch({ headless: false });但这不是大规模抓取的真正解决方案。

幸运的是,Puppeteer 还提供了截图功能。这对于调试很有用,而且,例如,对于创建目标页面的快照也很有用。

await page.screenshot({ path: 'youtube.png', fullPage: true });

就这么简单!该fullPage参数默认为 false,因此您必须将其传递为true获取整个页面的内容。

稍后我们将看到这对我们有何帮助。

使用 Puppeteer 执行 Javascript

如您所知,Puppeteer 提供多种功能来与目标站点和浏览器进行交互。但假设您想做一些不同的事情。您可以直接在浏览器上使用 Javascript 运行的东西。你怎么能那样做?

Puppeteer 的evaluate函数将采用任何 Javascript 函数或表达式并为您执行。使用evaluate,您几乎可以在页面上执行任何操作。添加或删除 DOM 节点。修改样式。检查项目localStorage并在节点上公开它们。读取和修改 cookie。

const storageItem = await page.evaluate("JSON.parse(localStorage.getItem('ytidb::LAST_RESULT_ENTRY_KEY'))"); 
console.log(storageItem); 
 
// { 
//	data: { hasSucceededOnce: true }, 
//	expiration: 1665752343778, 
//	creation: 1663160343778 
// }

我们选择了这个localStorage例子。您可以访问 YouTube 生成的密钥并使用 返回它evaluate。如前所述,您几乎可以控制浏览器上发生的任何事情。甚至是用户看不到的东西。

考虑到我们没有添加异常控制或任何为简洁起见的防御措施。在上面的例子中,如果键不存在,它将返回null。但是其他一些问题可能会失败并破坏您的刮刀。

使用 Puppeteer 提交表单

浏览时的另一个典型操作是提交表单。您可以在网络抓取时复制该行为。一个用例是使用 Puppeteer 登录网站。

按照我们的示例,我们将填写搜索表单并提交。为此,我们需要单击一个按钮并键入文本。我们的入口点将是相同的。然后,搜索表单会将我们带到另一个页面。

请记住,Puppeteer 将在没有 cookie 的情况下浏览网页– 除非您另有说明。在 YouTube 的例子中,这意味着在页面顶部看到 cookie 横幅。在您接受或拒绝它们之前,您无法与该页面进行交互。所以我们必须删除它。否则,搜索将无法进行。

youtube_cookies

我们必须找到按钮,等待它出现,单击它,然后等待一秒钟。最后一步是对话框消失所必需的。

根据click 的文档,我们应该使用 click and wait for navigation Promise.all。它在我们的案例中不起作用,所以我们选择了替代方法。

不要担心长 CSS 选择器。有几个按钮,我们必须具体说明。此外,YouTube 使用自定义 HTML 元素,例如ytd-button-renderer.

const cookieConsentSelector = 'tp-yt-paper-dialog .eom-button-row:first-child ytd-button-renderer:first-child'; 
 
await page.waitForSelector(cookieConsentSelector); 
page.click(cookieConsentSelector); 
await page.waitForTimeout(1000);

下一步是填写表格。在这种情况下,我们将使用两个 Puppeteer 函数:type输入查询和press通过点击提交表单Enter。我们也可以click按钮。

如您所见,我们正在对用户将直接在浏览器上执行的指令进行编码。

const searchInputEl = await page.$('#search-form input'); 
await searchInputEl.type('top 10 songs'); 
await searchInputEl.press('Enter');

最后,等待搜索页面加载,并截图。我们已经了解了如何使用 Puppeteer 来做到这一点。

await page.waitForSelector('ytd-two-column-search-results-renderer ytd-video-renderer'); 
await page.screenshot({ path: 'youtube_search.png', fullPage: true });

youtube_search

在 Puppeteer 中阻止或拦截请求

正如您在屏幕截图中看到的,刮板正在加载图像。这有利于调试目的。但不适用于大型爬虫项目。

Web 抓取工具应该优化资源并尽可能提高抓取速度。不加载图片很容易。为此,我们可以利用Puppeteer 对资源阻塞或拦截请求的支持

例如,通过调用page.setRequestInterception(true),Puppeteer 将使您能够检查请求并根据类型中止它们。在访问页面之前运行这部分是至关重要的。

await page.setRequestInterception(true); 
 
// Check for files that end/contains png or jpg 
page.on('request', interceptedRequest => { 
    if ( 
        interceptedRequest.url().endsWith('.png') || 
        interceptedRequest.url().endsWith('.jpg') || 
        interceptedRequest.url().includes('.png?') || 
        interceptedRequest.url().includes('.jpg?') 
    ) { 
        interceptedRequest.abort(); 
    } else { 
        interceptedRequest.continue(); 
    } 
}); 
 
// Go to the target website 
await page.goto('https://www.youtube.com/watch?v=tmNXKqeUtJM');

不是最优雅的解决方案 – 目前 – 但它完成了工作。

youtube_recommended_videos_wihtout_images

每个被拦截的请求都是一个HTTPRequest。除了 URL 之外,您还可以访问资源类型。它使我们更容易屏蔽所有图像

// list the resources we don't want to load 
const excludedResourceTypes = ['stylesheet', 'image', 'font', 'media', 'other', 'xhr', 'manifest']; 
page.on('request', interceptedRequest => { 
    // block resources based in their type 
    if (excludedResourceTypes.includes(interceptedRequest.resourceType())) { 
        interceptedRequest.abort(); 
    } else { 
        interceptedRequest.continue(); 
    } 
});

这是使用 Puppeteer 阻止请求的两种主要方式。我们可以详细检查 URL 或以更通用的类型方法阻止。

不细说了,有一个插件可以屏蔽资源。还有一个更具体的实现adblocker的。

通过阻止这些资源,您可能会节省 80% 的带宽!毫不奇怪,当今互联网上的大部分内容都是基于图像或视频的。这些比纯文本更重要。

最重要的是,更少的流量意味着更快的抓取

而且,如果您使用的是计量代理,那么抓取也更便宜。

说到代理,我们如何在 Puppeteer 中使用它们?

避免机器人检测

反机器人软件越来越普遍也就不足为奇了。由于易于集成,几乎任何网站都可以运行防御性解决方案。如果您加入我们,您将学习如何使用 Puppeteer 绕过反机器人解决方案:例如CloudflareAkamai 。

正如您可能已经猜到的那样,避免检测的第一个也是更常见的解决方案是代理。

在 Puppeteer 中使用代理

代理是充当您的连接和目标站点之间的中介的服务器。您会将您的请求发送到代理,然后它会将它们中继到最终服务器。

为什么我们需要中介?正如您可能已经猜到的那样,它会更慢。但对网页抓取更有效。

禁止爬虫的最简单方法可能是通过其 IP。一天内来自同一个 IP 的数百万个请求?这是显而易见的,任何防御系统都会阻止这些连接。

但是多亏了代理,你可以拥有不同的 IP轮换代理可以为每个请求分配一个新的 IP,使反机器人更难禁止您的爬虫。

我们将为演示使用免费代理,但我们不推荐它们。它们可能适用于测试,但并不可靠。请注意,下面的方法可能不适合您。他们的寿命很短。

(async () => { 
    const browser = await puppeteer.launch({ 
        // pass the proxy to the browser 
        args: ['--proxy-server=23.26.236.11:3128'], 
    }); 
    const page = await browser.newPage(); 
 
    // example page that will print the calling IP address 
    await page.goto('https://www.httpbin.org/ip'); 
 
    const ip = await page.$eval('pre', node => node.innerText); 
    console.log(ip); 
    // { 
    //	"origin": "23.26.236.11" 
    // } 
 
    await browser.close(); 
})();

Puppeteer 接受 Chromium 将在启动时设置的一组参数。您可以查看他们关于网络设置的文档以获取更多信息。

此实现将使用相同的代理发送所有爬虫的请求。这可能不适合你。如上所述,除非您的代理轮换 IP,否则目标服务器将一次又一次地看到相同的 IP。并禁止它。

幸运的是,有Node JS 库可以帮助我们旋转代理puppeteer-page-proxy支持 HTTP 和 HTTPS 代理、身份验证以及更改每页使用的代理。甚至每个请求,多亏了请求拦截(正如我们之前看到的)。

避免使用高级代理进行地理封锁

一些反机器人供应商,如 Cloudflare,允许客户按位置自定义挑战级别

让我们以一家位于法国的商店为例。它可能会在欧洲其他地区销售一小部分,但不会运往世界其他地区。

在这种情况下,具有不同级别的严格性是有意义的。欧洲的安全性较低,因为在那里浏览网站更为常见。从外部访问时具有更高的挑战选项。

解决方法同上一节:代理。在这种情况下,他们必须允许geolocation。高级或住宅代理通常提供此功能。您会为每个想要的国家/地区获得不同的 URL,并且这些 URL 将仅使用来自所选国家/地区的 IP。它们可能看起来像这样"http://my-user--country-FR:my-password@my-proxy-provider:1234"

在 Puppeteer 中设置 HTTP 标头

默认情况下,PuppeteerHeadlessChrome作为其用户代理发送。不需要最新的技术就可以意识到它可能是网络抓取软件。

同样,有几种方法可以在 Puppeteer 中设置 HTTP 标头。最常见的一种是使用setExtraHTTPHeaders. 在访问该页面之前,您必须执行所有与标题相关的功能。像这样,它将在访问任何外部站点之前拥有所有必需的数据集。

但是,如果您使用它来设置用户代理,请小心使用它。

const page = await browser.newPage(); 
 
// set headers 
await page.setExtraHTTPHeaders({ 
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36', 
    'custom-header': '1', 
}); 
 
// example page that will print the sent headers 
await page.goto('https://www.httpbin.org/headers'); 
 
const pageContent = await page.$eval('pre', node => JSON.parse(node.innerText)); 
const userAgent = await page.evaluate(() => navigator.userAgent); 
console.log({ headers: pageContent.headers, userAgent }); 
 
// { 
//	 headers: { 
//		'Custom-Header': '1', 
//		'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36', 
//		... 
//	 }, 
//	 userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/101.0.4950.0 Safari/537.36' 
// }

你能发现 Antibot 系统的不同之处吗?

我们发送了标题,没有问题。但是navigator.userAgent浏览器上显示的属性是默认属性——一个简单的检查。我们将在下面的代码片段中添加另外两个 (appVersionplatform) 以查看它是否正确。

让我们尝试下一种更改用户代理的方法:通过浏览器创建时的参数。

const browser = await puppeteer.launch({ 
    args: ['--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36'], 
}); 
 
const appVersion = await page.evaluate('navigator.appVersion'); 
const platform = await page.evaluate('navigator.platform'); 
 
// { 
//	userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36', 
//	appVersion: '5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36', 
//	platform: 'Linux x86_64', 
// }

ups,我们这里有问题。现在我们将尝试第三个选项:setUserAgent

await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36'); 
 
// { 
//	userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36', 
//	appVersion: '5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36', 
//	platform: 'Linux x86_64', 
// }

结果和以前一样。至少这个选项更容易处理,我们可以根据请求更改用户代理。我们可以将它与user-agents NodeJS 包结合起来。

一定有解决办法吧?再一次,Puppeteer 自带evaluateOnNewDocument,它可以在访问页面之前改变navigator对象。这意味着目标页面将看到我们想要显示的内容。

为此,我们必须覆盖该platform属性。当 Javascript 访问该值时,下面的函数将返回一个硬编码的字符串。

await page.evaluateOnNewDocument(() => 
    Object.defineProperty(navigator, 'platform', { 
        get: function () { 
            return 'Win32'; 
        }, 
    }) 
); 
 
// { 
//	userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36', 
//	platform: 'Win32', 
// }

我们现在可以设置自定义标头和用户代理。另外,修改不匹配的属性。

我们稍微简化了这个检测部分。为了避免这些问题,通常添加一个名为 stealth 的 Puppeteer 插件。他们还使用这种技术来避免被发现。您可以浏览代码,它是开源的。

如果您有兴趣,我们写了一篇关于如何避免机器人检测的指南。它使用 Python 作为代码示例,但原理是相同的。

您现在可以开始使用 Puppeteer 进行抓取并提取所需的所有数据。

结论

在此 Puppeteer 教程中,您学习了从安装到高级主题的所有知识。不要忘记使用 Puppeteer 或其他无头浏览器进行网络抓取的两个主要原因:提取动态数据和绕过反机器人系统。

我们希望您明确 5 个要点:

  1. 在哪些情况下以及为什么使用 Puppeteer 进行网页抓取。
  2. 安装并应用基础知识以开始提取数据。
  3. CSS 选择器来获取你想要的数据。
  4. 如果您可以手动完成,Puppeteer 可能有适合您的功能。
  5. 使用良好的代理和 HTTP 标头避免机器人检测。

没有人说使用 Puppeteer 进行网页抓取很容易,但您更接近于访问您想要的任何内容!

我们遗漏了很多东西,所以当您需要其他功能时,请查看官方文档。请记住,我们也没有涵盖网络抓取。您需要从一页到数千页并扩展您的抓取系统

类似文章