プログラマでありたい

おっさんになっても、プログラマでありつづけたい

Lambdaで作るクローラー/スクレイピング


 AWS Lambda Advent Calendar 201414 Advent Calendar 201414

 re:InventLambdaLambda

クローラー/スクレイピングとは?



 Webクローラーは、Webサイトを巡回してデータを取得するプログラムです。スクレイピングは、取得したデータから目的の情報を抜き出すことを指します。一般的には、クローラーの中に、スクレイピングの機能を包含していることが多いです。また、特定のページだけ取得してデータを抜き出すことを、スクレイピングと呼ぶことが多いです。

クローラーの機能



 クローラーの機能は、ざっくり分類すると下記の4つです。
今回は、2と3の部分をLambdaで実施します。

1. 巡回先を決定する
2. ダウンロード
3. 情報を抜き出す
4. (データを保存する)

Lambdaクローラーのモデル




 Lambda
f:id:dkfj:20141214045906p:plain
 LambdaCrowlerURLNode.jshttp使S3S3EventparseHtmlLambdaparseHtmls3cheerioYahoo!Finance


Lambdaクローラーの実装




 LambdaCrowlerURL
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.requestcallbackS3
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 

 parseHtmlcheerio
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);
    } 
};

 S3cheeriocheerio使
 zip
├── node_modules
│     ├── aws-sdk
│     └── cheerio
├parseHtml.js

 zip
zip -r code.zip parseHtml.js node_modules/

File name
f:id:dkfj:20141214051542p:plain
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	

ハマリポイント



 成果物としてのソースは非常に簡単ですが、かなりハマりました。ハマったポイントは、以下の通りです。

  1. そもそもNode.js知らん
  2. S3 Event Notificationのロール登録時に、ロール中にprincipalのserviceにs3.amazonaws.comが必要

課題




 1Lambdas3 event notification,Kinesis stream, DyanmoDB streamLambda
 DynamoDBLambdaLambda使Lambda使

まとめ



 駆け足でしたが、やはりLambdaは魅力的なサービスです。クローラーを作ってると、IPアドレスとリソースの確保が大変です。もう少し色々考察してみたいと思います。ちなみに、Rubyによるクローラー開発技法という本を書いたのですが、リクエストの中で多いのがPython版とNode.js版を出してとのことです。Lambdaが出たことなので、誰かNode.js版書きませんか?

See Also:
KimonoLabsと今後のサービスのあり方のはなし
プログラミング・レスで5分でサックリWebスクレイピング「kimonolabs」
『Rubyによるクローラー開発技法』を書きました


参照:
AWS Lambda Advent Calendar 2014
クローラー/スクレイピング Advent Calendar 2014
全部俺Advent Calendar

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例