超快速Node.js手撸一个Tumblr爬虫

也不算是突发奇想,早就有这个打算了可是一直都懒得写,毕竟像我这种懒人可没有耐心刷瀑布流,而且刷的时候还发现,有不断重复的图片。作为一个老司机,怎么能这样就忍受!

本项目全程同步操作,因为怕异步太快而被封,也懒得研究到底会不会封和怎么防封了。

不过,说实话 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粘贴至配置文件

1532822125885

firstUrl的获取

同上,找到request url 为如下格式的请求, 复制其中的 1532822125885

/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 (opens new window)

https://github.com/190434957/tumblr-crawler