超快速Node.js手撸一个Tumblr爬虫
也不算是突发奇想,早就有这个打算了可是一直都懒得写,毕竟像我这种懒人可没有耐心刷瀑布流,而且刷的时候还发现,有不断重复的图片。作为一个老司机,怎么能这样就忍受!
本项目全程同步操作,因为怕异步太快而被封,也懒得研究到底会不会封和怎么防封了。
不过,说实话 javaScript 的同步代码比异步代码难写多了(异步函数强写成同步)。。呕~
一、构建项目依赖文件
可以用IDE一步新建node.js 项目,也可以自己新建一个 package.json,加入如下依赖:
"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⊙)…
比较暴力和无脑的一份代码,追求想到就要立即写完的速度,毕竟(#^.^#) 你懂的~
所以也没有考虑更多的通用配置和模块封装
github 代码地址为: tumblr-crawler https://github.com/190434957/tumblr-crawler
