也不算是突发奇想,早就有这个打算了可是一直都懒得写,毕竟像我这种懒人可没有耐心刷瀑布流,而且刷的时候还发现,有不断重复的图片。作为一个老司机,怎么能这样就忍受!
本项目全程同步操作,因为怕异步太快而被封,也懒得研究到底会不会封和怎么防封了。
不过,说实话 javaScript 的同步代码比异步代码难写多了(异步函数强写成同步)。。呕~
# 一、构建项目依赖文件
可以用IDE一步新建node.js 项目,也可以自己新建一个 package.json (opens new window),
加入如下依赖
"dependencies": {
"cheerio": "^1.0.0-rc.2",
"superagent": "^3.8.3",
"superagent-proxy": "^1.0.3",
"yamljs": "^0.3.0"
}
然后工程目录下运行即可
npm install --save
# 二、 构建项目配置文件
新建crawler.yml 作为项目配置文件,格式定位如下
crawler:
cookie: _ga=GAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx9720
userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36
firstUrl: /dashboard/xx/xxxxxxxxxxxxxx
maxPage: 101
- 根配置crawler
- cookie 访问时候使用的cookie
- userAgent 简单防封措施
- firstUrl 第一次访问的资源URL
- maxPage 程序运行获取最大页数
# 三、获取配置相关信息
通过对页面的简单观察,获取瀑布流页面的方法有如下简单的一种。
cookie的获取
打开tumblr首页,并打开开发者工具,跳到network标签下,
找到request url 为tumblr域名下的请求(dashboard……),
复制其cookie粘贴至配置文件
firstUrl的获取
同上,找到request url 为如下格式的请求, 复制其中的
/dashboard/x/xxxxxx/ 参数可以忽略
复制到配置文件
# 四、撸代码
找到了瀑布流规律,也就不需要用无头浏览器一类的了,直接用更加快速的 httpclient + jQuery
superagent 一个高效灵活的http client 库
cheerio 是一个服务端用的类jQuery 可以用jQuery语法快速定位爬取位置(不用琢磨规律和正则啦~)
- 创建main.js 为代码文件 初始化引入模块和常量
const yaml = require('yamljs'); const request = require('superagent'); const fs = require('fs'); const cheerio = require('cheerio'); require('superagent-proxy')(request); //代理库,不使用代理请去掉 /** * 项目配置文件的读取 */ const crawlerProperties = yaml.parse(fs.readFileSync('./crawler.yml').toString()); const baseUrl = 'https://www.tumblr.com'; const cookie = crawlerProperties.crawler.cookie; const userAgent = crawlerProperties.crawler.userAgent; const firstUrl = crawlerProperties.crawler.firstUrl; const maxPage = crawlerProperties.crawler.maxPage; /** * 全局设置 */ const agent = request .agent() .set('Cookie', cookie) .set('User-Agent', userAgent);
简单防封措施 因为不想研究网站的防封措施,就暴力一点sleep几个毫秒算了
/** * 睡眠函数 防封 * @param time 毫秒数 * @returns {Promise<>} 没用 */ function sleep(time) { return new Promise((resolve) => { setTimeout(() => { resolve(); }, time); }) }
- 图片URL获取递归
urlSet 为简单的去重措施
pageNum 为当前递归页数 (与firstUrl页数不对应,仅对应程序轮次)
firstUrl 为资源url
maxTurnPage 为当前轮转最大获取页数 (数字越大,去重率越好,执行总轮次越少)
使用superagent获取网页body之后使用cheerio解析, 就可以用jQuery语法进行操作啦,jQuery大家都懂的,出入不是很大
/** * 图片url 递归 * @param urlSet 去重用url Set * @param pageNum 页数 * @param firstUrl 开始url * @param maxTurnPage 轮次最大页数 * @returns {Promise<*>} 一堆递归用的数据 */ async function getPageImages(urlSet, pageNum, firstUrl, maxTurnPage) { console.log(`${pageNum} now ${urlSet.size} urls`) const res = await agent .get(`${baseUrl}/svc${firstUrl}`) .proxy('http://127.0.0.1:1080') //代理地址, 不需要代理请去掉 .catch(err => console.log(err)); /** @namespace res.body.response.DashboardPosts */ if (res != null && res.hasOwnProperty('body') && res.body.hasOwnProperty('response') && res.body.response.hasOwnProperty('DashboardPosts') && res.body.response.DashboardPosts.hasOwnProperty('body')) { const htmlStr = res.body.response.DashboardPosts.body; const $ = cheerio.load(htmlStr); const imgDivs = $('.post_media ').find('img'); //cheerio 快速定位 //遍历获取url, 添加到urlSet imgDivs.each((idx, ele) => { if (!ele.hasOwnProperty('attribs') || ele.attribs.src == null) { return; } urlSet.add(ele.attribs.src); }); } else { console.log(`get page ${pageNum} error`) let srcUrlArr = Array.from(urlSet) console.log(`${srcUrlArr.length} URLs`); fs.appendFile(`./urls.json`, JSON.stringify(srcUrlArr)); return {srcUrlArr, nextPage: pageNum, firstUrl}; } const nextUrl = res.headers['tumblr-old-next-page']; //获取下一页url if (nextUrl == null) { debugger // 未知问题打了个断点,目前还没重现过问题,等下一轮优化 } if (pageNum >= maxTurnPage) { //本轮递归结束 let srcUrlArr = Array.from(urlSet) console.log(`${srcUrlArr.length} URLs`); fs.appendFile(`./urls.json`, JSON.stringify(srcUrlArr)); return {srcUrlArr, nextPage: pageNum + 1, nextUrl}; // return一个有用的对象 } await sleep(200); //睡眠函数 return await getPageImages(urlSet, ++pageNum, nextUrl, maxTurnPage); //递归 }
- 下载图片函数递归
/** * 下载图片递归入口 * @param arr url数组 * @param idx 递归索引 * @returns {Promise<*>} 结束 */ async function downloadImg(arr, idx) { if (idx >= arr.length) { return 'end img'; } const url = arr[idx]; const urlSplit = url.split('/'); const fileName = `${urlSplit[urlSplit.length - 2]}_${urlSplit[urlSplit.length - 1]}`; if (!fs.existsSync('./pics/')) { fs.mkdirSync('./pics/') } const stream = fs.createWriteStream(`./pics/${fileName}`); await agent .get(url) .proxy('http://127.0.0.1:1080') //代理地址, 不需要代理请去掉 .pipe(stream); await sleep(200); return await downloadImg(arr, ++idx); }
- 主函数递归
/** * 轮次递归入口 * @param pageNum 开始页数 * @param firstUrl 递归开始url * @param maxTurnPage 轮次最大页数 * @returns {Promise<*>} 本次结束位的下一次url */ async function mainEntry(pageNum, firstUrl, maxTurnPage) { console.log(`starting ${pageNum} page`) //maxPage 为配置文件中获取的总页数 //end URL 请保存作为下一次程序开始的first URL if (pageNum >= maxPage || firstUrl == null) { return `end Url ${firstUrl}`; } let res = await getPageImages(new Set(), pageNum, firstUrl, pageNum + maxTurnPage - 1); await downloadImg(res.srcUrlArr, 0); return await mainEntry(res.nextPage, res.nextUrl, maxTurnPage); }
- 运行程序 以上代码从上往下写完之后,结尾加上如下代码即可立即运行
/** * 开始执行程序 * 每轮爬取10页 * 返回值为本次结束位的下一次url ( crawler.yml -> crawler.firstUrl ) */ mainEntry(1, firstUrl, 10) .then(res => { console.log(res) });
# 五、(⊙o⊙)…
- 图片URL获取递归
比较暴力和无脑的一份代码,追求想到就要立即写完的速度,毕竟(#^.^#)
你懂的~
所以也没有考虑更多的通用配置和模块封装
github 代码地址为: tumblr-crawler (opens new window)
https://github.com/190434957/tumblr-crawler