因为本人比较喜欢收藏海报,最近想在网上下载一些海报的原图,拿去打印出来。海报的主要获取地方是豆瓣电影,但想要下载多张海报原图,就要一张图一张图的点击,而且页面跳转的层级也比较多,很麻烦,而且我发现小图和原图的图片id是一样的,只是资源地址不一样,也就是在电影介绍页面获取图片的id,就可以通过字符串拼接的方式生成原图的资源地址,所以想着自己写个脚本,在电影介绍页面就能批量获取图片的id,满足下载多张海报原图的需求。
github项目地址:github.com/lffrom0303/…
gitee项目地址:gitee.com/lffrom0303/…
需求分析
- 实现页面多选图片功能
- 全选/取消全选功能
- 下载勾选图片的原图到本地
实现页面多选图片功能
实现这个功能,也就是在页面所有的图片附近,加一个选择框,并且能够每次进入豆瓣电影网页以及子网页,都能多选图片,这样选择起来就比较方便快捷,于是就想到了用chrome的插件实现。
初始化编写chrome插件
按教程新建文件后基本骨架大概长这样
简单解释下各个文件的用途
manifest.json:这是插件的配置文件,定义了插件的元数据和行为。
background.js:用于定义插件的后台逻辑,持续运行并响应浏览器事件。
content.js:注入并在网页的上下文中运行,可以访问并操作网页的 DOM。
popup.html:定义浏览器工具栏图标的弹出界面(点击插件图标时显示的 UI)。
popup.js:处理 popup.html 的逻辑。
icon.png:插件的图标。
加载插件
全选/取消全选按钮
// content.js
// 创建按钮容器
const container = document.createElement("div");
container.style.position = "fixed";
container.style.top = "10px";
container.style.right = "10px";
container.style.zIndex = "1000";
container.style.backgroundColor = "white";
container.style.border = "1px solid #ccc";
container.style.padding = "10px";
container.style.display = "flex";
container.style.flexDirection = "column";
container.style.gap = "10px";
// 创建下载按钮
const downloadButton = document.createElement("button");
downloadButton.textContent = "下载选中图片";
container.appendChild(downloadButton);
// 创建全选按钮
const selectAllButton = document.createElement("button");
selectAllButton.textContent = "全选/取消全选";
container.appendChild(selectAllButton);
document.body.appendChild(container);
图片选择框
豆瓣的图片资源都是放在相同的文件目录结构下的,可以直接通过匹配/view/photo/拿到图片元素
// content.js
const handleImages = () => {
return Array.from(document.querySelectorAll("img")).filter((img) =>
img.src.includes("/view/photo/")
);
};
// 获取所有图片并显示复选框
let images = handleImages();
const checkboxes = [];
// 函数来为新获取的图片添加复选框
function addCheckboxToImages(images) {
images.forEach((img, index) => {
// 创建复选框并插入到图片的父元素中
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.style.position = "absolute";
checkbox.style.margin = "5px"; // 调整边距以更好地显示
checkbox.style.left = "0px";
checkbox.style.top = "0px";
// 设置图片父元素的样式以便定位
img.style.position = "relative";
img.parentElement.style.position = "relative";
img.parentElement.style.display = "inline-block";
// 将复选框插入图片的父元素,使其显示在图片上
img.parentElement.appendChild(checkbox);
// 记录复选框引用
checkboxes.push(checkbox);
});
}
// 初次加载时为页面上已有的图片添加复选框
addCheckboxToImages(images);
但是发现仅仅是一部分的图片添加上了选择框,下面的一些图片并没有添加上选择框。
思考片刻,查看了下后台,发现一部分图片并不是页面一进来就获取的,而是动态获取的,而content.js的代码是一进来就执行,所以并不能第一时间获取到动态添加的图片元素,想到用setTimeout延后获取文档元素,但觉得动态元素的话,可能不仅仅是动态获取一次,会有多次,于是想到使用MutationObserver监听的方式,动态获取最新的元素,重新执行给元素添加选择框,并且给已经添加过选择框的元素添加属性hasCheckbox=true,标记已添加过选择框,代码如下:
// content.js
const handleImages = () => {
return Array.from(document.querySelectorAll("img")).filter((img) =>
img.src.includes("/view/photo/")
);
};
// 获取所有图片并显示复选框
let images = handleImages();
const checkboxes = [];
// 函数来为新获取的图片添加复选框
function addCheckboxToImages(images) {
images.forEach((img, index) => {
//++++++++++++++++++++++++++++++
if (!img.dataset.hasCheckbox) {
// 创建复选框并插入到图片的父元素中
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.style.position = "absolute";
checkbox.style.margin = "5px"; // 调整边距以更好地显示
checkbox.style.left = "0px";
checkbox.style.top = "0px";
// 设置图片父元素的样式以便定位
img.style.position = "relative";
img.parentElement.style.position = "relative";
img.parentElement.style.display = "inline-block";
// 将复选框插入图片的父元素,使其显示在图片上
img.parentElement.appendChild(checkbox);
//++++++++++++++++++++++++++++++
img.dataset.hasCheckbox = "true"; // 标记已处理
// 记录复选框引用
checkboxes.push(checkbox);
}
});
}
// 初次加载时为页面上已有的图片添加复选框
addCheckboxToImages(images);
使用 MutationObserver 监听新图片的加载
// 使用 MutationObserver 监听新图片的加载
const observer = new MutationObserver((mutationsList) => {
let newImagesDetected = false;
mutationsList.forEach((mutation) => {
if (mutation.type === "childList") {
mutation.addedNodes.forEach((node) => {
if (node.tagName === "IMG" && node.src.includes("/view/photo/")) {
newImagesDetected = true;
} else if (node.querySelectorAll) {
const newImages = node.querySelectorAll("img");
if (newImages.length > 0) {
newImagesDetected = true;
}
}
});
}
});
// 如果检测到新图片,更新 images 并添加复选框
if (newImagesDetected) {
images = handleImages();
addCheckboxToImages(images);
}
});
// 配置观察器来监听整个文档
observer.observe(document.body, {
childList: true,
subtree: true,
});
都设置上了,包括子页面。
全选/取消全选按钮实现逻辑
这个较为简单,就是拿到所有的checkbox,统一设置checkd属性为true/false
// 全选/取消全选的逻辑
selectAllButton.addEventListener("click", () => {
const allSelected = checkboxes.every((checkbox) => checkbox.checked);
checkboxes.forEach((checkbox) => {
checkbox.checked = !allSelected; // 全选或取消全选
});
});
点击下载勾选图片按钮实现逻辑
// 下载选中图片的逻辑
downloadButton.addEventListener("click", async () => {
const imageIdsList = [];
checkboxes.forEach((checkbox, i) => {
if (checkbox.checked) {
const imgSrc = images[i].src;
const imageId = imgSrc.split("/").pop().split(".")[0];
const imgPrefix = imgSrc.split(".")[0].split("//")[1];
console.log(imgSrc, imgPrefix, imageId);
imageIdsList.push({
imgPrefix,
imageId,
});
}
});
const serverUrl = "http://localhost:3000";
if (imageIdsList.length) {
await fetch(`${serverUrl}/movie/saveImageList`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
imageIdsList: imageIdsList,
}),
});
}
});
调试content.js脚本获取到选择的图片id数组
使用express请求图片原图并写入本地
npm install express --save
npm install cors --save-dev
node index.js
接口逻辑
// index.js
const express = require("express");
const cors = require("cors");
const fetchImages = require("./utils/photo");
const app = express();
const port = 3000;
app.use(cors());
// 使用 express.json() 中间件来解析 JSON 格式的请求体
app.use(express.json());
app.post("/movie/saveImageList", (req, res) => {
// 访问请求体中的参数
const { imageIdsList } = req.body;
// 获取图片数据
fetchImages(imageIdsList);
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
获取图片并且下载的方法
// photo.js
const axios = require("axios").default;
const fs = require("fs");
const path = require("path");
// 桌面路径
const desktopPath = path.join(
require("os").homedir(),
"Desktop",
"douban_images"
);
// 检查并创建桌面文件夹
if (!fs.existsSync(desktopPath)) {
fs.mkdirSync(desktopPath);
}
// 下载图片函数
const downloadImage = async (url, imagePath) => {
try {
// 检查文件是否已存在
if (fs.existsSync(imagePath)) {
// console.log(`文件 ${path.basename(imagePath)} 已存在,覆盖文件...`);
return;
}
const response = await axios({
url,
method: "GET",
responseType: "stream",
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
Referer: "https://www.douban.com/",
"Accept-Language": "en-US,en;q=0.9",
},
});
return new Promise((resolve, reject) => {
const writer = fs.createWriteStream(imagePath);
response.data.pipe(writer);
writer.on("finish", resolve);
writer.on("error", reject);
});
} catch (e) {
throw e;
}
};
// 主逻辑
const fetchImages = async (imageIds) => {
if (imageIds.length) {
for (const { imageId, imgPrefix } of imageIds) {
let imageDownloaded = false;
const url = `https://${imgPrefix}.doubanio.com/view/photo/raw/public/${imageId}.jpg`;
const imagePath = path.join(desktopPath, `${imageId}.jpg`);
try {
await downloadImage(url, imagePath);
console.log(`图片 ${url} 下载成功`);
imageDownloaded = true;
} catch (e) {}
if (!imageDownloaded) {
console.log(`图片 ${url} 下载失败`);
}
}
console.log("图片下载任务完成!");
}
};
module.exports = fetchImages;
回到豆瓣电影页面,随便选择多张图片
后记
一下午的成果,很多没有完善的,希望各位大佬能指正指正~~~~
觉得项目不错的话,动动小指头点点star💗💗💗~~~~