前段时间在网上畅游,在某网站上发现了不少养眼图片素材,于是想收取其为囊中之物,但人手下载效率太差,网上一下子又找不到合适的免费工具,于是就考虑开始开发这个扒图工具。
首先这个工具的需求仅定位于帮我批量下载某网站的图片,无需智能分析哪一张图片才是主图要下的,无需智能分析哪个连接跳转下一页的连接,所有这些分析都已人工找到的规律设置好。因此开发这个工具只要解决两个问题:
- 获取网页内容
- 分析网页内容,找到主图,分析图片总数
Nodejs的世界,要获取网页内容的库太多了,我选了我最顺手的axios,而分析网页内容当然jquery了,但还需要jsdom这个库将获取的网页内容模拟浏览器dom内容给jquery分析。
好了,废话不多说。上代码:
const iconv = require('iconv-lite')
const axios = require('axios').default
const Fs = require('fs')
const Path = require('path')
const A_MOMENT = 3000
async downloadPageContent(url){
const res = await axios({
url,
responseType: 'stream',
timeout: A_MOMENT, // 注意一,不设置timeout的话,网络不好时会一直loading不会继续跑下去。设了timeout,超时就可以跳到catch代码加以处理. A_MOMENT=3000
}).then(res =>
new Promise((resolve, reject) => {
const chunks = []
res.data.on('data', chunk => {
chunks.push(chunk)
})
res.data.on('end', () => {
const buffer = Buffer.concat(chunks)
const str = iconv.decode(buffer, 'gbk') // 注意二,某些网站为gbk编码,需要import('iconv')来decode
resolve(str)
})
res.data.on('error', reject)
}),
)
return res
},
async downloadImage(url, options) {
const { Referer, folder, name, id } = options
const extName = Path.extname(url)
const dir = Path.join(folder, this.padLeft(id, 4).substr(0, 2), id)
const path = Path.resolve(dir, `${name}${extName}`)
if (!Fs.existsSync(dir)) {
Fs.mkdirSync(dir, { recursive: true })
}
const writer = Fs.createWriteStream(path)
const response = await axios({
url,
method: 'GET',
responseType: 'stream',
timeout: 2 * A_MOMENT,
headers: {
Referer, // 坑,这里一定要设浏览该图片的网址,否则只会下载到同一张遮罩图片
'Sec-Fetch-Dest': 'image',
'User-Agent': ' Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36',
},
})
response.data.pipe(writer)
return new Promise((resolve, reject) => {
writer.on('finish', resolve)
writer.on('error', reject)
})
},
首先,定义两个函数,一个用来获取网页内容,一个用来下载图片,都是使用axios库。
其中调试运行过程遇到三个问题:
- 首先下载的网页内容中文乱码问题。 上网找资源,使用iconv-lite库轻松解决。
- 由于网络问题,有时网页或图片会一直loading就是不下载。只能设置timeout,加try catch,超时后可以跳到catch 代码作处理,重试或log下来悉随尊便。
- 下载图片时需要设置headers的Referer属性,值为你在网页上浏览该图片的网址。这个比较坑,不设置的话只会下载同一张广告图片,而不是你要下的正确图片。估计这是网站防盗图措施吧。
const jd = require('jsdom')
const jq = require('jquery')
const w = new jd.JSDOM().window
const $ = jq(w)
async getTotalNumber(pageContent) {
const eles = $(pageContent).find('.content-page span')
const spans= eles.map((index,ele)=>$(ele).text())
if (_.isArray(spans) && spans.length > 0) {
const res = spans.map(({ text }) => {
const reg = /共(\d+?)页/ig
const matchs = reg.exec(text)
if (matchs) {
const [, total] = matchs
return _.toNumber(total)
} else {
return null
}
}).filter(v => v).sort((a, b) => b - a)
return res[0]
} else {
throw new Error('getTotalNumberError')
}
},
asyn getImageUrl(pageContent){
const eles=$(pageContent).find('.content-pic>a>img')
const imgs=eles.map((index,ele)=>({src:$(ele).attr('src'),alt:$(ele).attr('alt')})
returm imgs
}
其次,定义两个函数,从网页内容中提取要下的图片网址,以及该系列图片一共有多少页。这涉及两个知识技能。
- jquery的selector。 参考:https://www.w3schools.com/jquery/jquery_ref_selectors.asp
- 正则表达式的运用。参考:https://baike.baidu.com/item/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F/1700215?fr=aladdin
async genDownloadPageUrl(id, total) {
const result = [`https://xxxx.com/xxx/${id}.html`]
for (let i = 1; i < total; i++) {
const url = `https://xxxx.com/xxx/${id}_${i+1}.html`
result.push(url)
}
return result
},
取得有多少图片后,就可以根据该网站的图片网址规律,获得各图片的浏览网址了。
const _ = require('lodash')
async batchDownloadImages(imagePageUrls,id) {
const MAX_LINE = 3
const batchs = _.chunk(imagePageUrls, MAX_LINE)
let b = 0
for (const batch of batchs) {
const c = await Promise.all(batch.map(url => {
return this.downloadPageContent(url)
.then(content => this.getImageUrl(content)
.then(images => {
if (images && images.length > 0) {
return Promise.all(images.map((image, index) =>
this.downloadImage(image.src, { Referer: url,folder:'F:\\',name:image.alt,id})
.then(() => 1)
.catch(error => { // errhandling
}),
)).then(result => result.reduce((p, c) => p + c, 0))
.then(result => result === images.length ? 1 : 0)
} else {
// errorhandling
}
})
.catch(error =>{ // errhandling
})
})).then(result => result.reduce((p, c) => p + c, 0))
b += c
}
return b === imagePageUrls.length
},
有了这些网址,就可以进行批量下载了。
我这里设置了一批3条line进行下载,MAX_LINE。当然如果资源许可的话,可以尝试设大些。
下载完成后,计算一下下载的图片数量是否与之前取得的数据是否一致,如果一致的话,则该系列的图片就成功下载完了。如果不一致的话,则需要log下以分析问题原因了。
async process(id) {
const url=`http://xxxxx.com/xxxx/${id}.html`
const page = await downloadPageContent(url)
// 省略errorhandle. 正常情况下,要判断url是否404或其他错误
const total = await getTotalNumber(page)
const imagePageUrls = await genDownloadPageUrl(id, total)
const success = await batchDownloadImages(imagePageUrls,id)
}
// 主体思路:
// 1. 分析第一页内容,取得该系列图片一共有多少张
// 2. 取得各页图片的网址
// 3. 批量下载图片
(async()=>{
const maxid=5349 // 最大
for(let i=maxid;i>0;i--){
await process(id) // 从最新鲜的图片开始下载
}
})()
以上程序主体基本完成,可以for循环一个系列一个系列的开始下载了。let us go~