批量下载豆瓣电影海报图片原图,喜欢电影海报的有福了。

1,091 阅读5分钟

因为本人比较喜欢收藏海报,最近想在网上下载一些海报的原图,拿去打印出来。海报的主要获取地方是豆瓣电影,但想要下载多张海报原图,就要一张图一张图的点击,而且页面跳转的层级也比较多,很麻烦,而且我发现小图和原图的图片id是一样的,只是资源地址不一样,也就是在电影介绍页面获取图片的id,就可以通过字符串拼接的方式生成原图的资源地址,所以想着自己写个脚本,在电影介绍页面就能批量获取图片的id,满足下载多张海报原图的需求。

image.png

image.png

image.png

github项目地址:github.com/lffrom0303/…

gitee项目地址:gitee.com/lffrom0303/…

需求分析

  1. 实现页面多选图片功能
  2. 全选/取消全选功能
  3. 下载勾选图片的原图到本地

实现页面多选图片功能

实现这个功能,也就是在页面所有的图片附近,加一个选择框,并且能够每次进入豆瓣电影网页以及子网页,都能多选图片,这样选择起来就比较方便快捷,于是就想到了用chrome的插件实现。

初始化编写chrome插件

自定义开发chrome插件的教程

按教程新建文件后基本骨架大概长这样

image.png

简单解释下各个文件的用途

manifest.json:这是插件的配置文件,定义了插件的元数据和行为。

background.js:用于定义插件的后台逻辑,持续运行并响应浏览器事件。

content.js:注入并在网页的上下文中运行,可以访问并操作网页的 DOM。

popup.html:定义浏览器工具栏图标的弹出界面(点击插件图标时显示的 UI)。

popup.js:处理 popup.html 的逻辑。

icon.png:插件的图标。

加载插件

image.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);

image.png

图片选择框

豆瓣的图片资源都是放在相同的文件目录结构下的,可以直接通过匹配/view/photo/拿到图片元素 image.png

image.png

// 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);

image.png

但是发现仅仅是一部分的图片添加上了选择框,下面的一些图片并没有添加上选择框。

image.png

思考片刻,查看了下后台,发现一部分图片并不是页面一进来就获取的,而是动态获取的,而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,
});

image.png

image.png

image.png

都设置上了,包括子页面。

全选/取消全选按钮实现逻辑

这个较为简单,就是拿到所有的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数组 image.png

image.png

使用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;

回到豆瓣电影页面,随便选择多张图片

image.png

image.png

image.png

image.png

image.png

image.png

后记

一下午的成果,很多没有完善的,希望各位大佬能指正指正~~~~

觉得项目不错的话,动动小指头点点star💗💗💗~~~~