在这篇文章中,我们将讨论如何用Puppeteer有效地下载文件。自动下载文件有时会很复杂。你也许需要明确指定一个下载位置,同时下载多个文件,等等。不幸的是,所有这些用例都没有很好的记录。
这就是为什么我写这篇文章,分享我多年来在使用Puppeteer工作时想出的一些技巧和窍门。我们将通过几个实际的例子,深入了解Puppeteer用于文件下载的API。让我们开始吧。
通过模拟点击按钮下载图像
在第一个例子中,我们将看一下一个简单的场景,即我们自动点击按钮下载一个图片。我们将在一个新的浏览器标签中打开一个URL。然后,我们将在该页面上找到下载按钮。最后,我们将点击下载按钮。听起来很简单吧?
我们可以使用下面的脚本来实现下载过程的自动化。
const puppeteer = require('puppeteer');
async function simplefileDownload() {
const browser = await puppeteer.launch({
headless: false
});
const page = await browser.newPage();
await page.goto(
'https://unsplash.com/photos/tn57JI3CewI',
{ waitUntil: 'networkidle2' }
);
await page.click('._2vsJm ')
}
simplefileDownload();
我把headless选项设置为假(在第4行)。这将使我们能够实时观察自动化。脚本本身是不言自明的。我们正在创建一个Puppeteer的新实例。然后我们用给定的URL打开一个新标签。最后,我们使用click()函数来模拟按钮的点击。让我们来运行这个脚本。它工作得相当好。然而,有一个小问题。图像是在操作系统的默认下载路径中下载的。
Chrome和我们操作系统的本地文件系统之间存在着紧密的耦合。各个操作系统的下载位置可能会有所不同。如果我们想让我们的脚本随后处理和删除文件,由于这种紧密耦合,就会变得更加困难。
我们可以通过在我们的脚本中明确指定路径来避免默认的下载路径。让我们更新我们的脚本来设置路径。
const puppeteer = require('puppeteer');
const path = require('path');
const downloadPath = path.resolve('./download');
async function simplefileDownload() {
const browser = await puppeteer.launch({
headless: false
});
const page = await browser.newPage();
await page.goto(
'https://unsplash.com/photos/tn57JI3CewI',
{ waitUntil: 'networkidle2' }
);
await page._client.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: downloadPath
});
await page.click('._2vsJm ')
}
simplefileDownload();
我们在第2行和第3行使用Node的本地路径来指定我们的下载路径。在第15行,我们使用Puppeteer的Page.setDownloadBehavior属性来绑定Chrome浏览器的路径。
复制下载请求
接下来,让我们看看如何通过HTTP请求来下载文件。我们不是模拟点击,而是要找到图像源。下面是它是如何完成的:
const fs = require('fs');
const https = require('https');
async function downloadWithLinks() {
const browser = await puppeteer.launch({
headless: false
});
const page = await browser.newPage();
await page.goto(
'https://unsplash.com/photos/tn57JI3CewI',
{ waitUntil: 'networkidle2' }
);
const imgUrl = await page.$eval('._2UpQX', img => img.src);
https.get(imgUrl, res => {
const stream = fs.createWriteStream('somepic.png');
res.pipe(stream);
stream.on('finish', () => {
stream.close();
})
})
browser.close()
}
这个脚本的第一部分与我们之前的例子几乎相同。在第13行,事情变得有趣了。我们在页面上直接找到图片的DOM节点,并获得其src属性。src属性是一个URL。我们向这个URL发出HTTPS GET请求,并使用Node的本地fs模块将该文件流写入我们的本地文件系统。 只有当我们想要下载的文件在DOM中暴露出src时,才可以使用这个方法。
你为什么要使用这种方法呢?
为什么使用这种方法而不是像前面的例子中所看到的那样模拟按钮点击。嗯,首先,这种方法可以更快。我们可以同时下载多个文件。 假设一个页面有几个图片,我们想下载所有的图片。点击这些图片中的一张,会把用户带到一个新的页面,从那里,用户可以下载该图片。要下载下一张图片,用户必须回到前一页。这似乎很乏味。我们可以写一个模仿这种行为的脚本,但如果第一页在DOM中暴露了图片的URL,我们就不需要这样做。
让我们深入了解这种情况的一个例子。假设我们想从这个网站爬取一些图片。观察DOM,我们可以看到这些图片有src属性。
下面的脚本将收集所有的图像源并下载它们。
const puppeteer = require('puppeteer');
const fs = require('fs');
const https = require('https');
async function downloadMultiple() {
const browser = await puppeteer.launch({
headless: false
});
const page = await browser.newPage();
await page.goto(
'https://unsplash.com/t/wallpapers',
{ waitUntil: 'networkidle2' }
);
const imgUrls = await page.$$eval('._2UpQX', imgElms => {
const urls = [];
imgElms.forEach(elm => {
urls.push(elm.src);
})
return urls;
});
imgUrls.forEach((url , index) => {
https.get(url, res => {
const stream = fs.createWriteStream(`download-${index}.png`);
res.pipe(stream);
stream.on('finish', () => {
stream.close();
})
})
});
browser.close()
}
downloadMultiple();
并行下载多个文件
在接下来的部分,我们将深入探讨一些高级概念。我们将讨论并行下载。下载小文件很容易。然而,如果你必须下载多个大文件,事情就会变得复杂。你看Node.js的核心是一个单线程的系统。Node有一个单一的事件循环。它一次只能执行一个进程。因此,如果我们要下载10个文件,每个文件大小为1千兆字节,每个文件需要3分钟下载,那么使用一个进程,我们将不得不等待10 x 3 = 30分钟来完成任务。这一点都不符合性能要求。
那么,我们如何解决这个问题呢? 答案是并行进程。我们的CPU核心可以同时运行多个进程。我们可以在Node中叉取多个child_proces。子进程是Node.js处理并行编程的方式。我们可以将子进程模块与我们的Puppeteer脚本结合起来,并行地下载文件。
下面的代码片段是用Puppeteer运行并行下载的一个简单例子:
// main.js
const fork = require('child_process').fork;
const ls = fork("./child.js");
ls.on('exit', (code)=>{
console.log(`child_process exited with code ${code}`);
});
ls.on('message', (msg) => {
ls.send('https://unsplash.com/photos/AMiglZWQSQQ');
ls.send('https://unsplash.com/photos/TbEqd-GNC5w');
ls.send('https://unsplash.com/photos/FiVujM6egyU');
ls.send('https://unsplash.com/photos/yGBJB6lHYVw');
});
在这个解决方案中,我们有两个文件,第一个是main.js。在这个文件中,我们启动我们的子进程并发送我们想要下载的图片的URL。对于每个URL,将启动一个新的子线程。
// child.js
const puppeteer = require('puppeteer');
const path = require('path');
const downloadPath = path.resolve('./download');
process.on('message', async (url)=> {
console.log("CHILD: url received from parent process", url);
await download(url)
});
process.send('Executed');
async function download(url) {
const browser = await puppeteer.launch({
headless: false
});
const page = await browser.newPage();
await page.goto(
url,
{ waitUntil: 'networkidle2' }
);
// Download logic goes here
await page._client.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: downloadPath
});
await page.click('._2vsJm ')
//
}
[文中代码源自Scrapingbee]
在child.js文件中,主要的下载功能被实现。请注意,我们这里的下载功能与我们先前的解决方案是相同的。唯一的区别是Process.on函数。这个函数从主进程接收一个消息(在我们的例子中是一个链接),并在Node的主事件循环之外启动子进程。我们的主脚本同时产生了4个chrome实例,并平行地启动了文件下载。
最后的思考
我希望上面分享的解释和代码实例能让你更好地了解Puppeteer中的文件下载工作。你可以用Puppeteer下载文件,有很多方法。一旦你对Puppeteer的API以及它在Node.js生态系统中的配合有了扎实的了解,你就可以想出最适合你的定制解决方案。


