最近发现了一个AI图片的网站,然后自己就下载了一些。碰巧这几天学习Puppeteer
的知识,准备利用Puppeteer
进行图片的爬取。
代码是根据浏览器插件-Headless Recorder生成的Puppeteer脚步进行修改的。
基础知识可参考
:
浏览器插件-Headless Recorder
Headless Recorder
允许您记录与网站的交互并自动生成Puppeteer或Playwright脚本。它会捕捉您在浏览器中的操作,并将其转换为可用于自动化测试、网页抓取或其他任务的代码。
插件地址:Headless Recorder
使用Headless Recorder的步骤如下:
- 从Chrome网络商店安装Headless Recorder Chrome扩展程序。
- 在Chrome中打开您想要与之交互的网站。
- 单击Chrome工具栏中的Headless Recorder扩展程序图标以打开录制器。
- 在网站上执行您的交互操作,比如点击按钮、填写表单、在页面之间导航等。
- 单击Headless Recorder扩展程序中的"停止"按钮以停止录制。
- 录制的交互操作将被转换为Puppeteer或Playwright代码,您可以将其复制粘贴到您自己的脚本中。
Headless Recorder是一个非常有用的工具,可以快速创建自动化脚本,而无需手动编写所有代码。它可以在自动化重复任务或测试Web应用程序时节省您的时间和精力。
模拟图片下载
图片网站:天极图片
选择任意一张图片点进去,然后点击图片,点击下载图片按钮。
利用浏览器插件生成脚步:
js
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch()
const page = await browser.newPage()
const navigationPromise = page.waitForNavigation()
await page.goto('https://pic.yesky.com/')
await page.setViewport({ width: 1366, height: 607})
await page.waitForSelector('.picList1 > .layout > li:nth-child(3) > .picBox > img')
await page.click('.picList1 > .layout > li:nth-child(3) > .picBox > img')
await navigationPromise
await page.waitForSelector('#atlas > .layout > .atlasSwiper > .bigPic > img')
await page.click('#atlas > .layout > .atlasSwiper > .bigPic > img')
await page.waitForSelector('body > .bigPic_fc > .swiper > .btns > .download')
await page.click('body > .bigPic_fc > .swiper > .btns > .download')
await browser.close()
这里我们先看如何下载图片里的一系列图片。
从上述代码,我们可以看到,下载图片的流程为:
- 点击图片预览
- 预览界面有下载按钮,点击下载按钮,图片即可下载。
- 点击下一个,显示下一张图片,再次点击按钮即可。
- 重复第三步,直到下载全部即可。
从上述流程中,我们需要解决一个问题:一共需求点击多少次下一页按钮,即这一页面总共包含多少张图片
。
查看页面,我们可以看到:
这样我们只需获取这个元素对应的值即可:
js
let total = await page1.$eval(
".atlas_information ul li span i",
(el) => {
let text = el.innerHTML;
return text.split("张")[0];
}
);
这样我们就可以获取该页面总的图片数量。
那最后代码如下:
js
const puppeteer = require("puppeteer");
// 谷歌浏览器地址
const CHROME_EXECUTALBE_PATH =
"C:/Program Files/Google/Chrome/Application/chrome.exe";
const downloadImage = async (page, total) => {
for (let i = 0; i < total; i++) {
// 点击下载按钮
await page.waitForSelector(
"body > .bigPic_fc > .swiper > .btns > .download"
);
await page.click("body > .bigPic_fc > .swiper > .btns > .download");
// 点击下一页按钮
await page.waitForSelector(
"body > .bigPic_fc > .swiper > .swiper-button-next"
);
await page.click("body > .bigPic_fc > .swiper > .swiper-button-next");
}
};
(async () => {
const browser = await puppeteer.launch({
headless: false,
defaultViewport: {
width: 1366,
height: 607,
},
args: ["--start-maximized"],
executablePath: CHROME_EXECUTALBE_PATH, // 这个路径写上就可以执行了
});
const page = await browser.newPage();
await page.goto("https://pic.yesky.com/307/2147478807.shtml");
// 获取页面图片数量
let total = await page.$eval(
".atlas_information > ul > li:nth-child(5) > span > i",
(el) => {
let text = el.innerHTML;
return text.split("张")[0];
}
);
// 点击图片预览
await page.waitForSelector("#atlas > .layout > .atlasSwiper > .bigPic > img");
await page.click("#atlas > .layout > .atlasSwiper > .bigPic > img");
// 图片下载
downloadImage(page, total);
await browser.close();
})();
下载第一页的所有图片
上一章节,我们讲述了如何下载某一个图片的系列图片。这一章节主要讲述如何下载第一页的所有图片对应的图片系列。
这里我们可以获取第一页所有图片对应的页面链接:
js
const linkArr = await page.$$eval(
".list .classification_listContent .picName",
(elements) => elements.map((element) => element.href)
);
然后我们依次遍历即可:
js
const puppeteer = require("puppeteer");
const CHROME_EXECUTALBE_PATH =
"C:/Program Files/Google/Chrome/Application/chrome.exe";
const downloadImage = async (page, total) => {
for (let i = 0; i < total; i++) {
await page.waitForSelector(".bigPic_fc .swiper .btns .download");
await page.click(".bigPic_fc .swiper .btns .download");
await page.waitForSelector(".bigPic_fc .swiper .swiper-button-next");
await page.click(".bigPic_fc .swiper .swiper-button-next");
console.log("正在下载:", i + 1, "/", total);
// 添加500ms的等待时间
await new Promise((resolve) => setInterval(resolve, 500));
}
};
(async () => {
const browser = await puppeteer.launch({
headless: false,
defaultViewport: {
width: 1366,
height: 607,
},
args: ["--start-maximized"],
executablePath: CHROME_EXECUTALBE_PATH, // 这个路径写上就可以执行了
});
const page = await browser.newPage();
await page.goto("https://pic.yesky.com/c/6_25152.shtml");
const linkArr = await page.$$eval(
".list .classification_listContent .picName",
(elements) => elements.map((element) => element.href)
);
// 避免打开多个浏览器窗口
let page1 = await browser.newPage();
for (let link of linkArr) {
await page1.goto(link);
// 获取页面图片数量
let total = await page1.$eval(
".atlas_information > ul > li:nth-child(5) > span > i",
(el) => {
let text = el.innerHTML;
return text.split("张")[0];
}
);
await page1.waitForSelector("#atlas .layout .atlasSwiper .bigPic img");
await page1.click("#atlas .layout .atlasSwiper .bigPic img");
await downloadImage(page1, total);
}
await browser.close();
})();
执行上述代码,即可下载第一页的所有图片。
下载某一系列的所有图片
上一章节我们讲述了如何下载第一页的所有图片,这一章节我们讲述如何下载某一系列的所有图片。
由于图片总页码不多,点击对应系列即可查询到该系列的总页数,然后我们依次遍历即可。
js
const puppeteer = require("puppeteer");
const fs = require("fs");
const request = require("request");
const path = require("path");
const CHROME_EXECUTALBE_PATH =
"C:/Program Files/Google/Chrome/Application/chrome.exe";
const downloadImage = async (page, total) => {
for (let i = 0; i < total; i++) {
// 点击下载按钮
await page.waitForSelector(
"body > .bigPic_fc > .swiper > .btns > .download"
);
await page.click("body > .bigPic_fc > .swiper > .btns > .download");
// 点击下一页按钮
await page.waitForSelector(
"body > .bigPic_fc > .swiper > .swiper-button-next"
);
await page.click("body > .bigPic_fc > .swiper > .swiper-button-next");
console.log("正在下载:", i + 1, "/", total);
// 添加500ms的等待时间
await new Promise((resolve) => setInterval(resolve, 500));
}
};
(async () => {
const browser = await puppeteer.launch({
headless: false,
defaultViewport: {
width: 1366,
height: 607,
},
args: ["--start-maximized"],
executablePath: CHROME_EXECUTALBE_PATH, // 这个路径写上就可以执行了
});
const page = await browser.newPage();
for (let pageIndex = 1; pageIndex < 11; pageIndex++) {
await page.goto(`https://pic.yesky.com/c/6_25152_${pageIndex}.shtml`);
const linkArr = await page.$$eval(
".list .classification_listContent .picName",
(elements) => elements.map((element) => element.href)
);
let page1 = await browser.newPage();
for (let link of linkArr) {
await page1.goto(link);
let total = await page1.$eval(".atlas_information ul li span i", (el) => {
let text = el.innerHTML;
return text.split("张")[0];
});
await page1.waitForSelector("#atlas .layout .atlasSwiper .bigPic img");
await page1.click("#atlas .layout .atlasSwiper .bigPic img");
await downloadImage(page1, total);
}
}
await browser.close();
})();
其他系列也是同样的道理。
自定义文件名和存储路径
上面的代码是可以爬取所有的图片的,但是存在几个问题:
- 点击下载按钮,文件名是英文的拼接,并且同一系列是没有任何关联的。
- 文件存储位置是默认浏览器下载位置,无法自定义存储位置,这就导致所有的图片均下载在同一路径,一个文件夹包含近千张的图片。
而我们是想将同一系列的图片下载在同一个文件夹,文件夹命名为该系列的名称,并且图片的名称是系列名称+对应序号。
前面的代码我们是使用的是网站自带的下载功能。如果要实现上述的功能,但是如果使用网站自带的下载功能,是无法自定义文件名称的。
这里我们可以获取图片的对应地址,然后利用网络请求实现即可:
js
const download = (url, filename, customPath) => {
return new Promise((resolve, reject) => {
const filePath = customPath ? `${customPath}/${filename}` : filename;
request.head(url, function (err, res, body) {
request(url)
.pipe(fs.createWriteStream(filePath))
.on("close", () => resolve(filePath))
.on("error", reject);
});
});
};
然后我们要获取图片系列的名称,用作文件夹名称和文件名称:
js
let title = await page1.$eval(
"body > #atlas > .layout > h1",
(elements) => elements.innerText
);
但是在下载的时候,出现了一个问题:文件夹名称里面包含不允许的特殊符号,会导致图片下载中断。
这里我们就要做一下文件夹名称的特殊字段的过滤:
js
// 文件夹名称的特殊字段过滤
const sanitizeFilename = (filename) => {
return filename.replace(/[!@#$%^&*()+=\-[\]\\';,./{}|":<>?~_]/g, "_");
};
let rawTitle = await page1.$eval(
"body > #atlas > .layout > h1",
(elements) => elements.innerText
);
title = sanitizeFilename(rawTitle);
然后就剩最后一个功能,就是文件夹的建立:
js
// 使用路径拼接创建目录
const directoryPath = path.join(__dirname, title);
fs.mkdir(directoryPath, () => {});
最后的完整代码如下:
js
const puppeteer = require("puppeteer");
const fs = require("fs");
const request = require("request");
const path = require("path");
const CHROME_EXECUTALBE_PATH =
"C:/Program Files/Google/Chrome/Application/chrome.exe";
const download = (url, filename, customPath) => {
return new Promise((resolve, reject) => {
const filePath = customPath ? `${customPath}/${filename}` : filename;
request.head(url, function (err, res, body) {
request(url)
.pipe(fs.createWriteStream(filePath))
.on("close", () => resolve(filePath))
.on("error", reject);
});
});
};
const sanitizeFilename = (filename) => {
return filename.replace(/[!@#$%^&*()+=\-[\]\\';,./{}|":<>?~_]/g, "_");
};
(async () => {
const browser = await puppeteer.launch({
headless: false,
defaultViewport: {
width: 1960,
height: 1280,
},
args: ["--start-maximized"],
executablePath: CHROME_EXECUTALBE_PATH, // 这个路径写上就可以执行了
});
const page = await browser.newPage();
for (let pageIndex = 1; pageIndex < 11; pageIndex++) {
await page.goto(`https://pic.yesky.com/c/6_25152_${pageIndex}.shtml`);
const linkArr = await page.$$eval(
".list .classification_listContent .picName",
(elements) => elements.map((element) => element.href)
);
let page1 = await browser.newPage();
for (let link of linkArr) {
await page1.goto(link);
let total = await page1.$eval(".atlas_information ul li span i", (el) => {
let text = el.innerHTML;
return text.split("张")[0];
});
let rawTitle = await page1.$eval(
"body > #atlas > .layout > h1",
(elements) => elements.innerText
);
title = sanitizeFilename(rawTitle);
console.log(title);
await page1.waitForSelector("#atlas .layout .atlasSwiper .bigPic img");
await page1.click("#atlas .layout .atlasSwiper .bigPic img");
const imageArr = await page1.$$eval(
"body > .bigPic_fc > .swiper > ul li img",
(elements) =>
elements.map((element) => {
return element.src || element?.getAttribute("data-src");
})
);
// 使用路径拼接创建目录
const directoryPath = path.join(__dirname, title);
fs.mkdir(directoryPath, () => {});
let index = 1;
for (let url of imageArr) {
download(url, `${title}-${index}.png`, title);
index += 1;
}
// await downloadImage(page1, total);
await new Promise((resolve) =>
setTimeout(resolve, Math.min(2000, 1000 * total))
);
}
}
await browser.close();
})();
结语
上述代码实现的时候,可以将headless: false,字段注释掉,就不用打开浏览器了。
其实puppeteer
还有其他的很多功能,只是暂时没用到具体需求,或者有些需求是不方便公开的。这篇文章大家做做参考即可。