lanzoum.com/igBA53zz1eze
代码:
(async function () {
const MAX_IMAGES = 999;
const ZIP_NAME = "rich_pages_images.zip";
const REMOVE_DUPLICATES = false;
// 兼容:
// <img class="rich_pages wxw-img">
// .rich_pages 内部的 .wxw-img
// <wxw-img> 自定义标签
const elements = [
...document.querySelectorAll(
"img.rich_pages.wxw-img, .rich_pages.wxw-img, .rich_pages .wxw-img, wxw-img"
)
];
function getImageUrl(element) {
const innerImage =
element.tagName === "IMG"
? element
: element.querySelector("img");
const targets = [element, innerImage].filter(Boolean);
for (const target of targets) {
const sources = [
target.getAttribute("data-src"),
target.getAttribute("data-original"),
target.getAttribute("original-src"),
target.getAttribute("data-backsrc"),
target.getAttribute("src"),
target.currentSrc,
target.src
];
for (const source of sources) {
if (!source) continue;
const value = source.trim();
// 跳过透明占位图
if (
value.startsWith("data:image/gif") &&
value.length < 500
) {
continue;
}
try {
const parsedUrl = new URL(value, location.href);
// 删除 #imgIndex=24 等锚点
parsedUrl.hash = "";
return parsedUrl.href;
} catch {
return value.split("#")[0];
}
}
}
return "";
}
let imageUrls = elements
.map(getImageUrl)
.filter(Boolean);
if (REMOVE_DUPLICATES) {
imageUrls = [...new Set(imageUrls)];
}
imageUrls = imageUrls.slice(0, MAX_IMAGES);
if (!imageUrls.length) {
console.error("没有找到符合条件的图片。");
alert("没有找到符合条件的图片。");
return;
}
const progressBox = document.createElement("div");
Object.assign(progressBox.style, {
position: "fixed",
right: "20px",
bottom: "20px",
zIndex: "2147483647",
padding: "12px 18px",
background: "rgba(0,0,0,.85)",
color: "#fff",
borderRadius: "8px",
fontSize: "14px",
lineHeight: "1.6",
fontFamily: "Arial, sans-serif",
maxWidth: "380px",
whiteSpace: "pre-wrap",
wordBreak: "break-all",
boxShadow: "0 4px 20px rgba(0,0,0,.3)"
});
document.body.appendChild(progressBox);
function setProgress(text) {
progressBox.textContent = text;
console.log(text);
}
function getExtension(url, contentType) {
const mimeMap = {
"image/jpeg": "jpg",
"image/jpg": "jpg",
"image/png": "png",
"image/webp": "webp",
"image/gif": "gif",
"image/bmp": "bmp",
"image/svg+xml": "svg",
"image/avif": "avif",
"image/tiff": "tif"
};
const mime = String(contentType || "")
.split(";")[0]
.trim()
.toLowerCase();
if (mimeMap[mime]) {
return mimeMap[mime];
}
try {
const pathname = new URL(url).pathname;
const match = pathname.match(
/\.([a-zA-Z0-9]+)$/
);
if (match) {
let extension = match[1].toLowerCase();
if (extension === "jpeg") extension = "jpg";
if (extension === "tiff") extension = "tif";
if (
[
"jpg",
"png",
"webp",
"gif",
"bmp",
"svg",
"avif",
"tif"
].includes(extension)
) {
return extension;
}
}
} catch {}
return "jpg";
}
// ZIP 所需 CRC32
const crcTable = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let value = i;
for (let j = 0; j < 8; j++) {
value =
value & 1
? 0xedb88320 ^ (value >>> 1)
: value >>> 1;
}
crcTable[i] = value >>> 0;
}
function crc32(bytes) {
let crc = 0xffffffff;
for (const byte of bytes) {
crc =
crcTable[(crc ^ byte) & 0xff] ^
(crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0;
}
function uint16(value) {
return new Uint8Array([
value & 0xff,
(value >>> 8) & 0xff
]);
}
function uint32(value) {
return new Uint8Array([
value & 0xff,
(value >>> 8) & 0xff,
(value >>> 16) & 0xff,
(value >>> 24) & 0xff
]);
}
function joinArrays(arrays) {
const totalLength = arrays.reduce(
(total, array) => total + array.length,
0
);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const array of arrays) {
result.set(array, offset);
offset += array.length;
}
return result;
}
function getDosDateTime(date = new Date()) {
const year = Math.max(1980, date.getFullYear());
const dosTime =
(date.getHours() << 11) |
(date.getMinutes() << 5) |
Math.floor(date.getSeconds() / 2);
const dosDate =
((year - 1980) << 9) |
((date.getMonth() + 1) << 5) |
date.getDate();
return {
time: dosTime,
date: dosDate
};
}
async function compressData(originalBytes) {
// 浏览器支持时进行 ZIP Deflate 压缩
if ("CompressionStream" in window) {
try {
const sourceStream =
new Blob([originalBytes]).stream();
const compressionStream =
new CompressionStream("deflate-raw");
const compressedStream =
sourceStream.pipeThrough(compressionStream);
const compressedBytes = new Uint8Array(
await new Response(
compressedStream
).arrayBuffer()
);
// 压缩后更小时才采用
if (
compressedBytes.length <
originalBytes.length
) {
return {
method: 8,
bytes: compressedBytes
};
}
} catch (error) {
console.warn(
"浏览器不支持 deflate-raw,将使用原始方式打包:",
error
);
}
}
return {
method: 0,
bytes: originalBytes
};
}
async function createZip(files) {
const localParts = [];
const centralParts = [];
let localOffset = 0;
let centralSize = 0;
for (
let index = 0;
index < files.length;
index++
) {
const file = files[index];
const fileNameBytes =
new TextEncoder().encode(file.name);
const originalBytes = file.bytes;
const checksum = crc32(originalBytes);
const compressed =
await compressData(originalBytes);
const dos = getDosDateTime();
setProgress(
`正在压缩 ${index + 1}/${files.length}\n${file.name}`
);
const localHeader = joinArrays([
uint32(0x04034b50),
uint16(20),
uint16(0x0800),
uint16(compressed.method),
uint16(dos.time),
uint16(dos.date),
uint32(checksum),
uint32(compressed.bytes.length),
uint32(originalBytes.length),
uint16(fileNameBytes.length),
uint16(0),
fileNameBytes
]);
localParts.push(
localHeader,
compressed.bytes
);
const centralHeader = joinArrays([
uint32(0x02014b50),
uint16(20),
uint16(20),
uint16(0x0800),
uint16(compressed.method),
uint16(dos.time),
uint16(dos.date),
uint32(checksum),
uint32(compressed.bytes.length),
uint32(originalBytes.length),
uint16(fileNameBytes.length),
uint16(0),
uint16(0),
uint16(0),
uint16(0),
uint32(0),
uint32(localOffset),
fileNameBytes
]);
centralParts.push(centralHeader);
centralSize += centralHeader.length;
localOffset +=
localHeader.length +
compressed.bytes.length;
}
const endRecord = joinArrays([
uint32(0x06054b50),
uint16(0),
uint16(0),
uint16(files.length),
uint16(files.length),
uint32(centralSize),
uint32(localOffset),
uint16(0)
]);
return new Blob(
[
...localParts,
...centralParts,
endRecord
],
{
type: "application/zip"
}
);
}
async function fetchImage(url) {
const parsedUrl = new URL(url, location.href);
parsedUrl.hash = "";
/*
* 关键修改:
* credentials: "omit"
*
* 不向微信图片 CDN 发送当前网页 Cookie,
* 避免 Access-Control-Allow-Credentials 报错。
*/
const response = await fetch(parsedUrl.href, {
method: "GET",
mode: "cors",
credentials: "omit",
cache: "force-cache",
referrerPolicy: "no-referrer"
});
if (!response.ok) {
throw new Error(
`HTTP ${response.status} ${response.statusText}`
);
}
return response;
}
const downloadedFiles = [];
const failedUrls = [];
setProgress(
`共找到 ${imageUrls.length} 张图片\n开始获取……`
);
for (
let index = 0;
index < imageUrls.length;
index++
) {
const url = imageUrls[index];
setProgress(
`正在获取 ${index + 1}/${imageUrls.length}\n` +
`成功:${downloadedFiles.length}\n` +
`失败:${failedUrls.length}`
);
try {
const response = await fetchImage(url);
const blob = await response.blob();
if (!blob.size) {
throw new Error("图片内容为空");
}
if (
blob.type &&
!blob.type.startsWith("image/")
) {
console.warn(
`第 ${index + 1} 个资源类型不是图片:`,
blob.type,
url
);
}
const extension = getExtension(
url,
blob.type
);
const bytes = new Uint8Array(
await blob.arrayBuffer()
);
// 按成功顺序连续命名:1、2、3……
downloadedFiles.push({
name: `${downloadedFiles.length + 1}.${extension}`,
bytes
});
} catch (error) {
console.error(
`第 ${index + 1} 张图片获取失败:`,
url,
error
);
failedUrls.push(
`${index + 1}. ${url}\n错误:${error.message}`
);
}
}
if (!downloadedFiles.length) {
setProgress(
"所有图片均获取失败。\n请查看控制台中的具体错误。"
);
alert(
"所有图片均获取失败,请查看控制台中的错误信息。"
);
return;
}
const successCount = downloadedFiles.length;
if (failedUrls.length) {
downloadedFiles.push({
name: "下载失败记录.txt",
bytes: new TextEncoder().encode(
"以下图片获取失败:\n\n" +
failedUrls.join("\n\n")
)
});
}
setProgress(
`图片获取完成\n` +
`成功:${successCount} 张\n` +
`失败:${failedUrls.length} 张\n` +
`正在生成 ZIP……`
);
const zipBlob =
await createZip(downloadedFiles);
const downloadUrl =
URL.createObjectURL(zipBlob);
const link = document.createElement("a");
link.href = downloadUrl;
link.download = ZIP_NAME;
link.style.display = "none";
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => {
URL.revokeObjectURL(downloadUrl);
}, 30000);
setProgress(
`处理完成!\n` +
`成功:${successCount} 张\n` +
`失败:${failedUrls.length} 张\n` +
`文件:${ZIP_NAME}`
);
setTimeout(() => {
progressBox.remove();
}, 10000);
})();
ai写的,存个档省去下次再造轮子的问题(
)