为何要使用 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 下载文件。




