使用Puppeteer实现豆瓣电影详情内容爬虫:进阶篇

3,096 阅读5分钟

Puppeteer 是什么

  • Puppeteer 是 Node.js 工具引擎
  • Puppeteer 提供了一系列 API,通过 Chrome DevTools Protocol 协议控制 Chromium/Chrome 浏览器的行为
  • Puppeteer 默认情况下是以 headless 启动 Chrome 的,也可以通过参数控制启动有界面的 Chrome
  • Puppeteer 默认绑定最新的 Chromium 版本,也可以自己设置不同版本的绑定
  • Puppeteer 让我们不需要了解太多的底层 CDP 协议实现与浏览器的通信

Puppeteer 能做什么

官方称:“Most things that you can do manually in the browser can be done using Puppeteer”,那么具体可以做些什么呢?

  1. 网页截图或者生成 PDF
  2. 爬取 SPA 或 SSR 网站
  3. UI 自动化测试,模拟表单提交,键盘输入,点击等行为
  4. 捕获网站的时间线,帮助诊断性能问题
  5. 创建一个最新的自动化测试环境,使用最新的 js 和最新的 Chrome 浏览器运行测试用例
  6. 测试 Chrome 扩展程序 ...

Puppeteer 常用API

  • Browser: 对应一个浏览器实例,一个 Browser 可以包含多个 BrowserContext
  • BrowserContext: 对应浏览器一个上下文会话,就像我们打开一个普通的 Chrome 之后又打开一个隐身模式的浏览器一样,BrowserContext 具有独立的 Session(cookie 和 cache 独立不共享),一个 BrowserContext 可以包含多个 Page
  • Page:表示一个 Tab 页面,通过 browserContext.newPage()/browser.newPage() 创建,browser.newPage() 创建页面时会使用默认的 BrowserContext,一个 Page 可以包含多个 Frame
  • Frame: 一个框架,每个页面有一个主框架(page.MainFrame()),也可以多个子框架,主要由 iframe 标签创建产生的
  • ExecutionContext: 是 javascript 的执行环境,每一个 Frame 都一个默认的 javascript 执行环境
  • ElementHandle: 对应 DOM 的一个元素节点,通过该该实例可以实现对元素的点击,填写表单等行为,我们可以通过选择器,xPath 等来获取对应的元素
  • JsHandle:对应 DOM 中的 javascript 对象,ElementHandle 继承于 JsHandle,由于我们无法直接操作 DOM 中对象,所以封装成 JsHandle 来实现相关功能
  • CDPSession:可以直接与原生的 CDP 进行通信,通过 session.send 函数直接发消息,通过 session.on 接收消息,可以实现 Puppeteer API 中没有涉及的功能
  • Coverage:获取 JavaScript 和 CSS 代码覆盖率
  • Tracing:抓取性能数据进行分析
  • Response: 页面收到的响应
  • Request: 页面发出的请求

更多请查阅官方文档 github地址

准备工作

安装模块

# 安装puppeteer
npm install puppeteer --save
# puppeteer 需要安装 chromium,可以选择修改 puppeteer 的下载源:
npm config set puppeteer_download_host https://npm.taobao.org/mirrors

# 安装chalk
npm install chalk --save

先思考

我们可以先去观察下这个 页面HTML结构, 爬取这个页面以及每个详情页面我们需要什么

  1. 爬取页面所需要的页面元素
  2. 跟页面的交互(比如分页)
  3. 该怎么样获取多页内容
  4. 怎样跳到详情页面 ...

基本思路

  1. 打开我们要爬取的页面
  2. 获取数据列表以及分页按钮
  3. 循环列表打开每一个item
  4. 在页面中执行js拿到我们想要拿到的数据
  5. 当前页数据抓取完毕点击下一页按钮 ...

爬取豆瓣电影

async index() {
    const { ctx } = this;
    const log = console.log;
    /// 图方便我把常用的数据写在了这里
    const scrape = {
      url: 'https://movie.douban.com/tag/#/',
      click: '.more',
      page: 2,
      itemList: '.list-wp > a',
    };
    /// 启动一个浏览器环境
    const browser = await puppeteer.launch();
    log(chalk.green('服务正常启动'));
    /// 捕获错误
    try {
      /// 打开一个新的页面
      const page = await browser.newPage();
      /// 我们需要监听 request的话 必须要开启
      await page.setRequestInterception(true);
      /// 监听内部的request
      page.on('request', interceptedRequest => {
        if (interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg'))
		  interceptedRequest.abort();
		else
		  interceptedRequest.continue();
      });
      /// 打开一个页面 config 请自行查阅文档
      await page.goto(scrape.url, {
        waitUntil: 'networkidle2',
        timeout: 0,
      });
      
      /// scrape.page  爬取几页内容
      for (let l = 0; l < scrape.page; l++) {
        /// 等待页面元素加载完毕
        await page.waitForSelector(scrape.click);
        if (l > 0) {
          const submit = await page.$(scrape.click);
          if (!submit) {
            log(chalk.red('按钮不存在!'));
            return;
          }
          await submit.click();
          await page.waitFor(2500);
        }
        console.clear();
        /// 格式化进度
        log(chalk.yellow(ctx.helper.formatProgress(ctx, l, scrape.page)));
        /// 获取当前列表
        const itemList = await page.?(scrape.itemList);
        // 开始循环当前列表 我们看到列表一页数据在20条  所以我们从当前页面*20开始循环 这样就不会出现每次重新循环都从第一条数据开始
        for (let i = l * 20; i < itemList.length; i++) {
          const items = await page.?(scrape.itemList);
          const item = items[i];
          /// 获取列表的第i个元素的url
          const url = await page.evaluate(item => {
            return item ? item.href : '';
          }, items[i]);
          if (url) {
            const page2 = await browser.newPage();
            await page2.goto(url, {
              timeout: 0,
            });
            /// 等待一会
            await page2.waitFor(1000);
            
            /// page2.evaluate 使我们可以在页面中执行js方法
            const result = await page2.evaluate(() => {
              const info = document.querySelector('#info');
              const screenwriter = [];
              const starring = [];
              const types = [];
               info.querySelectorAll('.attrs')[1].querySelectorAll('a').forEach(item => {
                screenwriter.push(item.innerText);
              });
              info.querySelectorAll('.attrs')[2].querySelectorAll('a').forEach(item => {
                starring.push(item.innerText);
              });
              info.querySelectorAll('[property="v:genre"]').forEach(item => {
                types.push(item.innerText);
              });
				
              return {
                title: document.querySelector('h1') ? document.querySelector('h1').innerText : '',
                mainpic: document.querySelector('.nbgnbg > img') ? document.querySelector('.nbgnbg > img').src : '',
                director: info.querySelectorAll('.attrs')[0].querySelector('a') ? info.querySelectorAll('.attrs')[0].querySelector('a').innerText : '',
                screenwriter,
                starring,
                types,
                release_date: info.querySelector('[property="v:initialReleaseDate"]') ? info.querySelector('[property="v:initialReleaseDate"]').innerText : '',
                length: info.querySelector('[property="v:runtime"]') ? info.querySelector('[property="v:runtime"]').innerText : '',
                rating_num: document.querySelector('[property="v:average"]') ? document.querySelector('[property="v:average"]').innerText : '',
              };
            });
            
            /// 写入文件
            fs.appendFile('./movie.json', JSON.stringify(result, null, '\t', {
              flag: 'a',
            }), function(err) {
              if (err) {
                throw err;
              }
            });

          }
        }
      }
      await browser.close();
      log(chalk.green('服务正常结束'));
    } catch (error) {
      console.log(error);
      log(chalk.red('服务意外终止'));
      await browser.close();
    }
  }

爬取的内容

{
	"title": "致命女人 第一季 Why Women Kill Season 1 (2019)",
	"mainpic": "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2566967861.webp",
	"director": "大卫·格罗斯曼",
	"screenwriter": [
		"马克·切利",
		"艾莉莎·荣格"
	],
	"starring": [
		"刘玉玲",
		"金妮弗·古德温",
		"柯尔比·豪威尔-巴普蒂斯特",
		"杰克·达文波特",
		"山姆·贾格",
		"里德·斯科特",
		"亚历珊德拉·达达里奥",
		"赛迪·卡尔瓦诺",
		"里奥·霍华德",
		"艾丽莎·科波拉",
		"凯蒂·芬内朗",
		"更多..."
	],
	"types": [
		"剧情",
		"喜剧",
		"犯罪"
	],
	"release_date": "2019-07-21(洛杉矶LGBT电影节)",
	"length": "",
	"rating_num": "9.4"
}

结尾

无事撸一撸,撸的不对勿喷,大家一起学习一起成长 Come on