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: {
      width: 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

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

posted @ 2020-03-19 13:31  木石心  阅读(567)  评论(0编辑  收藏  举报