話題のAWS Lambda Advent Calendar 2014の14日目です。クロスポストで、クローラー/スクレイピング Advent Calendar 2014の14日目でもあります。
re:Inventで発表されて以来、注目のLambdaです。サーバを用意しなくても、バッチを直接実行できるとあって、ユースケースを考えるだけで夢が広がります。今年はクローラー本を出したこともあって、Lambdaで作るクローラー/スクレイピングをテーマにします。
クローラー/スクレイピングとは?
Webクローラーは、Webサイトを巡回してデータを取得するプログラムです。スクレイピングは、取得したデータから目的の情報を抜き出すことを指します。一般的には、クローラーの中に、スクレイピングの機能を包含していることが多いです。また、特定のページだけ取得してデータを抜き出すことを、スクレイピングと呼ぶことが多いです。
クローラーの機能
クローラーの機能は、ざっくり分類すると下記の4つです。
今回は、2と3の部分をLambdaで実施します。
1. 巡回先を決定する
2. ダウンロード
3. 情報を抜き出す
4. (データを保存する)
Lambdaクローラーのモデル
Lambdaクローラーのモデルとしては、次のような形になります。
LambdaCrowlerで、指定されたURLをNode.jsのhttpモジュールを使ってダウンロードします。ダウンロードをしたファイルをS3に保存します。保存先のS3にEventの設定をしておき、parseHtmlのLambdaファンクションを呼び出すようにします。呼び出されたparseHtmlは、イベント情報から呼び元のs3ファイルを取得します。ファイル取得後に、cheerioというスクレイピング用のモジュールを利用して所定の情報を抜き出します。今回は、Yahoo!Financeから株価情報を取得する例とします。
Lambdaクローラーの実装
まずLambdaCrowlerの実装です。諸般の事情により、取得先のURLと保存するファイル名はリテラルで記述しています。
console.log('Loading event');
var aws = require('aws-sdk');
var s3 = new aws.S3({apiVersion: '2006-03-01'});
var http = require ('http');
exports.handler = function(event, context) {
var bucket = 'lambda-crawler';
var key = 'test';
var body;
http.get("http://stocks.finance.yahoo.co.jp/stocks/history/?code=9984.T", function(res) {
console.log("Got response: " + res.statusCode);
res.on("data", function(chunk) {
console.log('chunk: ');
body += chunk;
});
res.on('end', function(res) {
console.log('end')
putObject(context, bucket, key ,body);
});
}).on('error', function(e) {
context.done('error', e);
});
function putObject(context, bucket, key ,body) {
var params = {Bucket: bucket, Key: key, Body: body};
console.log('s3 putObject' + params);
s3.putObject(params, function(err, data) {
if (err) {
console.log('put error' + err);
context.done(null, err);
} else {
console.log("Successfully uploaded data to myBucket/myKey");
context.done(null, "done");
}
});
}
};
注意点としては、http.requestがcallbackを利用している点です。リクエストの終了イベントを拾い、その後にS3に保存するように記述しています。実行すると次のようなログがでます。
START RequestId: 2b379992-8300-11e4-88d6-37d91d3def55
2014-12-13T19:42:26.841Z 2b379992-8300-11e4-88d6-37d91d3def55 Got response: 200
2014-12-13T19:42:26.842Z 2b379992-8300-11e4-88d6-37d91d3def55 chunk:
2014-12-13T19:42:26.842Z 2b379992-8300-11e4-88d6-37d91d3def55 chunk:
2014-12-13T19:42:27.022Z 2b379992-8300-11e4-88d6-37d91d3def55 chunk:
2014-12-13T19:42:27.022Z 2b379992-8300-11e4-88d6-37d91d3def55 chunk:
2014-12-13T19:42:27.203Z 2b379992-8300-11e4-88d6-37d91d3def55 chunk:
2014-12-13T19:42:27.384Z 2b379992-8300-11e4-88d6-37d91d3def55 chunk:
2014-12-13T19:42:27.384Z 2b379992-8300-11e4-88d6-37d91d3def55 end
2014-12-13T19:42:27.384Z 2b379992-8300-11e4-88d6-37d91d3def55 s3 putObject[object Object]
2014-12-13T19:42:27.574Z 2b379992-8300-11e4-88d6-37d91d3def55 Successfully uploaded data to myBucket/myKey
END RequestId: 2b379992-8300-11e4-88d6-37d91d3def55
REPORT RequestId: 2b379992-8300-11e4-88d6-37d91d3def55 Duration: 2558.28 ms Billed Duration: 2600 ms Memory Size: 128 MB Max Memory Used: 18 MB
次にparseHtmlの実装です。非標準のモジュールであるcheerioを利用しています。
console.log('Loading event');
var cheerio = require('cheerio');
var aws = require('aws-sdk');
var s3 = new aws.S3({apiVersion: '2006-03-01'});
var http = require ('http');
exports.handler = function(event, context) {
var record = event.Records[0];
var bucket = record.s3.bucket.name;
var key = record.s3.object.key;
console.log('record:' + record);
console.log('bucket:' + bucket);
console.log('key:' + key);
getObject(context, bucket, key)
function getObject(context, bucket, key) {
console.log('s3 getObject');
s3.getObject({Bucket:bucket, Key:key},
function(err,data) {
if (err) {
console.log('error getting object ' + key + ' from bucket ' + bucket +
'. Make sure they exist and your bucket is in the same region as this function.');
context.done('error','error getting file'+err);
}
else {
var contentEncoding = data.ContentEncoding;
var contentBody = data.Body.toString(contentEncoding);
parseHtml(context, contentBody);
}
}
);
}
function parseHtml(context, body) {
console.log('parseHtml');
var $ = cheerio.load(body);
var title = $("title").text();
var stockPrice = $('td[class=stoksPrice]').text();
console.log('stockPrice:'+stockPrice);
}
};
まずイベントからバケット名とファイル名を取得します。その情報を元に、S3からファイルを取得します。ファイルを取得したら、cheerioでスクレイピングです。cheerioの使い方は割愛しますが、タグ名+クラス名で、要素を指定しています。
尚、非標準のモジュールを利用しているので、ソースと一緒にモジュールもzipで固めてアップロードする必要があります。
├── node_modules
│ ├── aws-sdk
│ └── cheerio
├parseHtml.js
上記のような構造のディレクトリをまとめてzip化します。
zip -r code.zip parseHtml.js node_modules/
アップロードしたファイルの中で、どのファイルを実行するかは「File name」で指定します。
実行すると、次のようなログが出てきます。stockPriceの部分が、今回取得しようとした情報です。
START RequestId: 1d834bf2-8300-11e4-800c-bd34141de807
2014-12-13T19:42:05.492Z 1d834bf2-8300-11e4-800c-bd34141de807 record:[object Object]
2014-12-13T19:42:05.492Z 1d834bf2-8300-11e4-800c-bd34141de807 bucket:lambda-crawler
2014-12-13T19:42:05.492Z 1d834bf2-8300-11e4-800c-bd34141de807 key:test
2014-12-13T19:42:05.493Z 1d834bf2-8300-11e4-800c-bd34141de807 s3 getObject
2014-12-13T19:42:06.174Z 1d834bf2-8300-11e4-800c-bd34141de807 parseHtml
2014-12-13T19:42:07.173Z 1d834bf2-8300-11e4-800c-bd34141de807 stockPrice:7,336
Process exited before completing request
END RequestId: 1d834bf2-8300-11e4-800c-bd34141de807
REPORT RequestId: 1d834bf2-8300-11e4-800c-bd34141de807 Duration: 1920.75 ms Billed Duration: 2000 ms Memory Size: 128 MB Max Memory Used: 27 MB
ハマリポイント
成果物としてのソースは非常に簡単ですが、かなりハマりました。ハマったポイントは、以下の通りです。
- そもそもNode.js知らん
- S3 Event Notificationのロール登録時に、ロール中にprincipalのserviceにs3.amazonaws.comが必要
課題
一応完成しましたが、1つ課題があります。最初のLambdaファンクションを呼び出すのを誰にするかです。呼び出し方として、s3 event notification,Kinesis stream, DyanmoDB stream、もしくはプログラムから呼び出すなどの手段あります。そもそも今回Lambdaでクローラーを作ろうと思った目的は、サーバレスで作りたかったからです。どの呼び出し元を選んだとしても、イベント起こす為の起点が必要です。その辺りどうするかがポイントになりますね。
クローラーという用途で考えると、何らかの方法で取得対象をDynamoDBに登録して、Lambdaを呼び出して結果を記載するという方法があっているかもしれません。もしくは、呼び元の小さなインスタンスを用意して、処理自体をLambdaの潤沢なリソースを使うというのも考えられます。いずれにせよ、Lambdaを使うには、信頼性の高いスケジュール・ジョブの実行主体が必要になります。この辺りもサービス化して貰えるとありがたいですね。