AWS上にサーバレスな汎用クローラを展開するぞ。

サーバレスな汎用スクレイパーを作った。 - あのにのに

前回はAPIGatewayとLambdaで、指定したURLの指定した位置にあるデータを抜き出すAPIを作った。

今回はサイト内探索をするようなシステムをAWS上に構築しようと思う

注意:クローラは用法用量を守って、相手方のサイトに迷惑がかからないように十分な配慮を徹底しましょう。

今回作るもの

f:id:anoChick:20161211213223p:plain

こんな感じの構成をイメージしてる。

DynamoDBの1レコードがサイトへの1リクエストに常に対応するものとし、内部リンクのURLを新たにDynamoDBのテーブルに追加していく。

得られたデータは対応レコードに格納される。

URLがテーブルに追加されると、DynamoDB Streamsに流され、Lambdaで実行される。

というような繰り返し。

DynamoDBのデータスキーム

1.Request先URL :target_url

例) https://example.com/items/1

2.巡回対象サイトのrootURL :root_url

例) https://example.com/

3.対象データのセレクタ :selector

例) .score

4.対象データのスクレイピング結果 :result

例) 4.2

f:id:anoChick:20161211214901p:plain

プライマリキーをtarget_urlに設定。

f:id:anoChick:20161211215707p:plain

ストリームに流れるのは"新しいイメージ"のみで良いはず。

SERVERLESS FRAMEWORKの用意

SERVERLESS FRAMEWORKが最高すぎる - あのにのに SERVERLESS FRAMEWORKを使う。 DynemoDBへのレコード追加時に、Lambdaに渡されるJSONのサンプルがほしいので一旦デプロイして動かしてみる。

functions:
  spide:
    handler: handler.spide
    events:
      - stream:
          arn: ####
          batchSize: 1
          startingPosition: LATEST

とりあえずデータを取得(スクレイピング

console.log(event);

ってやるとCloudWatchの方に吐き出されるので、そのデータをevent.jsonに入れてローカルで動かしてみる。

'use strict';
var client = require('cheerio-httpcli');

module.exports.spide = (event, context, callback) => {
  var url = event.Records[0].dynamodb.NewImage.target_url['S'];
  var selector = event.Records[0].dynamodb.NewImage.selector['S'];
  client.fetch(url, {}, function(err, $, res) {
    console.log($(selector).text());
  });
};

これで目的のデータは得られた。

結果をDynamoDBに格納

DynamoDBに入れておく。 DynamoDBを操作するために

GitHub - noradaiko/vogels: DynamoDB data mapper for node.js

をいれる。

var vogels = require('vogels'),
  Joi = require('joi');
var BasicSpider = vogels.define('BasicSpider', {
  hashKey: 'target_url',
  timestamps: true,
  schema: {
    target_url: Joi.string(),
    root_url: Joi.string(),
    selector: Joi.string(),
    result: Joi.string(),
  },
  tableName: 'BasicSpider'
});
BasicSpider.update({
  target_url: url,
  result: $(selector).text()
}, function(err, acc) {
  console.log('update account', acc);
});

でresultが追加されるはず。 ただ、今の状態だとUPDATEでもリクエストが飛んでしまうため、レコード追加時のみスクレイピング処理が走るようにする。

  if (!newImage.target_url || !newImage.selector || !newImage.root_url ||
    newImage.result || newImage.updatedAt)
    return;

一旦デプロイして動かしてみる。

f:id:anoChick:20161211233459p:plain

レコードを追加してちょっと待つと...

f:id:anoChick:20161211233520p:plain

入った!!!

DynamoDBに対象URLと対象要素のセレクタを入れると、resultにデータを保存する仕組み

が出来たことにななる。

これはこれでなんか良さそう。

内部リンクを巡る

過去に訪問したページ → UPDATE処理になる為実行されない。(無限ループはない)

対象データが存在しないページ → resultのないレコードとして残る。

とりあえず内部リンクを拾うって訪問したことのないページをレコード追加する。

$('a').each(function(i, e) {
  var link = ($(e).attr('href') || '').replace('rootURL','');
  if (link[0] == '/') {
    var target_url = rootURL + link;
    BasicSpider
      .query(
        target_url)
      .exec(function(err, acc) {

        if (acc.Count) return;

        BasicSpider.create({
          target_url: target_url,
          root_url: rootURL,
          selector: selector
        }, function(err, post) {
          console.log("create:" + target_url);
        });
      });
  }
});

後は諸々調整、timeoutとかmemorysizeとか。

実行

f:id:anoChick:20161212015117p:plain

取れてる。

ただ並列数がどうなるのかが気になる。 あまり同時に叩きすぎると対象サイトに迷惑がかかるのでコントロールしたい。

つづく