前言
一直对这种cli 和爬虫有一种神秘的感觉,今天就按照课程实践一下。记录过程
开始
npm init
生成一个新的package.json 文件,
抓取百度图片
新建一个image.handle.js 专门处理图片逻辑
// 模仿浏览器的行为访问网站
const superagent = require("superagent");
// 类似jquery 操作DOM元素,比jsDom 更轻量
const cheerio = require("cheerio");
// 分析百度图片的地址,是一个默认被encode转化后的地址
// https://image.baidu.com/search/index?tn=baiduimage
// ie=utf-8&word=%E5%93%88%E5%93%88
const word = '哈哈';
superagent.get(`http://image.baidu.com/search/index?tn=baiduimage&ie=utf-8&word=${encodeURIComponent(word)}`).end((err, res) => {
if (err) {
console.log(`访问失败-${err}`)
} else {
const htmlText = res.text;
const $ = cheerio.load(htmlText);
console.log(htmlText)
}
})
执行 node image.handler.js 打印结果
盲猜是遇到了百度的反爬策略了。 最大程度模拟浏览器行为!! 咱们把request headers也补上试试。
添加请求头 去浏览器network面板, 把这些请求头的值都复制下来.
...
// copy 请求头的信息
const headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cache-Control": "max-age=0",
"Connection": "keep-alive",
"sec-ch-ua-platform": "Windows",
'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="97", "Chromium";v="97"',
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36"
}
superagent.get(`http://image.baidu.com/search/index?tn=baiduimage&ie=utf-8&word=${encodeURIComponent(word)}`)
.set(headers)
.end((err, res) => {
if (err) {
console.log(`访问失败-${err}`)
} else {
const htmlText = res.text;
const $ = cheerio.load(htmlText);
console.log(htmlText)
}
})
执行 node image.handler.js 打印结果,貌似可以了,显示了一些数据。
5、 分析数据,获取图片字段生成列表
匹配规则:/"objURL":"(.*?)",/g 获取到objURL 对应的字段内容
superagent.get(`http://image.baidu.com/search/index?tn=baiduimage&ie=utf-8&word=${encodeURIComponent(word)}`)
.set(headers)
.end((err, res) => {
if (err) {
console.log(`访问失败-${err}`)
} else {
const htmlText = res.text;
const $ = cheerio.load(htmlText);
// 正则匹配对应的图片对象
const imageMatches = htmlText.match(/"objURL":"(.*?)",/g)
console.log(imageMatches)
}
})
得到一个 objURL 的数组对象
继续获取里面的图片
const imageMatches = htmlText.match(/"objURL":"(.*?)",/g).map(item =>{
const imageUrl = item.match(/:"(.*?)",/g)
return RegExp.$1
})
console.log(imageMatches)
- 获取图片的标题列表
...
const htmlText = res.text;
const $ = cheerio.load(htmlText);
const imageMatches = htmlText.match(/"objURL":"(.*?)",/g).map(item =>{
const imageUrl = item.match(/:"(.*?)",/g)
return RegExp.$1
})
const titleMatches = htmlText.match(/"fromPageTitleEnc":"(.*?)",/g).map(item =>{
const title = item.match(/:"(.*?)",/g)
return RegExp.$1
})
console.log("🚀 ~ file: image.handler.js ~ line 36 ~ .end ~ titleMatches", titleMatches)
- 提取公共函数 封装一下获取url 和title 一样的代码
// 注意这里要写动态的正则表达式了, 因为咱们要传入动态的key
function getValuesByReg(str, key) {
const reg = new RegExp(`"${key}":"(.*?)"`,'g')
const matcheLists = str.match(reg);
const resList = matcheLists.map(item =>{
const res = item.match(/:"(.*?)"/g)
return RegExp.$1
})
return resList;
}
superagent.get(`http://image.baidu.com/search/index?tn=baiduimage&ie=utf-8&word=${encodeURIComponent(word)}`)
.set(headers)
.end((err, res) => {
if (err) {
console.log(`访问失败-${err}`)
} else {
const htmlText = res.text;
const $ = cheerio.load(htmlText);
const imageMatches = getValuesByReg(htmlText, 'objURL');
const titleMatches = getValuesByReg(htmlText, 'fromPageTitleEnc');
}
})
- 创建images目录存储图片
const fs = require("fs")
cosnt path = require("path")
// 创建images 目录存储图片
const dirPath = path.resolve(__dirname, "images")
if(fs.existsSync(dirPath)){
console.log('images 文件夹已存在,跳过创建文件夹步骤')
return
}
fs.mkdirSync(dirPath)
console.log('images 文件夹创建完成')
封装一下,方便创建不同的文件名称
function mkImageDir(pathname) {
const fullPath = path.resolve(__dirname, pathname);
if (fs.existsSync(fullPath)) {
console.log(`${pathname}目录已存在, 跳过此步骤`);
return;
}
fs.mkdirSync(fullPath);
console.log(`创建目录${pathname}成功`);
}
superagent.get(`http://image.baidu.com/search/index?tn=baiduimage&ie=utf-8&word=${encodeURIComponent(word)}`)
.set(headers)
.end((err, res) => {
if (err) {
console.log(`访问失败-${err}`)
} else {
const htmlText = res.text;
const $ = cheerio.load(htmlText);
const imageMatches = getValuesByReg(htmlText, 'objURL');
const titleMatches = getValuesByReg(htmlText, 'fromPageTitleEnc');
// 创建文件目录
mkImageDir("images")
}
})
9、下载图片到images目录下
// 下载图片到本地
function downloadImage(url, name, index){
return new Promise((resolve, reject) => {
const fullpath = path.join(__dirname, 'images', `${index}-${name}.png`)
// 通过接口访问url 获取内容,写入内容,二进制内容 binary
superagent.get(url).end((err, res) => {
if(err) return reject(err);
fs.writeFile(fullpath, res.body, 'binary', err =>{
if(err) return reject(err)
return resolve()
})
})
})
superagent.get(`http://image.baidu.com/search/index?tn=baiduimage&ie=utf-8&word=${encodeURIComponent(word)}`)
.set(headers)
.end((err, res) => {
if (err) {
console.log(`访问失败-${err}`)
} else {
const htmlText = res.text;
const $ = cheerio.load(htmlText);
const imageMatches = getValuesByReg(htmlText, 'objURL');
const titleMatches = getValuesByReg(htmlText, 'fromPageTitleEnc');
// 创建文件目录
mkImageDir("images")
imageMatches.forEach((url, index) => {
downloadImage(url, titleMatches[index], index)
})
}
})
10、引入一个进度条,看一下下载的进度
安装一个第三方包 cli-progress
yarn add cli-progress --S
// 引入文件 cli-progress
const cliProgress = require("cli-progress");
// 初始化 cliProgress 实例 bar1
const bar1 = new cliProgress.SingleBar({
clearOnComplete: false
}, cliProgress.Presets.shades_classic);
superagent.get(`http://image.baidu.com/search/index?tn=baiduimage&ie=utf-8&word=${encodeURIComponent(word)}`)
.set(headers)
.end(async (err, res) => {
if (err) {
console.log(`访问失败-${err}`)
} else {
const htmlText = res.text;
const $ = cheerio.load(htmlText);
const imageMatches = getValuesByReg(htmlText, 'objURL');
const titleMatches = getValuesByReg(htmlText, 'fromPageTitleEnc');
try {
// 创建文件目录
await mkImageDir("images");
// 总数量
const total = imageMatches.length;
// 初始成功的条数
let successed = 0
// 进度条开始
bar1.start(total, 0);
imageMatches.forEach((url, index) => {
downloadImage(url, titleMatches[index], index).then((resolve)=>{
successed++
// 进度条更新
bar1.update(successed)
}).then(()=>{
if(successed == total){
// 进度条停止
bar1.stop();
console.log('恭喜!图片下载完成!')
}
})
})
} catch(e){
console.log(e);
}
}
})
执行 node image.handler.js ,控制台打印信息
11、已存在images先删除, 再创建
function removeDir(pathname) {
const fullPath = path.resolve(__dirname, pathname);
const process = require('child_process');
console.log(`${fullPath} 目录已存在,准备执行删除`)
// mac
// process.execSync(`rm -rf ${fullPath}`);
// windows
process.execSync(`rimraf ${fullPath}`);
console.log(`历史目录${fullPath}删除完成`)
}
// 改造mkImageDir 为promise
function mkImageDir(pathname) {
return new Promise((resolve, reject) =>{
const fullPath = path.resolve(__dirname, pathname);
if (fs.existsSync(fullPath)) {
// 删除文件夹
removeDir(pathname);
}
fs.mkdirSync(fullPath);
console.log(`创建目录${pathname}成功`);
// 删除成功后已解决
return resolve();
})
}
12、把固定的查询关键词哈哈,通过cli来输入关键词。
- 安装 inquirer, commander
yarn add commander inquirer --registry=https://registry.npm.taobao.org - 初始化交互的问题(新建index.js).
// index.js
#!/usr/bin/env node
const fs = require('fs');
// 一个用户与命令行交互的工具
const inquirer = require('inquirer');
const commander = require('commander');
const { runImg } = require('./image.handler.js');
// 交互选项
const initQuestions = [{
type: 'checkbox',
name: 'channels',
message: '请选择想要搜索的渠道',
choices: [
{
name: '百度图片',
value: 'images'
},
{
name: '百度视频',
value: 'video'
}
]
},{
type: 'input',
name: 'keyword',
message: '请输入想要搜索的关键词'
}]
inquirer.prompt(initQuestions).then(result =>{
const {
channels,
keyword
} = result;
for (let channel of channels) {
switch (channel) {
case 'images': {
runImg(keyword)
break
}
}
}
})
把图片下载函数提取导出 runImg
// image.handler.js
...
function runImg(keyword) {
superagent
.get(`http://image.baidu.com/search/index?tn=baiduimage&ie=utf-8&word=${encodeURIComponent(keyword)}`)
.set(headers)
.end(async (err, res) => {
if (err) {
console.log(`访问失败-${err}`)
} else {
const htmlText = res.text;
const $ = cheerio.load(htmlText);
const imageMatches = getValuesByReg(htmlText, 'objURL');
const titleMatches = getValuesByReg(htmlText, 'fromPageTitleEnc');
try {
// 创建文件目录
await mkImageDir("images");
// 总数量
const total = imageMatches.length;
let successed = 0
// 进度条开始
bar1.start(total, 0);
imageMatches.forEach((url, index) => {
downloadImage(url, titleMatches[index], index).then((resolve)=>{
successed++
// 进度条更新
bar1.update(successed)
}).then(()=>{
if(successed == total){
// 进度条停止
bar1.stop();
console.log('恭喜!图片下载完成!')
}
})
})
} catch(e){
console.log(e);
}
}
})
}
module.exports = {
runImg
}
13、目前完成的都是请求首页的图片只有30张,如何请求自定义数量的图片数呢?
- 通过自定义cli 来交互输入数量
- 分析数量超过一页的数据,其他页数的数据是分页接口获取到的
- 递归请求,请求自定义数量
- 首先要接收用户收入的count参数
// index.js
...
const initQuestions = [{
type: 'checkbox',
name: 'channels',
message: '请选择想要搜索的渠道',
choices: [{
name: '百度图片',
value: 'images'
},
{
name: '百度视频',
value: 'video'
}
]
},
{
type: 'input',
name: 'keyword',
message: '请输入想要搜索的关键词',
},
{
type: 'number',
name: 'counts',
message: '请输入要下载图片的数量x, 最小30',
},
];
inquirer.prompt(initQuestions).then(result => {
// {"channel":["images"],"keyword":"哈哈哈哈", "counts": 2}
const {
channels,
keyword,
counts
} = result;
for (let channel of channels) {
switch (channel) {
case 'images': {
runImg(keyword, counts);
break;
}
}
}
})
// 添加一个参数 counts
function runImg(keyword, counts) { }
-
写递归请求, 请求自定义数量,涉及到分页,超过首页数据的展示分页数据,走接口拿到数据,未超过的则展示之前的数据
-
梳理逻辑
先把之前的请求封装一下, 因为set header是可以复用的.
// 封装请求地址,接口和页面都需要用到
function request(url){
return new Promise((resolve, reject)=>{
superagent
.get(url)
.set(headers)
.end(async (err, res) => {
if (err) {
console.log(`访问失败-${err}`)
} else {
resolve(res)
}
})
})
}
分页请求递归处理
// 分页封装
async function getImageByPage(start, total, word) {
let allImages = [];
while (start < total) {
const size = Math.min(60, total - start); // 限制每次最大请求60
const res = await request(`https://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&fp=result&queryWord=${encodeURIComponent(word)}&ie=utf-8&oe=utf-8&word=${encodeURIComponent(word)}&pn=${start}&rn=${size}&${Date.now()}=`);
allImages = allImages.concat(res.text);
start = start + size;
}
return allImages;
}
// runImg 首页获取数据,如果数量超过首页的数据,进行分页数据拼接,并进行最终的图片数据下载
function runImg(keyword, counts) {
request(`http://image.baidu.com/search/index?tn=baiduimage&ie=utf-8&word=${encodeURIComponent(keyword)}`).then(async (res) =>{
const htmlText = res.text;
const $ = cheerio.load(htmlText);
const imageMatches = getValuesByReg(htmlText, 'objURL');
const titleMatches = getValuesByReg(htmlText, 'fromPageTitleEnc');
// 为了方便和后续json数据结合, 修改为同一个数组
let allImageUrls = imageMatches.map((imgUrl, index) => ({
imgUrl,
title: titleMatches[index]
}));
try {
// 创建文件目录
await mkImageDir("images");
// 弟一页总数量
const firstPageCount = allImageUrls.length;
// 拼接分页数据
if(counts > firstPageCount){
// 如果要下载的图片数量大于初始的已请求的数量, 就再去请求补足counts
const restImgUrls = await getImageByPage(firstPageCount, counts, keyword);
console.log(restImgUrls)
const formatImgUrls = restImgUrls.filter(item => item.middleURL).map(item =>({
"imgUrl": item.middleURL,
"title": item.fromPageTitleEnc
}))
allImageUrls = allImageUrls.concat(formatImgUrls)
}
const total = allImageUrls.length
let successed = 0
// 进度条开始
bar1.start(total, 0);
allImageUrls.forEach((item, index) => {
downloadImage(item.imgUrl, item.title, index).then((resolve)=>{
successed++
// 进度条更新
bar1.update(successed)
}).then(()=>{
if(successed == total){
// 进度条停止
bar1.stop();
console.log('恭喜!图片下载完成!')
}
})
})
} catch(e){
console.log(e);
}
})
}
运行一下看看!!!发现 await getImageByPage(firstPageCount, counts, keyword)代码处抛出了错误,写的是禁止爬虫进入
这个时候想到上次禁止爬虫进入是没有模拟浏览器的请求头,那我们就查看接口的请求头需要什么参数。 咱们把能加的参数都加上, 然后看一下request header, 发现其实Accept和之前是不一样的, 需要改一下
// image.handler.js
// 分页接口的请求头内容
const headers2 = {...headers,'Accept': 'text/plain, */*; q=0.01'}
// 封装请求地址,加一个请求头参数,默认headers
function request(url,headers={...headers}){
return new Promise((resolve, reject)=>{
superagent
.get(url)
.set(headers)
.end(async (err, res) => {
if (err) {
console.log(`访问失败-${err}`)
} else {
resolve(res)
}
})
})
}
// 修改分页请求入参
async function getImageByPage(start, total, word) {
let allImages = [];
while (start < total) {
const size = Math.min(60, total - start); // 限制每次最大请求60
const res = await request(`https://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&fp=result&queryWord=${encodeURIComponent(word)}&ie=utf-8&oe=utf-8&word=${encodeURIComponent(word)}&pn=${start}&rn=${size}&${Date.now()}=`, headers2);
allImages = allImages.concat(JSON.parse(res.text).data);
start = start + size;
}
return allImages;
}
// runImg 首页获取数据,如果数量超过首页的数据,进行分页数据拼接,并进行最终的图片数据下载
function runImg(keyword, counts) {
// request 添加 参数headers
request(`http://image.baidu.com/search/index?tn=baiduimage&ie=utf-8&word=${encodeURIComponent(keyword)}`,headers).then(async (res) =>{
...
});
}
遇到图片写入有问题的时候,观察修复图片名称
npm run start 依次输入内容得到结果,小狗的图片创建并加载成功。
可以发现我们已经可以自定义图片数量下载了!!!