zoukankan      html  css  js  c++  java
  • puppeteer 学习

    puppeteer 学习

    随着各大发文平台增多,有时就不得不每个平台都需要注册一个账号,进行文章发布,这样才能扩大影响力。而每次进行这种操作,可想而知是否就有些痛苦了。而这次为大家推荐一个node包puppeteer

    puppeteer可以做很多时,基本可以阐述为,我们在浏览器上做什么,改工具便可以作为,因为其可以模拟人在页面上任何操作。这就给了爬虫,登录等一系列操作了。

    技能

    • 截屏
    • 爬虫
    • ...

    今天主要讲到爬虫,没有想到有朝一日,我们可以单纯利用js来进行获取自己想要的数据,而不是常说的 python, 我知道不能单纯依赖于某一个语言,但js确实给了我们接触其他领域的技能。

    简单来说,puppeteer 主要是通过 api 来对页面进行我们想要的操作,比如输入文字,获取想要的信息等,这样只要自己熟悉 api 基本都可以进行操作。而这里与我而言最难的是,如何让程序按照自己的设想一步步执行,每一小点都需要考虑周全。因为此时的代码就是将自己在页面上操作步骤进行分解,每一小点都必须到位。

    示例

    下面是爬取某网站的信息,需要登录后才能获取更多内容

    • 写入cookie(这样可以免登陆,可以跳过很多的坑)
    • 找到对应的元素
    • 获取内容,生成需要的数据
    /*
     * 爬虫
     * @LastEditors: Sinosaurus
     */
    const puppeteer = require('puppeteer')
    const path = require('path')
    const fs = require('fs')
    
    interface result {
      question: String,
      options: Array<String>,
      answer: String,
      analysis: String
    }
    
    function getTitleAndItems (page) {
      return new Promise(async resolve => {
        // 这个才是内容显示的
        const realTitleSelector = '#sub_detail > b'
        await page.waitForSelector(realTitleSelector)
        // question
        const titleSelector = '#sub_detail'
        await page.waitForSelector(titleSelector)
        const question = await page.$eval(titleSelector, el => el.innerText)
    
        const realItemSelector = '#sub_choices .form-check-input'
        await page.waitForSelector(realItemSelector)
    
        // options
        const subItemSelectors = '#sub_choices .alert-secondary'
        await page.waitForSelector(subItemSelectors)
        const options = await page.$$eval(subItemSelectors, items => {
          const textList = items.map(item => {
            return item.innerText
          })
          return textList
        })
    
        // analysis (解析)
        const btn_select = '#container > div:nth-child(2) > button.btn.btn-primary'
        await page.waitForSelector(btn_select)
        await page.click(btn_select, {
          delay: 100
        })
    
        const dialog_select = '#explanation'
        await page.waitForSelector(dialog_select)
        const analysis = await page.$eval(dialog_select, el => {
          const text = el.innerText
          const str = 'TODO:'
          return {
            analysis: text,
            answer: str
          }
        })
        
        const close_dialog_select = '#exampleModal > div > div > div.modal-footer > button'
        await page.waitForSelector(close_dialog_select)
        await page.click(close_dialog_select)
        const result = {question,options, ...analysis}
        resolve(result)
      })
    }
    
    ;(async () => {
      const browser = await puppeteer.launch({
        headless: false,
        // 忽略 https 的错误
        ignoreHTTPSErrors: true,
        slowMo: 50,
        defaultViewport: {
           1440,
          height: 1366
        }
      });
    
      const page = await browser.newPage()
      // setcookie
      // https://github.com/puppeteer/puppeteer/issues/2994#issuecomment-412740938
      const cookie = [{
            "domain": "**mytodo.vip**",
            "hostOnly": true,
            "httpOnly": true,
            "name": "session",
            "path": "/",
            "sameSite": "unspecified",
            "secure": false,
            "session": true,
            "storeId": "0",
            "value": "**",
            "id": 1
        }]
      page.setCookie(...cookie)
      console.log(await '准备前往目的地')
      await page.goto("URL")
      
      // 收集题目的列表
      let questionList = []
      const clickSelects = '#work_area button'
      await page.waitForSelector(clickSelects)
      const btnLength = await page.$$eval(clickSelects, el => el.length)
      console.log(`总共有${btnLength}题目`)
      for (let i = 1; i <= btnLength; i++) {
        const select = `#work_area > button:nth-child(${i})`
        /**
         * 1. 移到可视区域
         * 2. 点击
         */
        // await page.focus(select)
        await page.click(select, {
          delay: 150
        })
        console.log(`第${i}题开始`)
        // 此处需要判断页面内容发生了变化,不然一直重复
        // await page.waitFor(1500)
        if (i > 99) break
        const result = await getTitleAndItems(page) as result
        if (questionList.some(item => item.question === result.question)) {
          console.log('equeal')
          continue
        }
        questionList.push(result)
        console.log(`第${i}题结束`)
      }
      // 写入文件中
      const file = path.join(__dirname, 'aws.json')
      await fs.writeFileSync(file, JSON.stringify(questionList, null, 2), err => {
        if (err) {
          throw new Error(err)
        }
        console.log('ok')
      })
      await page.close()
      await browser.close()
    })()
    

    上面是一段执行代码,不做过多阐述,在这个过程中,并不是 puppeteer 有多难,而是自己如何梳理出想要的逻辑,还便于扩展,这个倒是有些难搞。而且对基本功要求很高,避免这个在不引入其他 库时,更多地是如何写出一个可以考虑周全的方案有些难。因为在爬取的过程中,你无法知道到底有哪些情况,只能一步步尝试,而在这个过程中,才是花费时间的大头。

    下面是我前段时间学习的一些历程

    API

    page

    const puppeteer = require('puppeteer')
    puppeteer.launch().then(async browser => {
      // page
      const page = await browser.newPage()
    })
    

    事件

    可以调用 node原生事件EventEmitter

    • on

        page.on('request', fn)
      

      事件列表
      close, console, dialog, domcontentloaded, error, frameattached, framedetached, framenavigated, load, metrics, pageerror, request, requestfailed, requestfinished, response, workercreated, workerdestroyed

    • once

        page.once('load', () => console.log('page loaded'))
      

      在使用await时,会导致load无法触发,await page.goto(url),因为await已经有了load的效果

    • removeListener 注销事件

        page.removeListener('request', fn)
      

    命名空间

    • coverage
    • keyboard
    • mouse
    • touchsreen
    • tracing
    • goto
    • waitForSelector
    • waitForNavigation({
      // 跳转页面,等待加载完
      waitUntil: 'load'
      })
    • content
    • evaluate
    • evaluate 相当于进入了dom上下文,可以在内部直接进行正常的 dom属性操作
    • click 点击
    • type 输入
    $、$$、$evel、$$evel
      $ => querySelector
      $$ => qyuerSelectorAll
      $evel => selector.$evel(select, node => console.log('拿到当前元素'))
      $$evel => selector.$$evel(select, nodes => console.log('拿到当前元素,这是一个数组'))
    

    参考链接

    如何使用当前浏览器的文件

    鉴于目前各大网页都设置了防爬虫处理

    • 滑块(有一定逻辑,简单滑动还不生效) == csdn
    • 图片滑块(无法知道滑到哪个位置) == 百度
    • 类名不固定(同一个元素,刷新后,id可能会变化)== csdn
    • 滑块千奇百怪
      • 百度 颠倒图片
      • 知乎 找出颠倒的文字
    • 验证码(这种应该可以结合命令行进行处理)
      若是想简单使用,最好是使用手机号登录,再结合命令行,或许是最直接的。奈何目前登录方式各样,有的使用 单点登录(包括第三方),导致页面无法一直锁定

    不得已,转战思路,能否通过本地提前登录,然后在开启自动化时,便已经登录,这样便可以跳过前面一大堆各种验证问题,现在只需要判断是否登录(可以通过上面是否有对应的用户即可)

    使用 userDataDir 时,会跟 {headless: false}相冲突,导致程序卡死,只能去掉,方可正常流转

    依旧不行,看选择的路径吧,原来是我的路径使用错误,利用 chrome://version可以查看到

    args (浏览器)

    project

    谷歌插件 puppeteer recorder

    可以快速生成选择的元素以及操作步骤

  • 相关阅读:
    JS基础
    NodeJs实现他人项目实例
    Node.js在任意目录下使用express命令‘不是内部或外部命令’解决方法
    HTTP基本知识
    RESTful API
    基本概念和方法1
    Node.js--安装express以及创建第一个express项目(windows)
    Node-debug方法
    css3动画划过有一个框
    translate 动画不同时间飞入文字
  • 原文地址:https://www.cnblogs.com/sinosaurus/p/12523784.html
Copyright © 2011-2022 走看看