NodeJS爬虫框架apify使用手册

11,581 阅读4分钟

apify是一个基于NodeJS的爬虫框架,它集成了puppeteercheerio等常应用于爬虫服务的NodeJS库,致力于填补复杂场景下的web页面爬虫服务上的功能空白,如通用的爬虫任务入口、爬虫任务的错误捕获和重试、爬虫任务队列/列表、爬虫内部状态监控以及代理池等。

提供范式化的爬虫api。

开发者可以专注于目标爬取逻辑的编写,同时通过设置handleFailedRequestFunction来编写爬虫任务失败的处理逻辑。

const Apify = require('apify');

Apify.main(async () => {
    // 初始化爬虫列表
    const requestList = new Apify.RequestList({
        sources: [
            { url: 'http://www.google.com/' },
            { url: 'http://www.example.com/' },
            { url: 'http://www.bing.com/' },
            { url: 'http://www.wikipedia.com/' },
        ],
    });
    await requestList.initialize();
    // 初始化爬虫
    const crawler = new Apify.BasicCrawler({
        // 并发爬取限制
        minConcurrency: 10,
        maxConcurrency: 50,
        // 单个任务重试次数
        maxRequestRetries: 1,
        // Increase the timeout for processing of each page.
        handlePageTimeoutSecs: 60,
        // 爬取任务列表
        requestList,
        // 页面处理函数
        handleRequestFunction: async ({ request, response }) => {
            // 目标页面爬取逻辑
        },
        handleFailedRequestFunction: async ({ request }) => {
            // 重试n次之后的爬取错误处理逻辑
        },
    });
    // 启动爬虫
    await crawler.run();
});

基于puppeteercheerio的扩展爬虫

CheerioCrawlerPuppeteerCrawler都于集成于BasicCrawler,分别提供了两种库特有页面html处理功能。

const crawler = new Apify.CheerioCrawler({
  // 爬虫列表
  requestList,
  // - html: 页面html字符串
  // - $: 包含目标html的cheerio对象
  handlePageFunction: async ({ request, html, $ }) => {
      console.log(`Processing ${request.url}...`);
      const title = $('title').text();
      const h1texts = [];
      $('h1').each((index, el) => {
          h1texts.push({
              text: $(el).text(),
          });
      });
  },
});

PuppeteerCrawler爬虫中,apify封装了puppeteer创建销毁browserpage的过程,开发者只需要专注于目标页面抓取逻辑的代码编写。

const crawler = new Apify.PuppeteerCrawler({
  // 爬虫任务对列
  requestQueue,
  // puppeteer浏览器启动参数配置
  launchPuppeteerOptions: {
  },
  handlePageFunction: async ({ request, page }) => {
      // page来自于puppeteer context创建,可以使用所有puppeteer相关的api
      const pageFunction = $posts => {
          const data = []
          $posts.forEach($post => {
              data.push({
                  title: $post.querySelector('.title a').innerText,
                  rank: $post.querySelector('.rank').innerText,
                  href: $post.querySelector('.title a').href,
              });
          });
          return data;
      };
      const data = await page.$$eval('.athing', pageFunction);
  },
});

爬虫内部状态监控

apify内部实现了SystemStatus类,以定时轮询的方式定时查询爬虫服务的内部状态信息,如内存、cpu、eventloop等。当爬虫设置了并发数配置(minConcurrencymaxConcurrency),apify将通过当前SystemStatus按一定比例对爬取并发数进行动态缩放。当前系统状态不足于满足当前爬虫任务的执行时,apify会暂定来自于requestListrequestQueue的爬虫任务,直到SystemStatus恢复正常。通过设置autoscaledPoolOptions参数可以动态管理爬虫任务的生命周期。

{
  runTaskFunction: async () => {
  },
  isTaskReadyFunction: async () => {
  },
  isFinishedFunction: async () => {
  },
}

通过AutoscaledPool实例动态控制任务队列/列表的执行。

    handlePageFunction: async ({ autoscaledPool }) => {
        autoscaledPool
            .run() ⇒ Promise                // 启动
            .abort() ⇒ Promise              // 停止
            .pause([timeoutSecs]) ⇒ Promise // 暂停
            .resume()                       // 重启
    }

内置代理池

在爬虫服务中,为了规避目标网站的反爬机制,添加代理以隐藏浏览器来源,往往需要通过编写复杂的逻辑给请求设置不同的代理IP。apify内置了代理池功能,通过proxyUrls参数传入ip地址的数组。apify内部将自动维护一个代理池,随机分配代理给每一个爬虫任务。

本地数据存储

apify基于fs模块实现了DataSetKeyValueStore两种数据结构用于数据的本地存储。

DataSet
// 写入一个行数据到默认的本地存储
await Apify.pushData({ col1: 123, col2: 'val2' });

// 打开一个DateSet
const dataset = await Apify.openDataset('some-name');

// 写入多行数据
await dataset.pushData([{ foo: 'bar2', col2: 'val2' }, { col3: 123 }]);
KeyValueStore
// 写入一个键值对到默认的keyValueStore.
await Apify.setValue('OUTPUT', { myResult: 123 });

// 获取
const store = await Apify.openKeyValueStore('some-name');

// Write a record. JavaScript object is automatically converted to JSON,
// strings and binary buffers are stored as they are
await store.setValue('some-key', { foo: 'bar' });

// Read a record. Note that JSON is automatically parsed to a JavaScript object,
// text data returned as a string and other data is returned as binary buffer
const value = await store.getValue('some-key');

// Drop (delete) the store
await store.drop();

丰富的工具函数

Apify.utils.log (日志分级)
// 支持日志分级
log.setLevel(log.LEVELS.ERROR);
log.debug('Debug message'); 
log.info('Info message'); 
log.error('Error message'); 
Apify.utils.puppeteer

puppeteer常用的工具函数:

Apify.utils.puppeteer 
.addInterceptRequestHandler ⇒ Promise                        // 页面请求拦截处理
.gotoExtended ⇒ Promise<Response>                            // page跳转前置处理
.infiniteScroll ⇒ Promise                                    // 页面滚动
.saveSnapshot ⇒ Promise                                      // 页面快照
.injectFile(page, filePath, [options]) ⇒ Promise             // 页面植入脚本
.injectJQuery(page) ⇒ Promise                                // 页面植入jquery
.injectUnderscore(page) ⇒ Promise                            // 页面植入undeerscore
.blockRequests(page, [options]) ⇒ Promise                    // 顾虑页面请求
.cacheResponses(page, cache, responseUrlRules) ⇒ Promise     // 缓存页面响应
.compileScript(scriptString, context) ⇒ function             // 在page上下文执行额外脚本

apify解决了NodeJS爬虫服务中的很多痛点,使用apify编写爬虫程序,能很大程度上的减少代码量,开发者能够专注于编写目标页面的爬取逻辑。除了以上介绍的优点外,apify仍然存在一些不足,如不支持动态requestQueue,手动切换代理。不过目前所支持的功能,已经可以满足大部分业务常景了。

参考资料