返回博客
指南
米赫内亚-奥克塔维安·马诺拉切2023年4月25日阅读时长:8分钟

如何使用 Puppeteer 制作刮刀并下载文件

如何使用 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提供的页面域。

此外,网络爬虫还有第三种技术。它引入了一个新的组件:HTTP 客户端。通过这种方式,网络爬虫先收集文件的 `hrefs`,然后使用 HTTP 客户端下载这些文件。每种方法都有其特定的应用场景,因此我们将探讨这两种方法。

只需点击一下按钮,即可在 Puppeteer 中下载文件

如果幸运的话,您想要抓取文件的网站使用了按钮,那么您只需在 Puppeteer 中模拟点击事件即可。在此情况下,文件下载器的实现非常简单。Puppeteer 甚至对 Page.click() 方法进行了文档说明,您可以在此处找到更多信息。

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

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

我们的脚本只需实现四个简单的步骤。不过,在开始编写代码之前,我想先告诉大家:就像您日常使用的浏览器一样,由 Puppeteer 控制的 Chrome 实例会将下载的文件保存在默认的下载文件夹中,该文件夹位于:

  • \Users\<username>\Downloads for Windows
  • /Users/<username>/Downloads for Mac
  • /home/<username>/Downloads for Linux

基于此,让我们开始编写代码吧。假设我们是天体物理学家,需要从美国宇航局(NASA)收集一些数据,以便稍后进行处理。目前,我们先专注于下载这些 .doc 文件。

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

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

NASA文档列表页面,浏览器开发者工具中突出显示了一个下载链接元素

#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,并访问目标网站
  • 选中所有包含我们稍后要点击的 `href` 的 `tr` 元素
  • 遍历 tr 元素,并 a. 检查元素内的文本是否包含单词“doc” b. 如果包含,则构建选择器并点击该元素
  • 关闭浏览器

就这样。使用 Puppeteer 下载文件再简单不过了。

使用 CDP 在 Puppeteer 中下载文件

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

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

第二个方面是使用 CDP 在浏览器中设置路径。正如我之前所说,我们将使用页面域的 `.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,并假设我们要构建一个用于抓取拍卖车辆图片的抓取工具。

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` 元素,提取其源网址,将其追加到一个数组中,然后返回该数组。这个函数没什么特别之处。

#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 是 WebScrapingAPI 的全栈及 DevOps 工程师,负责开发产品功能并维护确保平台平稳运行的基础设施。

开始构建

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

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