puppeteer 基本操作

269 min read
// 演示自动访问百度网站并抓取相关搜索关键词
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch(
      {executablePath: './chromium/chrome', headless: false});
  const page = await browser.newPage();
  await page.goto('https://www.baidu.com/');

  await page.focus('#kw'); // 焦点到搜索关键字输入框
  await page.type('Chrome Headless', {delay: 100}); // 输入关键字 Chrome Headless
  await page.click('#su'); // 点击“百度一下”提交按钮

  const waitForElement = page.waitForSelector('#rs > table > tbody > tr:nth-child(3) > th:nth-child(5) > a', {visible:true,timeout: 3000}); // 等待 3 秒或者页面显示完成 注释1
  try {// 此处是可能产生例外的语句
    await waitForElement;    
    var kw = [];  // 注释2
    kw[0] = await page.$eval('#rs > table > tbody > tr:nth-child(1) > th:nth-child(1) > a', el => el.innerHTML); //注释3
    kw[1] = await page.$eval('#rs > table > tbody > tr:nth-child(1) > th:nth-child(3) > a', el => el.innerHTML);
    kw[2] = await page.$eval('#rs > table > tbody > tr:nth-child(1) > th:nth-child(5) > a', el => el.innerHTML);
    kw[3] = await page.$eval('#rs > table > tbody > tr:nth-child(2) > th:nth-child(1) > a', el => el.innerHTML);
    kw[4] = await page.$eval('#rs > table > tbody > tr:nth-child(2) > th:nth-child(3) > a', el => el.innerHTML);
    kw[5] = await page.$eval('#rs > table > tbody > tr:nth-child(2) > th:nth-child(5) > a', el => el.innerHTML);
    kw[6] = await page.$eval('#rs > table > tbody > tr:nth-child(3) > th:nth-child(1) > a', el => el.innerHTML);
    kw[7] = await page.$eval('#rs > table > tbody > tr:nth-child(3) > th:nth-child(3) > a', el => el.innerHTML);
    kw[8] = await page.$eval('#rs > table > tbody > tr:nth-child(3) > th:nth-child(5) > a', el => el.innerHTML);

    console.log('相关搜索关键词:');
    //遍历相关搜索关键词.
    for(var i=0;i<kw.length;i++){
        console.log(kw[i]);  
    }
  } catch (error) {
        // 此处是负责例外处理的语句
    console.log('网页中没有找到相关搜索');
  } finally {
        // 此处是出口语句   
  }

  browser.close(); // 关闭退出。可注释掉此行代码,便于观察最后的结果
})();

延时

await page.waitForTimeout(3000);	

常用的元素函数选择

page.$(selector)    // document.querySelector
page.$$(selector)   // document.querySelectorAll
page.$x(expression)  // xpath

模拟输入

page.mouse
page.keyboard
page.click(selector[, options])        //在被选择元素上模拟点击
page.type(selector, text[, options])    //在被选择的输入框中输入
page.hover(selector)                //模拟鼠标移动到被选择元素上
page.select(selector, ...values)        //在被选择元素上模拟选择select选项
page.tap(selector)                    // 在被选择元素上模拟触摸

等待

page.waitForNavigation(options)
page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])
page.waitForSelector(selector[, options])
page.waitForXPath(xpath[, options])
page.waitForFunction(pageFunction[, options[, ...args]])


其中最常用的是page.waitForNavigation,常用于等待跳转结束,例如点击搜索按钮后,等待跳转至搜索结果页面。

const navigationPromise = page.waitForNavigation();
await page.click('a.my-link'); 
await navigationPromise;

配置等待时间

// Configure the navigation timeout
await page.goto('https://ourcodeworld.com', {
  waitUntil: 'load',
  // Remove the timeout
  timeout: 0
});

信息查看

puppeteer提供了一些查看页面信息的函数,

  1. page.url()
  2. page.content()
  3. page.frames()
  4. page.mainFrame()
  5. page.metrics()
  6. page.target()
  7. page.title()
  8. page.viewport()

请求中断

page.setRequestInterception提供了中断请求的机制,例如,我们可以通过它实现一个无图模式。

await page.setRequestInterception(true);
page.on('request', interceptedRequest => {
    if (interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg'))
        interceptedRequest.abort();
    else
        interceptedRequest.continue();
});
await page.goto('https://example.com');

这里有一个interceptedRequest对象,它提供了三种响应模式:abort、continue和respond。

内容保存

内容保存主要包括保存为pdf和截图

page.pdf(options)
page.screenshot([options])

事件通知

page.on('load',
    async () => {
      console.log('page loading done, start fetch...');
});
close frameattached pageerror
console framedetached request
dialog framenavigated requestfailed
domcontentloaded load requestfinished
error metrics response

执行脚本

执行脚本最常用的函数是page.evaluate,它类似于在控制台中执行指令。

console.log(await page.evaluate('1 + 2'));
var title = await page.evaluate('document.title')

它也可以用来执行写好的node函数,实际上该函数是在浏览器中执行的,但可以像本地函数一样编写,还支持参数传值。

var title = await **page**.evaluate(async (i) => {
  return document.title + ' ' + i;
}, 'hello');

console.log(title);

虽然这node函数不能调试,但仍然是有非常大的好处的,

  1. 不用考虑字符串转义的问题,书写起来非常直接
  2. 脚本在IDE中有高亮显示和智能提示的,写起来更加方便

另外,还有几个其它的执行脚本的函数,应用于不同的场合,也是非常有用的。

  1. page.evaluateHandle(pageFunction, ...args)
  2. page.evaluateOnNewDocument(pageFunction, ...args)
  3. page.$$eval(selector, pageFunction[, ...args])
  4. page.$eval(selector, pageFunction[, ...args])

例如:

const searchValue = await page.$eval('#search', el => el.value);
const preloadHref = await page.$eval('link[rel=preload]', el => el.href);
const html = await page.$eval('.main-container', e => e.outerHTML);
// 引入一些需要用到的库以及一些声明
import * as puppeteer from 'puppeteer' // 引入Puppeteer
import mongo from '../lib/mongoDb' // 需要用到的 mongodb库,用来存取爬取的数据
import chalk from 'chalk' // 一个美化 console 输出的库

const log = console.log // 缩写 console.log
const TOTAL_PAGE = 50 // 定义需要爬取的网页数量,对应页面下部的跳转链接

// 定义要爬去的数据结构
interface IWriteData { 
  link: string // 爬取到的商品详情链接
  picture: string // 爬取到的图片链接
  price: number // 价格,number类型,需要从爬取下来的数据进行转型
  title: string // 爬取到的商品标题
}

// 格式化的进度输出 用来显示当前爬取的进度
function formatProgress (current: number): string { 
  let percent = (current / TOTAL_PAGE) * 100
  let done = ~~(current / TOTAL_PAGE * 40)
  let left = 40 - done
  let str = `当前进度:[${''.padStart(done, '=')}${''.padStart(left, '-')}]   ${percent}%`
  return str
}
// 进入代码的主逻辑
async function main() {
  // 首先通过Puppeteer启动一个浏览器环境
  const browser = await puppeteer.launch()
  log(chalk.green('服务正常启动'))
  // 使用 try catch 捕获异步中的错误进行统一的错误处理
  try {
    // 打开一个新的页面
    const page = await browser.newPage()
    // 监听页面内部的console消息
    page.on('console', msg => {
      if (typeof msg === 'object') {
        console.dir(msg)
      } else {
        log(chalk.blue(msg))
      }
    })

    // 打开我们刚刚看见的淘宝页面
    await page.goto('https://s.taobao.com/search?q=gtx1080&imgfile=&js=1&stats_click=search_radio_all%3A1&initiative_id=staobaoz_20180416&ie=utf8')
    log(chalk.yellow('页面初次加载完毕'))

    // 使用一个 for await 循环,不能一个时间打开多个网络请求,这样容易因为内存过大而挂掉
    for (let i = 1; i <= TOTAL_PAGE; i++) {
      // 找到分页的输入框以及跳转按钮
      const pageInput = await page.$(`.J_Input[type='number']`)
      const submit = await page.$('.J_Submit')
      // 模拟输入要跳转的页数
      await pageInput.type('' + i)
      // 模拟点击跳转
      await submit.click()
      // 等待页面加载完毕,这里设置的是固定的时间间隔,之前使用过page.waitForNavigation(),但是因为等待的时间过久导致报错(Puppeteer默认的请求超时是30s,可以修改),因为这个页面总有一些不需要的资源要加载,而我的网络最近日了狗,会导致超时,因此我设定等待2.5s就够了
      await page.waitFor(2500)

      // 清除当前的控制台信息
      console.clear()
      // 打印当前的爬取进度
      log(chalk.yellow(formatProgress(i)))
      log(chalk.yellow('页面数据加载完毕'))

      // 处理数据,这个函数的实现在下面
      await handleData()
      // 一个页面爬取完毕以后稍微歇歇,不然太快淘宝会把你当成机器人弹出验证码(虽然我们本来就是机器人)
      await page.waitFor(2500)
    }

    // 所有的数据爬取完毕后关闭浏览器
    await browser.close()
    log(chalk.green('服务正常结束'))

    // 这是一个在内部声明的函数,之所以在内部声明而不是外部,是因为在内部可以获取相关的上下文信息,如果在外部声明我还要传入 page 这个对象
    async function handleData() {
      // 现在我们进入浏览器内部搞些事情,通过page.evaluate方法,该方法的参数是一个函数,这个函数将会在页面内部运行,这个函数的返回的数据将会以Promise的形式返回到外部 
      const list = await page.evaluate(() => {
        
        // 先声明一个用于存储爬取数据的数组
        const writeDataList: IWriteData[] = []

        // 获取到所有的商品元素
        let itemList = document.querySelectorAll('.item.J_MouserOnverReq')
        // 遍历每一个元素,整理需要爬取的数据
        for (let item of itemList) {
          // 首先声明一个爬取的数据结构
          let writeData: IWriteData = {
            picture: undefined,
            link: undefined,
            title: undefined,
            price: undefined
          }

          // 找到商品图片的地址
          let img = item.querySelector('img')
          writeData.picture = img.src

          // 找到商品的链接
          let link: HTMLAnchorElement = item.querySelector('.pic-link.J_ClickStat.J_ItemPicA')
          writeData.link = link.href

          // 找到商品的价格,默认是string类型 通过~~转换为整数number类型
          let price = item.querySelector('strong')
          writeData.price = ~~price.innerText
          
          // 找到商品的标题,淘宝的商品标题有高亮效果,里面有很多的span标签,不过一样可以通过innerText获取文本信息
          let title: HTMLAnchorElement = item.querySelector('.title>a')
  
          writeData.title = title.innerText

          // 将这个标签页的数据push进刚才声明的结果数组
          writeDataList.push(writeData)
        }
        // 当前页面所有的返回给外部环境
        return writeDataList
        
      })
      // 得到数据以后写入到mongodb
      const result = await mongo.insertMany('GTX1080', list)

      log(chalk.yellow('写入数据库完毕'))
    }

  } catch (error) {
    // 出现任何错误,打印错误消息并且关闭浏览器
    console.log(error)
    log(chalk.red('服务意外终止'))
    await browser.close()
  } finally {
    // 最后要退出进程
    process.exit(0)
  }
}

源码 https://github.com/MrTreasure/Algorithm/tree/master/src/Puppeteer

请求头设置

    const browser = await puppeteer.launch({
        executablePath: '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome',
        headless: true,
        args: ['--no-sandbox']
    });
    const page = await browser.newPage();
    await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36');

设置Cookies

page.setCookie(options)
async function addCookies(cookies_str: string, page: puppeteer.Page, domain: string){
    let cookies = cookies_str.split(';').map(pair=>{
        let name = pair.trim().slice(0,pair.trim().indexOf('='))
        let value = pair.trim().slice(pair.trim().indexOf('=')+1)
        return {name,value,domain}
    });
    await Promise.all(cookies.map(pair=>{
        return page.setCookie(pair)
    }))
}

puppeteer 知乎

for(let answer of answerlist){ //这个answerlist就是page.$$('选择器')获得的元素列表
        let data = await page.evaluate((answer)=>{
            //这个一个html转码方法  如: 
//&lt;script&gt;alert(2);&lt;/script&gt =>   <script>alert(2);</script>
            function HTMLDecode(text:string) { 
                let temp = document.createElement("div"); 
                temp.innerHTML = text; 
                let output = temp.innerText || temp.textContent; 
                return output; 
            } 
            //以下就是通过选择器,获得各种元素,然后得到我们想要的数据
           //如作者,回答内容,标题,多少个赞 等等
            let comment = answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.answer-actions > div > a.toggle-comment');
            //这里我们点击评论按钮,response事件发生,我们的函数执行,收集数据
            comment.click();

            let title = answer.querySelector('.zm-item-title > a').innerHTML;

            let author = answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.answer-head > div > span.name');
            let author_link = '';
            if(!author){
                author = answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.answer-head > div > span > span > a.author-link').innerHTML;
                author_link = 'https://www.zhihu.com'+answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.answer-head > div > span > span > a.author-link').getAttribute('href');
            }else{
                author = author.innerHTML;
            }
       
            let content = answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.zm-item-rich-text > textarea.content').innerHTML;
 
            content = HTMLDecode(content);
            let like = answer.querySelector('div.zm-item-fav > div.zm-item-answer > div.zm-item-vote > a').innerHTML;

            return { title, author, content, like ,author_link}
           
        },answer)
        await timeout(2000);
        if(Obj.data.length == 1){
            await timeout(2000);
        }
        console.log(Obj.data)
        await timeout(1000)
        data.content = handler_content(data.content)
        data.comments = Obj.data;
        //这里我们就完成了一个回答的收集了
        datas.push(data);
        Obj.data = [];
        console.log(data.title)
    }
    //保存到数据库
    for(let data of datas){
        let comments = data.comments;
        delete data.comments;
        let res = await db.insert( {table: 'zhihu', data} );
        if(res  ){
            console.log(res)
            for(let c of comments){
                c.zhihu = res.insertId;
                await db.insert({table: 'zhihu_comment', data: c})
            }
        }
    }