返回博客
指南
Mihnea-Octavian ManolacheLast updated on Mar 31, 20262 min read

如何使用 Puppeteer 构建爬虫并下载文件

如何使用 Puppeteer 构建爬虫并下载文件

如果你从事网页抓取工作且使用 Node.js,那么你很可能听说过 Puppeteer。而且你肯定遇到过需要使用 Puppeteer 下载文件的任务。这确实是抓取社区中一个常见的任务。但 Puppeteer 的官方文档对此并未做详细说明。

幸运的是,我们将共同解决这个问题。本文将探讨 Puppeteer 中的文件下载功能。今天我想带大家了解两个重点:

  • 深入理解 Puppeteer 如何处理下载任务
  • 使用 Node.js 和 Puppeteer 构建一个可运行的文件下载爬虫

读完本文后,你将掌握开发人员构建文件爬虫所需的理论与实践技能。如果这个项目听起来像我一样令人兴奋,那就让我们开始吧!

为何要使用 Puppeteer 下载文件?

文件爬虫的应用场景非常广泛,StackOverflow 上也充斥着众多开发者寻求如何使用 Puppeteer 下载文件的解答。我们需要明白,文件涵盖了图片、PDF、Excel 或 Word 文档等众多类型。不难理解,这些文件为何能为某些人提供至关重要的信息。

例如,代发货行业的企业往往依赖从外部来源(如电商平台)抓取的图片。另一个典型的应用场景是用于监控官方文件的公司,甚至小型项目。我本人就有一段脚本,专门从合作伙伴的网站下载发票。

说到使用 Puppeteer 下载文件,我发现大多数人选择它的主要原因有两个:

  • 它专为 Node.js 设计,而 Node.js 是最受欢迎的编程语言之一,无论在前端还是后端都广受青睐。
  • 它能打开真实的浏览器,而某些网站依赖 JavaScript 来呈现内容。这意味着,如果你使用无法渲染 JavaScript 文件的普通 HTTP 客户端,将无法下载这些文件。

Puppeteer 如何处理文件下载

要理解如何使用 Puppeteer 下载文件,我们必须先了解 Chrome 是如何实现的。这是因为,从本质上讲,Puppeteer 是一个通过 Chrome DevTools 协议(CDP)“控制”Chrome 的库。

在 Chrome 中,文件可以通过以下方式下载:

  • 手动下载,例如点击按钮
  • 通过编程方式,利用 CDP 中的 Page Domain 实现

此外,在网页抓取中还存在第三种技术。即引入一个新的参与者:HTTP 客户端。通过这种方式,网页抓取工具会收集文件的 `href` 链接,随后使用 HTTP 客户端下载这些文件。每种方法都有其特定的应用场景,因此我们将探讨这两种方式。

在 Puppeteer 中通过点击按钮下载文件

幸运的是,若您要抓取文件的网站使用了按钮,您只需在 Puppeteer 中模拟点击事件即可。在此场景下,文件下载器的实现非常简单。Puppeteer 甚至对 Page.click() 方法进行了文档说明,您可在此处查阅更多信息。

由于这是“类人”行为,我们需要做的是:

  • 打开浏览器
  • 导航至目标网页
  • 定位按钮元素(例如通过其 CSS 选择器或 xPath)
  • 点击该按钮

脚本中只需实现这四个简单的步骤。不过,在开始编写代码之前,请注意:与您日常使用的浏览器一样,由 Puppeteer 控制的 Chrome 实例会将下载的文件保存到默认下载文件夹中,路径为:

  • Windows 系统:\Users\<username>\Downloads
  • /Users/<username>/Downloads(适用于 Mac)
  • Linux 系统:/home/<用户名>/Downloads

了解这一点后,让我们开始编写代码。假设我们是天体物理学家,需要从 NASA 收集一些数据,以便后续处理。目前,我们先专注于下载 .doc 文件。

#1:识别“可点击”元素

我们将访问 NASA 的网站并检查页面元素。我们的重点是识别“可点击”元素。要查找这些元素,请打开开发者工具(Chrome 浏览器中为 Command + Option + I / Control + Shift + I):

#2:编写项目代码

import puppeteer from "puppeteer"

(async () => {

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

   const page = await browser.newPage()

   await page.goto('https://www.nasa.gov/centers/dryden/research/civuav/civ_uav_doc-n-ref.html',

       { waitUntil: 'networkidle0' })

   const tr_elements = await page.$x('html/body/div[1]/div[3]/div[2]/div[2]/div[5]/div[1]/table[2]/tbody/tr')

   for (let i = 2; i<=tr_elements.length; i ++) {

       const text = await tr_elements[i].evaluate(el => el.textContent)

       if (text.toLocaleLowerCase().includes('doc')) {

           try {

               await page.click(`#backtoTop > div.box_710_cap > div.box_710.box_white.box_710_white > div.white_article_wrap_detail.text_adjust_me > div.default_style_wrap.prejs_body_adjust_detail > table:nth-child(6) > tbody > tr:nth-child(${i}) > td:nth-child(3) > a`)

           }catch {}

       }

   }

   await browser.close()

})()

此处的操作步骤如下:

  • 启动 Puppeteer 并导航至目标网站
  • 选中所有 `tr` 元素,这些元素包含我们稍后要点击的 `href` 属性
  • 遍历这些 `tr` 元素并 a. 检查元素内的文本是否包含“doc”一词 b. 若包含,则构建选择器并点击该元素
  • 关闭浏览器

就这样。使用 Puppeteer 下载文件可以简单到极致。

使用 CDP 在 Puppeteer 中下载文件

我知道对于小型项目,默认的下载文件夹并不是什么大问题。但对于大型项目,您肯定希望将 Puppeteer 下载的文件整理到不同的目录中。 而这就是 CDP 派上用场的地方。为了实现这一目标,我们将保留当前的代码并仅在此基础上进行扩展。

首先想到的是解析当前目录的路径。幸运的是,我们可以使用内置的 `node:path` 模块。我们只需将 `path` 模块导入项目,并使用 `resolve` 方法即可,稍后您将看到具体实现。

第二个方面是使用 CDP 在浏览器中设置路径。正如我之前所说,我们将使用 Page Domain 的 `.setDownloadBehavior` 方法。以下是添加这两项后的更新代码:

import puppeteer from "puppeteer"

import path from 'path'

(async () => {

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

   const page = await browser.newPage()

   const client = await page.target().createCDPSession()

   await client.send('Page.setDownloadBehavior', {

       behavior: 'allow',

       downloadPath: path.resolve('./documents')

   });

   await page.goto('https://www.nasa.gov/centers/dryden/research/civuav/civ_uav_doc-n-ref.html',

       { waitUntil: 'networkidle0' })

   const tr_elements = await page.$x('html/body/div[1]/div[3]/div[2]/div[2]/div[5]/div[1]/table[2]/tbody/tr')

   for (let i = 1; i<=tr_elements.length; i ++) {

       const text = await tr_elements[i].evaluate(el => el.textContent)

       if (text.toLocaleLowerCase().includes('doc')) {

           try {

               await page.click(`#backtoTop > div.box_710_cap > div.box_710.box_white.box_710_white > div.white_article_wrap_detail.text_adjust_me > div.default_style_wrap.prejs_body_adjust_detail > table:nth-child(6) > tbody > tr:nth-child(${i}) > td:nth-child(3) > a`)

           } catch {}

       }

   }

   await browser.close()

})()

以下是新增代码的功能说明:

  • 我们正在创建一个新的 CDPSession 来“直接使用 Chrome 开发者工具协议”
  • 我们触发 `Page.setDownloadBehavior` 事件,其中 a. `behavior` 设置为允许下载 b. `downloadPath` 使用 `node:path` 构建,指向我们将文件存储的文件夹

若想通过 Puppeteer 更改文件保存目录,只需完成上述操作即可。此外,我们已成功实现了构建文件下载型网络爬虫的目标。

使用 Puppeteer 和 Axios 下载文件

我们讨论的第三种方案是收集目标网站的链接,并使用 HTTP 客户端进行下载。我个人更倾向于使用 axios,但在网页抓取领域也有其他替代方案。因此,在本教程中,我将使用 axios,并假设我们正在构建一个用于抓取拍卖车辆图片的爬虫。

#1:收集文件链接

const get_links = async (url) => {

   const hrefs = []

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

   const page = await browser.newPage()

   await page.goto(url, { waitUntil: 'networkidle0' })

   const images = await page.$$('img')

   for (let i = 1; i<=images.length; i ++) {

       try {

          hrefs.push(await images[i].evaluate(img => img.src))

      } catch {}

   }

   await browser.close()

   return hrefs

}

相信此刻您已熟悉 Puppeteer 的语法。与上述脚本不同,我们现在要做的是遍历 `img` 元素,提取其源 URL,将其追加到数组中,并返回该数组。这个函数没有什么花哨之处。

#2:使用 axios 保存文件

const download_file = async (url, save) => {

   const writer = fs.createWriteStream(path.resolve(save))

   const response = await axios({

       url,

       method: 'GET',

       responseType: 'stream'

   })

   response.data.pipe(writer)

   return new Promise((resolve, reject) => {

       writer.on('finish', resolve)

       writer.on('error', reject)

   })

}

这个函数稍显复杂,因为它引入了两个新包:`fs` 和 `axios`。`fs` 包中的第一个方法不言自明,它创建了一个可写流。你可以在此处了解更多相关信息。

接下来,我们使用 axios 连接服务器的 URL,并告知 axios 响应类型为“流”。最后,由于我们正在处理流,我们将使用 `pipe()` 函数将响应写入流中。

完成上述配置后,剩下的就是将这两个函数组合成可运行的程序。只需添加以下几行代码即可:

let i = 1

const images = await get_links('https://www.iaai.com/Search?url=PYcXt9jdv4oni5BL61aYUXWpqGQOeAohPK3E0n6DCLs%3d')

images.forEach(async (img) => {

   await download_file(img, `./images/${i}.svg`)

   i += 1

})

结论

Puppeteer 的文档对文件下载的说明可能不够清晰。尽管如此,今天我们还是探索出了多种实现方法。但请注意,使用 Puppeteer 抓取文件并非易事。因为 Puppeteer 会启动一个无头浏览器,而这类浏览器通常会被迅速封禁。

如果您正在寻找一种隐蔽的程序化文件下载方式,不妨关注网络爬虫服务。在 Web Scraping API,我们投入了大量时间和精力来隐藏我们的指纹,以避免被检测到。这在我们的成功率上得到了切实体现。使用 Web Scraping API 下载文件就像发送一个 curl 请求一样简单,剩下的工作由我们来处理。

综上所述,我衷心希望今天的文章能对您的学习有所帮助。此外,希望我们已达成最初设定的双重目标。从现在起,您应该能够构建自己的工具,并使用 Puppeteer 下载文件。

关于作者
Mihnea-Octavian Manolache, 全栈开发工程师 @ WebScrapingAPI
Mihnea-Octavian Manolache全栈开发工程师

Mihnea-Octavian Manolache 是 WebScrapingAPI 的全栈及 DevOps 工程师,负责开发产品功能并维护确保平台平稳运行的基础设施。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。