爬虫爬不到数据?试试puppeteer(Node.js)

栏目: Node.js · 发布时间: 5年前

内容简介:前不久,在学校仿微博鲜知微信小程序的时候,正愁数据从哪来,翻到了数据一样的页面微博新鲜事(需退出登录状态),接着用cheerio爬取数据。结果翻车了,检查了一下发现发出请求拿到的body是空的,到微博新鲜事的网页源代码一看,发现...人家的html是js渲染的,应该是还有一次跳转。哇,好狠!本着只要思想不滑坡,办法总比困难多的精神,我用上了puppeteer。就我的使用体验来讲,puppeteer就像是一个完整的浏览器一样,它真正的去解析、渲染页面,所以上面提到因为跳转拿不到页面结构的问题也可以解决。

前不久,在学校仿微博鲜知微信小程序的时候,正愁数据从哪来,翻到了数据一样的页面微博新鲜事(需退出登录状态),接着用cheerio爬取数据。结果翻车了,检查了一下发现发出请求拿到的body是空的,到微博新鲜事的网页源代码一看,发现...人家的html是js渲染的,应该是还有一次跳转。哇,好狠!

本着只要思想不滑坡,办法总比困难多的精神,我用上了puppeteer。

puppeteer

就我的使用体验来讲,puppeteer就像是一个完整的浏览器一样,它真正的去解析、渲染页面,所以上面提到因为跳转拿不到页面结构的问题也可以解决。

话不多说,先来安装试试吧,因为puppeteer还挺大的,安装就用cnpm吧(淘宝镜像的快很多)。

  • 安装cnpm,有的话直接跳过

    npm install -g cnpm --registry=https://registry.npm.taobao.org

  • 安装puppeteer

    cnpm i puppeteer -S

  • Hello World

    举一个栗子试试就爬微博新鲜事的第一个选项的标题吧

    爬虫爬不到数据?试试puppeteer(Node.js)
    const puppeteer = require('puppeteer');
    
    const url = 'https://weibo.com/?category=novelty';
    const sleep = (time) => new Promise((resolve, reject) => { // 因为中间包含一次人为设置的跳转所以只好搞一个sleep等跳转
      setTimeout(() => {
        resolve(true);
      }, time);
    })
    async function getindex(url) {
      const browser = await puppeteer.launch({  // 一个浏览器对象
        headless: false // puppeteer的功能很强大,但这里用不到无头,就关了
      });
      const page = await browser.newPage(); // 创建一个新页面
      await page.goto(url, { // 跳转到想要的url,并设置跳转等待时间
        timeout: 60000
      });
      await sleep(60000); // 等待第二次跳转完成
      const data = await page.$$eval('.UG_list_b', (lists) => { // 相当于document.querySelectorAll('.UG_list_b')
        var newarr = [Array.from(lists)[0]] // 因为只要第一个,所以把其他的去掉了,若要所有的结果直接取Array.from(lists)即可
        return newarr.map(node => { // 遍历数组选择标题
          const title = node.querySelector('.list_des .list_title_b a').innerText;
          return title;
        })
      });
      browser.close(); // 关闭浏览器
      return data;
    }
    
    getindex(url)
      .then(res => {
        console.log(res);
      })
    复制代码

    结果如下

    爬虫爬不到数据?试试puppeteer(Node.js)

    简要的解释一下这里用到的API:

    • puppeteer.launch([object]):

      通过 puppeteer.launch([object]) 创建一个 Browser 对象,它通过接收一个非必须的对象参数进行配置。

      可以设置字段包括 defaultViewport (默认视口大小), ignoreHTTPSErrors (是否在导航期间忽略 HTTPS 错误), timeout (超时时限)等。

    • browser.newPage()

      通过 browser.newPage() 创建一个新的 Page 对象,在浏览器中会打开一个新的标签页。

    • page.goto(url[, options])

      根据传入的url,页面导航去相应的页面,它也通过接收一个非必须的对象参数进行配置。

      可以设置字段包括 timeout (跳转等待时限), waitUntil (满足什么条件认为页面跳转完成,默认为load)。

      但是从这个demo的逻辑来说,只有第二次跳转 passport.weibo.com/visitor/vis… 并渲染完成才认为页面跳转完成,而这第二次跳转是人为设计的,所以直接访问微博新鲜事未跳转完成时返回的状态码仍是200而不是3开头的,使得难以区分是否跳转完成。

    • page.$$eval(selector, pageFunction[, ...args])

      selector是选择器,如'.class', '#id', 'a[href]'等

      pageFunction是在浏览器实例上下文中要执行的方法

      ...args是要传给 pageFunction 的参数。

      其作用相当于在页面上执行 Array.from(document.querySelectorAll(selector)),然后把匹配到的元素数组作为第一个参数传给 pageFunction 并执行,返回的结果也是 pageFunction 返回的。

      而 page.evaluate(pageFunction) 有大致相同的功能,还更灵活。

    • browser.close()

      这个没什么好说的,就是关闭浏览器,毕竟谷歌浏览器占用内存还是不少的,要是家里有矿的当我没说。

      更多详细信息可查询文档

实际使用

需求是抓取微博新鲜事页面的标题、头图、作者、时间等信息。

爬虫爬不到数据?试试puppeteer(Node.js)

并抓取对应话题点击进去的页面信息,包括其左边分类线戳的类别,类别对应下的所有微博,包括博文、博主、时间、转发数、评论数、点赞数。

爬虫爬不到数据?试试puppeteer(Node.js)

还有,要抓取对应话题里所有对应博文的页面信息,包括博文博主,相应的转发、评论、点赞数,以及博文下的所有评论,包括评论层主头像、昵称、评论内容和点赞数。

爬虫爬不到数据?试试puppeteer(Node.js)

并将信息都存成json文件。

  • 分析

    需要暂存爬下来的url地址,并遍历存下爬取的信息。

    而且微博设置的障碍还不止有二次跳转,还有随机跳到到访问过于频繁,请24小时后试的页面和未登录状态下随机跳转到weibo.com/login.php 以及 504,这个只要用 page.url() 获取当前网址比对处理即可。

    爬虫爬不到数据?试试puppeteer(Node.js)
    爬虫爬不到数据?试试puppeteer(Node.js)

    麻烦的只有页面类型复杂繁琐,话题页面有四图、一图、纯文本、视频类型,他们的DOM结构都不同,博文页面也有些许不同,但都不是技术难点。

    爬虫爬不到数据?试试puppeteer(Node.js)
  • 实际代码

    小项目我就不分目录了,直接上代码吧

    const puppeteer = require('puppeteer');
    const fs = require('fs');
    const baseurl = 'https://weibo.com';
    const Dir = './data/';
    
    const sleep = (time) => new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(true);
      }, time);
    })
    
    async function doSpider(url, pageFunction) { // 爬虫函数
      const browser = await puppeteer.launch({
        headless: false
      });
      const page = await browser.newPage();
      await page.goto(url, {
        timeout: 100000
      });
      await sleep(60321);
      if (page.url().indexOf(url) === -1) {
        console.log('+------------------------------------');
        console.log('|失败,当前页面:' + page.url());
        console.log('|再次跳转:    ' + url);
        console.log('+------------------------------------');
        browser.close();
        return doSpider(url, pageFunction);
      }
      const data = await page.evaluate(pageFunction, url);
      browser.close();
      return data;
    }
    
    async function runPromiseByQueue(myPromises) {  // 利用数组reduce给爬虫执行的结果排序
      return await myPromises.reduce(
        async (previousPromise, nextPromise) => previousPromise.then(await nextPromise),
        Promise.resolve([])
      );
    }
    
    function saveLocalData(name, data) {// 数据存文件
      fs.writeFile(Dir + `${name}.json`, JSON.stringify(data), 'utf-8', err => {
        if (!err) {
          console.log(`${name}.json保存成功!`);
        }
      })
    }
    
    const spiderIndex = () => { //新鲜事页面的选择器
      const lists = document.querySelectorAll('.UG_list_b');
      var newarr = [Array.from(lists)[0], Array.from(lists)[1]]
      return newarr.map(node => {
        const title = node.querySelector('.list_des .list_title_b a').innerText;
        const picUrl = node.querySelector('.pic.W_piccut_v img').getAttribute('src');
        const newUrl = node.querySelector('.list_des .list_title_b a').getAttribute('href');
        const userImg = node.querySelector('.subinfo_box a .subinfo_face img').getAttribute('src');
        const userName = node.querySelector('.subinfo_box a .subinfo').innerText;
        const time = node.querySelector('.subinfo_box>.subinfo.S_txt2').innerText;
        return {
          title,
          picUrl,
          newUrl,
          userImg,
          userName,
          time
        }
      })
    }
    
    const spiderTopic = (url) => {  // 话题页面的选择器
      const picUrl = document.querySelector('.UG_list_e .list_nod .pic img').getAttribute('src');
      const topicTitle = document.querySelector('.UG_list_e .list_title').innerText;
      const des = document.querySelector('.UG_list_e .list_nod .list_des').innerText;
      const userImg = document.querySelector('.UG_list_e .list_nod .subinfo_box a .subinfo_face img').getAttribute('src');
      const userName = document.querySelector('.UG_list_e .list_nod .subinfo_box a .subinfo').innerText;
      const time = document.querySelector('.UG_list_e .list_nod .subinfo_box>.subinfo.S_txt2').innerText;
      const lists = document.querySelectorAll('.UG_content_row');
      const types = Array.from(lists).map(node => {
        const title = node.querySelector('.UG_row_title').innerText;
    
        const v2list = node.querySelectorAll('.UG_list_v2 .list_des');
    
        var v2Item = Array.from(v2list).map(node => {
          const content = node.querySelector('h3').innerText;
          const userImg = node.querySelector('.subinfo_box a .subinfo_face img').getAttribute('src');
          const userName = node.querySelector('.subinfo_box a .subinfo').innerText;
          const time = node.querySelector('.subinfo_box>.subinfo.S_txt2').innerText;
          const like = node.querySelector('.subinfo_box.subinfo_box_btm .subinfo_rgt:nth-of-type(1) em:nth-of-type(2)').innerText;
          const comment = node.querySelector('.subinfo_box.subinfo_box_btm .subinfo_rgt:nth-of-type(3) em:nth-of-type(2)').innerText;
          const relay = node.querySelector('.subinfo_box.subinfo_box_btm .subinfo_rgt:nth-of-type(5) em:nth-of-type(2)').innerText;
          const newUrl = node.getAttribute('href');
          return newitem = {
            content,
            userImg,
            userName,
            time,
            like,
            comment,
            relay,
            newUrl,
            img: []
          }
        })
    
        const alist = node.querySelectorAll('.UG_list_a');
    
        var aItem = Array.from(alist).map(node => {
          const content = node.querySelector('h3').innerText;
          const newUrl = node.getAttribute('href');
          const img1 = node.querySelector('.list_nod .pic:nth-of-type(1) img').getAttribute('src');
          const img2 = node.querySelector('.list_nod .pic:nth-of-type(2) img').getAttribute('src');
          const img3 = node.querySelector('.list_nod .pic:nth-of-type(3) img').getAttribute('src');
          const img4 = node.querySelector('.list_nod .pic:nth-of-type(4) img').getAttribute('src');
          const userImg = node.querySelector('.subinfo_box a .subinfo_face img').getAttribute('src');
          const userName = node.querySelector('.subinfo_box a .subinfo').innerText;
          const time = node.querySelector('.subinfo_box>.subinfo.S_txt2').innerText;
          const like = node.querySelector('.subinfo_box .subinfo_rgt:nth-of-type(2) em:nth-of-type(2)').innerText;
          const comment = node.querySelector('.subinfo_box .subinfo_rgt:nth-of-type(4) em:nth-of-type(2)').innerText;
          const relay = node.querySelector('.subinfo_box .subinfo_rgt:nth-of-type(6) em:nth-of-type(2)').innerText;
          return newitem = {
            img: [img1, img2, img3, img4],
            content,
            userImg,
            userName,
            time,
            like,
            comment,
            relay,
            newUrl
          }
        })
    
        const blist = node.querySelectorAll('.UG_list_b');
    
        var bItem = Array.from(blist).map(node => {
          const content = node.querySelector('.list_des h3').innerText;
          const newUrl = node.getAttribute('href');
          var img = ''
          if (node.querySelector('.pic img') != null) {
            img = node.querySelector('.pic img').getAttribute('src');
          }
          const userImg = node.querySelector('.list_des .subinfo_box a .subinfo_face img').getAttribute('src');
          const userName = node.querySelector('.list_des .subinfo_box a .subinfo').innerText;
          const time = node.querySelector('.list_des .subinfo_box>.subinfo.S_txt2').innerText;
          const like = node.querySelector('.list_des .subinfo_box .subinfo_rgt:nth-of-type(2) em:nth-of-type(2)').innerText;
          const comment = node.querySelector('.list_des .subinfo_box .subinfo_rgt:nth-of-type(4) em:nth-of-type(2)').innerText;
          const relay = node.querySelector('.list_des .subinfo_box .subinfo_rgt:nth-of-type(6) em:nth-of-type(2)').innerText;
          return newitem = {
            img: [img],
            content,
            userImg,
            userName,
            time,
            like,
            comment,
            relay,
            newUrl
          }
        })
        return {
          title,
          list: [...v2Item, ...aItem, ...bItem]
        }
      })
      return {
        topicTitle,
        picUrl,
        des,
        userImg,
        userName,
        time,
        newUrl: url,
        types
      }
    }
    
    const spiderPage = (url) => {   // 博文页面的选择器
      var data = {}
      const content = document.querySelector('.WB_text.W_f14').innerText;
      const piclist = document.querySelectorAll('.S_bg1.S_line2.bigcursor.WB_pic');
      const picUrls = Array.from(piclist).map(node => {
        const picUrl = node.querySelector('img').getAttribute('src');
        return picUrl;
      })
      const time = document.querySelector('.WB_detail>.WB_from.S_txt2>a').innerText;
      const userImg = document.querySelector('.WB_face>.face>a>img').getAttribute('src');
      const userName = document.querySelector('.WB_info>a').innerText;
      const like = document.querySelector('.WB_row_line>li:nth-of-type(4) em:nth-of-type(2)').innerText;
      const comment = document.querySelector('.WB_row_line>li:nth-of-type(3) em:nth-of-type(2)').innerText;
      const relay = document.querySelector('.WB_row_line>li:nth-of-type(2) em:nth-of-type(2)').innerText;
    
      const lists = document.querySelectorAll('.list_box>.list_ul>.list_li[comment_id]');
      const commentItems = Array.from(lists).map(node => {
        const userImg = node.querySelector('.WB_face>a>img').getAttribute('src');
        const userName = node.querySelector('.list_con>.WB_text>a[usercard]').innerText;
    
        const [frist, ...contentkey] = [...node.querySelector('.list_con>.WB_text').innerText.split(':')]
        const content = [...contentkey].toString();
        const time = node.querySelector('.WB_func>.WB_from').innerText
        const like = node.querySelector('.list_con>.WB_func [node-type=like_status]>em:nth-of-type(2)').innerText
    
        return {
          userImg,
          userName,
          content,
          time,
          like
        }
      })
      return data = {
        newUrl: url,
        content,
        picUrls,
        time,
        userImg,
        userName,
        like,
        comment,
        relay,
        commentItems
      }
    }
    
    function start() {  //主要操作,不封起来太难看了
      doSpider(baseurl + '/?category=novelty', spiderIndex) //爬完新鲜事页面给数据
        .then(async data => {
          await saveLocalData('Index', data);// 爬完新鲜事页面给的数据存成叫Index的文件
          return data.map(item => item.newUrl);// 把下一次爬的url都取出来
        })
        .then(async (urls) => {
          let doSpiders = await urls.map(async url => {//把url全部爬上就绪,返回函数待 排序 处理
            if (url[1] === '/') {// 有部分url只缺协议部分不缺baseurl,加个区分
              let newdata = await doSpider('https:' + url, spiderTopic);
              return async (data) => [...data, newdata]
            } else {
              let newdata = await doSpider(baseurl + url, spiderTopic);
              return async (data) => [...data, newdata]
            }
          })
          let datas = await runPromiseByQueue(doSpiders);//挖坑排序并执行
          return datas;
        })
        .then(async data => {
          saveLocalData('Topic', data);// 数据存文件
          let list = [];
          await data.forEach(item =>// 取出下一次爬的所有url
            item.types.forEach(type =>
              type.list.forEach(item =>
                list.push(item.newUrl)
              )
            )
          )
          return list;
        })
        .then(async (urls) => {
          let doSpiders = await urls.map(async url => {// 重复上述的爬虫就绪
            if (url[1] === '/') {
              let newdata = await doSpider('https:' + url, spiderPage);
              return (data) => [...data, newdata]
            } else {
              let newdata = await doSpider(baseurl + url, spiderPage);
              return (data) => [...data, newdata]
            }
          })
          let datas = await runPromiseByQueue(doSpiders);// 重复一样的爬虫操作,并根据挖好的坑对结果排序
          return datas;
        })
        .then(async data => {
          saveLocalData('Page', data);// 数据存成文件
        })
    }
    
    start();
    复制代码
    • 运行过程简要说明:

      运行可以分三个过程( 新鲜事页面获取包括多个话题页面url在内的信息并存储、 并发爬 话题页面获取包括多个博文页面url在内的信息并存储、 并发爬 博文页面包括所有评论在内的信息并存储),并且数据可以通过存下来的newUrl字段来匹配主从,建立联系。

    我原来写过一个把所有爬数据的异步操作都串联起来的版本,但是效率太低了,就用数组的reduce方法挖坑排序后直接并发操作,大大提升了效率(我跟你港哦,这个reduce真的好好用豁)。

    下面是控制台输出和3个爬下来的数据文件(行数太多影响观感就格式化后截成图):

    爬虫爬不到数据?试试puppeteer(Node.js)
    爬虫爬不到数据?试试puppeteer(Node.js)
    爬虫爬不到数据?试试puppeteer(Node.js)
    爬虫爬不到数据?试试puppeteer(Node.js)

    这份代码只爬了新鲜事里的头两个话题,爬的页面加起来13个,并没有爬列表里所有项,要爬所有项的话,改相应那行的代码就好spiderIndex里newarr的值即可,如 var newarr = Array.from(lists) 。剩下的,就交给时间吧...建议睡个觉享受生活,起来说不定就好了。也有另一种可能,大眼怪(新浪)看你请求太多,暂时把你IP拒了。

    最后奉上github库 代码和数据文件都在这

    写在最后

    马上就要大四,学到现在,h5,小程序,vue,react,node,java都写过,设计模式、函数式编程、懒加载杂七杂八之类啥的平时逮到啥学啥。其实也挺开心走了编程,一步一步实现也感觉不错,秋招我应该也会去找实习,在这问问大佬们去实习前还有没有啥要注意的,第一次准备有点无从下手的感觉。

    有啥错误请务必指出来互相交流学习,毕竟我还菜嘛,如果方便的话,能留个赞么,谢谢啦。


以上所述就是小编给大家介绍的《爬虫爬不到数据?试试puppeteer(Node.js)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Essential ActionScript 3.0

Essential ActionScript 3.0

Colin Moock / Adobe Dev Library / June 22, 2007 / $34.64

ActionScript 3.0 is a huge upgrade to Flash's programming language. The enhancements to ActionScript's performance, feature set, ease of use, cleanliness, and sophistication are considerable. Essentia......一起来看看 《Essential ActionScript 3.0》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

URL 编码/解码
URL 编码/解码

URL 编码/解码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具