网页爬虫抓取百度图片(保存新浪博客的历史文章备份到电脑本地,功能很强大 )
优采云 发布时间: 2021-12-24 15:04网页爬虫抓取百度图片(保存新浪博客的历史文章备份到电脑本地,功能很强大
)
原因
想把新浪博客文章的历史备份到本地,用Puppeteer写个爬虫试试。
之前喜欢用Python3+Selenium,但是发现Puppeteer好像很容易安装,功能也很强大。
第一次使用Puppeteer,查了一些资料。
本文参考:从无头浏览器保存图像
本教程中的方法非常丰富,但是在爬取新浪博客的过程中,文章中的很多方法都遇到了障碍,最后发现并实践了一个更好的方法。
第一次尝试:node.js 直接请求下载文件
这是我自己的想法。最简单的方法就是拿到图片src后直接请求下载到本地。
const downloadFile = async (url, filePath) => {
return axios({
method: "get",
url: url,
responseType: "stream",
}).then((response) => {
response.data.pipe(fs.createWriteStream(filePath));
});
};
复制代码
通过axios下载保存图片失败,新浪退回图片,防止盗链。原理暂时不清楚。
看来我们需要想办法绕过它。
第二次尝试:从新的 Canvas 中提取图像
Extract Image from a New Canvas,这种方法是创建一个空白的canvas元素,将目标图片写入其中,然后将图片数据提取为DataURL。
如果图像数据来自不同的来源,这将导致画布被污染,并且试图从画布中获取图像数据将导致错误。
const getDataUrlThroughCanvas = async (selector) => {
// Create a new image element with unconstrained size.
const originalImage = document.querySelector(selector);
const image = document.createElement('img');
image.src = originalImage.src;
// Create a canvas and context to draw onto.
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = image.width;
canvas.height = image.height;
// Ensure the image is loaded.
await new Promise((resolve) => {
if (image.complete || (image.width) > 0) resolve();
image.addEventListener('load', () => resolve());
});
context.drawImage(image, 0, 0);
return canvas.toDataURL();
};
复制代码
我尝试过并将其重写为批处理选择器版本。运行后,由于跨域问题出现错误:Uncaught DOMException: Failed to execute'toDataURL' on'HTMLCanvasElement': Tainted canvases may not be export 它可以通过 image.setAttribute("crossOrigin",' 解决Anonymous'),但是又报跨域错误,解决失败。
第三次尝试:调用浏览器执行fetch方法下载图片
(Re-)Fetch the Image from the Server,这种方法是将JS代码注入浏览器执行,直接在浏览器层面进行操作。
const assert = require('assert');
const getDataUrlThroughFetch = async (selector, options = {}) => {
const image = document.querySelector(selector);
const url = image.src;
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`Could not fetch image, (status ${response.status}`);
}
const data = await response.blob();
const reader = new FileReader();
return new Promise((resolve) => {
reader.addEventListener('loadend', () => resolve(reader.result));
reader.readAsDataURL(data);
});
};
try {
const options = { cache: 'no-cache' };
const dataUrl = await page.evaluate(getDataUrlThroughFetch, '#svg', options);
const { mime, buffer } = parseDataUrl(dataUrl);
assert.equal(mime, 'image/svg+xml');
fs.writeFileSync('logo-fetch.svg', buffer, 'base64');
} catch (error) {
console.log(error);
}
复制代码
这种方法的实践也产生了错误。看了控制台的提示,也是因为跨域。
应该是新浪做了跨域限制。注入的JS不是新浪的,无法正常获取新浪的图片。
第四次尝试:通过Chromium的DevTools协议获取图片
使用DevTools协议提取图片,也就是说我们访问的是Puppeteer的api控制的浏览器。原理类似于我们F12打开开发者工具界面,在Sources中找到对应下载的图片。
const assert = require('assert');
const getImageContent = async (page, url) => {
const { content, base64Encoded } = await page._client.send(
'Page.getResourceContent',
{ frameId: String(page.mainFrame()._id), url },
);
assert.equal(base64Encoded, true);
return content;
};
try {
const url = await page.evaluate(() => document.querySelect('#svg').src)
const content = await getImageContent(page, url);
const contentBuffer = Buffer.from(content, 'base64');
fs.writeFileSync('logo-extracted.svg', contentBuffer, 'base64');
} catch (e) {
console.log(e);
}
复制代码
就这样,我也跑了,第一次看到图片被一张一张的下载到本地,很是欣慰。
但是这种方法有一定几率对图片内容造成损坏,出现绿色背景。我怀疑是因为看的时候图片没有下载完整,所以又放弃了。
第五次尝试:【原创终极方案】直接通过page.waitForResponse方法获取响应
结合第四次尝试中图片加载不完整的思考,基于对Puppeteer文档的查询,找到了那个教程中没有提到的方法:page.waitForResponse。
page.waitForResponse 方法是等待一个响应结束继续执行,并返回到 Puppeteer Response 实例。
使用示例:
const firstResponse = await page.waitForResponse('https://example.com/resource');
const finalResponse = await page.waitForResponse(response => response.url() === 'https://example.com' && response.status() === 200);
return finalResponse.ok();
复制代码
因此,我们在获取到图片的url后,可以直接使用该方法在浏览器中等待图片下载加载完毕,在加载后返回的Response对象中获取图片数据,更加简单高效比上一个教程中的方法。
const imgResp = await detailPage.waitForResponse(img.real_src, {
timeout: 10000,
});
const buffer = await imgResp.buffer();
const imgBase64 = buffer.toString("base64");
fs.writeFileSync(
`data/images/${index}_${img.index}.jpg`,
imgBase64,
"base64"
);
复制代码
对我来说,这是我发现并成功实践的最终和最推荐的解决方案。
踩了这么多坑,记录下来供大家参考。