前端爬取别的网页并下载网页上的内容到本地。上网查到可以使用node.js的Puppeteer库来实现。下面我根据我的理解来**说一下puppeteer是什么?**参考puppeteer中文文档。
puppeteer中文意思是被操纵的木偶,从字面意思理解操纵木偶应该很简单。官方文档给的定义是:puppeteer是node的一个库(方法),Puppeteer 默认以 headless 模式运行(其实就是前端通过headless模式来运行谷歌浏览器),headless谷歌浏览器有说明,感兴趣的可以去看。也可以通过配置进行‘有头’模式去运行浏览器。
Puppeteer 至少需要 Node v6.4.0及以上的版本,这个在使用中需要注意。打开官方文档,发现一堆async/await,这是es7的语法。
我这个案例是下载图片到本地的案例,刚开始创建项目是需要npm init,否则项目运行不起来,还有一个注意点事在package.json里面配置type:“module”
创建浏览器实例,括号里面可以配置浏览器,headless:false,就是打开浏览器,反之就是关闭浏览器。
创建一个新的页面
加载浏览器页面,参数是要爬取网页的URL。
监听浏览器请求,就是被爬取网页的请求
这个方法是为了获取被爬取网页上的内容,比如本案例的图片的url,他的参数是一个回调函数,它还可以在页面上指向一些点击事件等操作,特别注意的是这个方法传参必须是字符串
这个方法是用来获取页面元素属性和值。
关闭浏览器
import axios from 'axios'
import path from 'path'
import fs from 'fs/promises'
import puppeteer from 'puppeteer'
const __dirname = path.resolve() // 当前文件所在目录
// 定义下载函数
const download = (url, dir, filename) =>
new Promise(async (resolve, reject) => {
try {
// { responseType: 'arraybuffer' } 不加这个会乱码
const { data } = await axios.get(url, { responseType: 'arraybuffer' }) // 请求图片数据
fs.writeFile(path.join(dir, filename), data).then(() => resolve(filename + ' -> 下载成功')) // 写入图片
} catch ({ code }) {
reject({ filename, url, code }) // 抛出错误
}
})
// 失败重试
Promise.retry = (fn, arg, count = 3) =>
new Promise(async (resolve, reject) => {
if (typeof fn !== 'function') reject(new Error('fn must be a function')) // 判断 fn 是否为函数
if (!Array.isArray(arg)) reject(new Error('arg must be an array')) // 判断 arg 是否为数组
let index = 0
while (index < count) {
try {
resolve(await fn(...arg))
return
} catch (e) {
index++
if (index === count - 1) reject(e)
}
}
})
// 图片下载函数
const imgDownloader = async (url, dir, count = 100, cb) => {
if (cb && typeof cb !== 'function') throw new Error('cb must be a function')
// 浏览器加载模块
async function getImgUrl(url) {
if (!url) throw new Error('url must be a string')
const options = {
defaultViewport: {
width: 1920,
height: 1080
},
headless: true // 不打开浏览器
// headless: false // 打开浏览器
// slowMo: 1000 // 慢慢加载
}
const browser = await puppeteer.launch(options) // 创建浏览器实例
const page = await browser.newPage() // 创建一个新的页面
try {
await page.goto(url) // 加载页面
// 定义加载方法
const loadAll = () =>
new Promise((resolve, reject) => {
// 没有数据请求后3秒 resolve() 结束异步等候
var timer
var timeout = () => (timer = setTimeout(() => resolve(true), 3000))
// 监听网页请求
page.on('request', () => {
clearTimeout(timer) // 清除定时器
timeout() // 重新设置定时器
// 网页下拉 这部分是在浏览器操作的 !!!!!这方法不在node执行
page.evaluate(cb => {
const height = document.body.offsetHeight
window.scrollTo(0, height + 500)
try {
const fn = eval(cb)
if (typeof fn === 'function') fn()
} catch (e) {
console.log(e)
}
}, `${cb}`)
})
})
console.log('开始加载网页数据...')
await loadAll() // 加载全部页面 没有请求2秒后停止
console.log('网页数据加载完毕!')
// 获取图片地址
const data = await page.$$eval('img', imgs => {
let imgUrlList = []
imgs.forEach(img => {
const property = [...img.attributes] // 将img的属性转换为数组
property.forEach(({ value }) => value.slice(0, 9).includes('//') && imgUrlList.push(value)) // 判断是否是url
})
return [...new Set(imgUrlList)].filter(i => i.indexOf('.svg', i.length - 9) == -1) // 去重
})
// 关闭浏览器
await browser.close()
const results = data.map(url => (/^http/.test(url) ? url : `https:${url}`)) // 判断是否是https协议
return results // 返回结果
} catch (e) {
// 关闭浏览器
console.log(e)
await browser.close()
}
}
// 尝试下载
try {
const urlList = await getImgUrl(url) // 获取图片下载地址列表
await fs.access(dir).catch(() => fs.mkdir(dir, { recursive: true })) // 创建目录
// 数组分片方法
const slicing = (arr, count) => {
if (!Array.isArray(arr)) throw new Error('arr must be an Array')
if (typeof count !== 'number' || count < 1) throw new Error('count must be a number')
let list = []
for (let i = 0; i < arr.length; i += count) {
list.push(arr.slice(i, i + count))
}
return list
}
// 下载列表
const list = slicing(urlList, count)
// 下载结果
const results = []
console.log(`${urlList.length}张照片待下载...`)
const startTime = new Date() // 开始时间
// 开始分片下载
for (let i = 0; i < list.length; i++) {
console.log(`开始下载 -> ${(i + 1) * count}`)
const downloadList = list[i].map((item, index) => {
// 文件名
const filename = (new Array(10).join('0') + (i * count + index)).slice(-6) + '.jpg'
return Promise.retry(download, [item, dir, filename])
})
const res = await Promise.allSettled(downloadList)
results.push(...res)
}
// 输出下载结果
results.forEach(({ value, reason }) => console.log(value || reason))
// 输出结束时间
const endTime = new Date() // 结束时间
console.log('下载完成 -> 耗时:' + (endTime - startTime) / 1000 + 's') // 耗时
} catch (e) {
console.error(e)
}
}
// const url =
// 'https://image.baidu.com/search/index?tn=baiduimage&ipn=r&ct=201326592&cl=2&lm=-1&st=-1&fm=result&fr=&sf=1&fmq=1652366997889_R&pv=&ic=&nc=1&z=&hd=&latest=©right=&se=1&showtab=0&fb=0&width=&height=&face=0&istype=2&dyTabStr=MCwzLDEsNCw1LDcsOCwyLDYsOQ%3D%3D&ie=utf-8&sid=&word=%E9%A3%8E%E6%99%AF'
const url = 'https://www.vcg.com/sets/516942437'
const dir = path.join(__dirname, 'imgs/img1') // 图片存储目录
// 第一个参数 要爬取网站的地址
// 第二个参数 图片存储目录
// 第三个参数 分片下载数量
// 第四个参数 回调函数用于浏览器操作
imgDownloader(url, dir, 200, () => {
const dom = document.querySelector('.jss50')
dom && dom.click()
})
说实话感觉Puppeteer做爬虫有点屈才了,它应该有更强大的功能,大家一起慢慢学习慢慢总结吧