[AWS Lambda]S3にテキストファイルが置かれたらイベントドリブンに形態素解析(kuromoji)



## はじめに

[lambdaハンズオン][lambda-hanson]を参考にlambdaを触ってみました。

プログラム書きたくて社会人になったのに気がついたらインフラエンジニアになってた人がアプリケーションもやれるのがAWSの良いところ！


## Lambda in JAVA!

いろんなjavaアプリケーションをイベント駆動で実行できるようになりますね！(知ったかぶり

[AWS Lambda Update – Run Java Code in Response to Events | AWS Official Blog][lambda-update]


[Amazon Web Services ブログ: 【AWS発表】AWS Lambdaのアップデート、イベントに応じてJavaコードを実行](http://aws.typepad.com/aws_japan/2015/06/aws-lambda-update-run-java-code-in-response-to-events.html)



## なにを試そう…

パッと思いついた **kuromoji.jar** をLambda Functionの中で使ってみよう。

外部からS3 Bucketにテキストファイルが置かれたら自動で形態素解析して結果を別のbucketに出力する。
（それ何に使えるの？とか、cloudsearch使ったら？とか言わせない。）

## やってみた

### 構成を考える


![lambda-grant.png](https://qiita-image-store.s3.amazonaws.com/0/17518/2c809ccf-a179-cdf2-79c6-49ca0df24396.png "lambda-grant.png")

※lambda:InvokeFunctionについては後述。特別設定は不要になったようです。
 
ここまで書いてみて必要そうな知識

* Java SDKで、S3からObjectをgetする
* Java SDKで、getしたObjectの文章を読込む
* Kuromojiで形態素解析する
* 結果をJava SDKでputする
* Lambda Functionの書き方



### 本当にkuromoji.jar動かせんの？（Lambdaにアップロードできんの？）を調べる

容量制限の話。

http://docs.aws.amazon.com/lambda/latest/dg/limits.html

Input	|Limit
---|---
Compressed function .zip file	|50 MB
Uncompressed function .zip file	|250 MB
Invoke request body JSON	|6 MB


圧縮して50MBまで。

kuromoji-0.7.7.jarが11M。いけそう。

### どうやって作るのかを調べる（開発環境）

http://docs.aws.amazon.com/lambda/latest/dg/lambda-java-how-to-create-deployment-package.html


* Creating a .jar Deployment Package Using Maven without any IDE (Java)
* Creating a .jar Deployment Package Using Maven and Eclipse IDE (Java)
* Creating a .zip Deployment Package (Java)
* Authoring Lambda Functions Using Eclipse IDE and AWS SDK Plugin (Java)


２番目のMaven and Eclipse IDEでやってみようかと思います。

### 開発環境を準備する

#### 作業PC

OS X Yosemite 10.10.3
homebrew, brew-cask インストール済み

#### Eclipseインストール

```bash:eclipse
brew cask install eclipse-platform
```



## Mavenプロジェクトの作成

http://docs.aws.amazon.com/lambda/latest/dg/java-create-jar-pkg-maven-and-eclipse.html

を参考。

![lambda002.png](https://qiita-image-store.s3.amazonaws.com/0/17518/255c93f0-1734-36f3-9ba2-6b3b3e6dc9ea.png "lambda002.png")



## Kuromojiを追加

http://www.atilika.org/
Maven artifact repository For ease of use with Maven or Ivy
を参照。



![lambda003.png](https://qiita-image-store.s3.amazonaws.com/0/17518/0abbf54d-adf1-fad6-ea4c-c29418be6f6a.png "lambda003.png")

![lambda004.png](https://qiita-image-store.s3.amazonaws.com/0/17518/56adf675-10b6-1e1c-5c20-4afcaeba10e3.png "lambda004.png")

![kuromoji-maven0001.png](https://qiita-image-store.s3.amazonaws.com/0/17518/8ec26222-0b24-9843-e611-fb9e0ad4353b.png "kuromoji-maven0001.png")

## aws toolkitインストール

### インストール

以下を参考にインストール

http://docs.aws.amazon.com/ja_jp/AWSToolkitEclipse/latest/GettingStartedGuide/tke_setup_install.html

### credential 設定

http://docs.aws.amazon.com/ja_jp/AWSToolkitEclipse/latest/GettingStartedGuide/tke_setup_creds.html

**Toolkit for Eclipse に AWS アクセスキーを追加するには**を参照。

## 構成図に合わせてAWSの設定をする

### S3 Bucket(ファイル受取用)

東京リージョンに作ります
~/.aws/configにregion設定済み。

```bash:makebucket
$ aws s3 mb s3://imura81gt-text-bucket
```

### S3 Bucket(処理結果出力用)

形態素解析した結果はこのバケットに入れることにします。

```bash:makebucket
$ aws s3 mb s3://imura81gt-text-bucket-toknized
```

入力先bucketと出力先bucketを一緒にするとたぶん無限lambdaして大変なことになる。
-> EventSourceの設定でPrefix/Sufixの設定ができるようになったので、無限lambdaを防ぐ方法はある。

 

## IAM role

### exec Role

[lambda-hanson資料][lambda-hanson]の

* Lambda exec Roleの作成

を行う。 `lambda_s3_exec_role` とする。


### Invoke Role

Event Sourceの設定の際にInvocation Roleの設定が必要でしたが、無くなってました。

## coding

こんな感じでやりました。
（普段bashくらいしか書かないので、名前の付け方がおかしかったりしたら教えて下さい）

![lambda1001.png](https://qiita-image-store.s3.amazonaws.com/0/17518/7d07c63b-8594-f8a6-7004-d792c6e035c8.png "lambda1001.png")

![lambda1002.png](https://qiita-image-store.s3.amazonaws.com/0/17518/6e54e800-fadd-e166-cafa-17a2677f0fc1.png "lambda1002.png")

![lambda1003.png](https://qiita-image-store.s3.amazonaws.com/0/17518/d5dfe1a9-b83a-f06b-ddd4-11f35f9aac10.png "lambda1003.png")


```java
package net.outofbounds8.kuromoji.lambda;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.S3Event;
import com.amazonaws.services.lambda.runtime.LambdaLogger;

import org.atilika.kuromoji.Token;
import org.atilika.kuromoji.Tokenizer;

import com.amazonaws.auth.ClasspathPropertiesFileCredentialsProvider;


import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.UUID;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.Bucket;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.ListObjectsRequest;
import com.amazonaws.services.s3.model.ObjectListing;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectSummary;

import org.atilika.kuromoji.Token;
import org.atilika.kuromoji.Tokenizer;



// sample http://docs.aws.amazon.com/lambda/latest/dg/java-wt-s3-log-event-data.html

import com.amazonaws.services.s3.event.S3EventNotification.S3EventNotificationRecord;

public class LambdaFunctionHandler implements RequestHandler<S3Event, Object> {

    @Override
    public Object handleRequest(S3Event input, Context context) {
        context.getLogger().log("Input: " + input);
        context.getLogger().log("Input: " + input);
        
        LambdaLogger lambdaLogger = context.getLogger();
        
        S3EventNotificationRecord record = input.getRecords().get(0);
        lambdaLogger.log(record.getEventName()); //イベント名
        lambdaLogger.log(record.getS3().getBucket().getName()); //バケット名
        lambdaLogger.log(record.getS3().getObject().getKey()); //オブジェクトのキー（オブジェクト名）

        String bucketName = record.getS3().getBucket().getName();
        String key = record.getS3().getObject().getKey();
        S3Object object = null;
        
        String tld = "-toknized";

        StringBuilder buf = new StringBuilder();
        buf.append(bucketName);
        buf.append(tld);
        String outputBucketName = buf.toString();
                		       
        //AmazonS3 s3 = new AmazonS3Client(new ClasspathPropertiesFileCredentialsProvider());
        AmazonS3 s3 = new AmazonS3Client();
        
        try {
            object = s3.getObject(new GetObjectRequest(bucketName, key));
            
            lambdaLogger.log("Content-Type: "  + object.getObjectMetadata().getContentType());
            lambdaLogger.log("object.getObjectContent:     " + object.getObjectContent());
            String text = getTextInputStream(object.getObjectContent());

            lambdaLogger.log(text); //イベント名

            File file = File.createTempFile("tokenize-", ".txt");
            file.deleteOnExit();

            Writer writer = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");

            Tokenizer tokenizer = Tokenizer.builder().build();
            
            for (Token token : tokenizer.tokenize(text)) {
                System.out.println(token.getSurfaceForm() + "\t" + token.getAllFeatures());
                writer.write(token.getSurfaceForm() + "\t" + token.getAllFeatures() + "\n");
            }           

            writer.close();

            s3.putObject(new PutObjectRequest(outputBucketName, "toknized-" + key, file));
            
            
        } catch (AmazonServiceException ase) {
            System.out.println("Caught an AmazonServiceException, which means your request made it "
                    + "to Amazon S3, but was rejected with an error response for some reason.");
            System.out.println("Error Message:    " + ase.getMessage());
            System.out.println("HTTP Status Code: " + ase.getStatusCode());
            System.out.println("AWS Error Code:   " + ase.getErrorCode());
            System.out.println("Error Type:       " + ase.getErrorType());
            System.out.println("Request ID:       " + ase.getRequestId());
        } catch (AmazonClientException ace) {
            System.out.println("Caught an AmazonClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with S3, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message: " + ace.getMessage());
        } catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
         
        
        
        return record.getS3().getObject().getKey();
        
        //http://dev.classmethod.jp/cloud/aws/aws-sdk-for-java-s3-create-download-application/
        	
        // TODO: implement your handler
        //return null;
    }

    private static String getTextInputStream(InputStream input) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(input, "UTF-8"));
        String text = "";
        String line = "";

        while (true) {
            line = reader.readLine();
            if (line == null) break;
            text = text + line;
        }
        return text;
    }

}
```



## Eclipseからlambda functionをアップロード


### upload

※東京リージョンが出る前に試した人はEclipseの
[help] - [check for update] からupdateを行うこと。
（デプロイする際に、古いものをしようしていると、東京リージョンが表示されないため）


![kuromoji-upload0002.png](https://qiita-image-store.s3.amazonaws.com/0/17518/7bfa6d31-b00e-2cce-75d3-7c6b1ca02fbe.png "kuromoji-upload0002.png")

![lambda005.png](https://qiita-image-store.s3.amazonaws.com/0/17518/e0b312e6-11f9-fae8-cea5-9b819a9a5c5f.png "lambda005.png")

![lambda006.png](https://qiita-image-store.s3.amazonaws.com/0/17518/75e5c512-a06c-5175-5d38-ed04f226216e.png "lambda006.png")

![lambda007.png](https://qiita-image-store.s3.amazonaws.com/0/17518/055cf812-6284-0822-0e8d-fca8474b64ee.png "lambda007.png")

![lambda008-1.png](https://qiita-image-store.s3.amazonaws.com/0/17518/86a37785-5373-5fce-8a5e-356ece9247dc.png "lambda008-1.png")

![lambda008.png](https://qiita-image-store.s3.amazonaws.com/0/17518/afa56cb0-eb5e-5c2c-f384-dee61ba8de3e.png "lambda008.png")

![lambda009.png](https://qiita-image-store.s3.amazonaws.com/0/17518/3a4fc378-ddde-d94d-44de-2a66e4d7a52c.png "lambda009.png")

ManagementConsole 開く
![lambda010.png](https://qiita-image-store.s3.amazonaws.com/0/17518/f7353e06-7f96-b2c3-1f00-a90ab535db8e.png "lambda010.png")
![lambda011.png](https://qiita-image-store.s3.amazonaws.com/0/17518/130c5033-cc93-9189-4f1e-c73048585e17.png "lambda011.png")
![lambda012.png](https://qiita-image-store.s3.amazonaws.com/0/17518/cbeb36c4-4243-fdf8-8de9-39db473173b7.png "lambda012.png")


### 実行！

送るファイル

```text:sumomo.txt
すもももももももものうち。
つまり、スモモは桃のうちであり
桃ももちろん桃のうちである、
という事である。
実際のところ、スモモはバラ科サクラ属であり、別名プラムという。
なので桃ではない。
このような文章のことを言葉遊びと言う。
```

s3になにもファイルがないのを確認

```
$ aws s3 ls s3://imura81gt-text-bucket/
$ aws s3 ls s3://imura81gt-text-bucket-toknized/
```

アップロード

```
$ aws s3 cp sumomo.txt s3://imura81gt-text-bucket/
upload: ./sumomo.txt to s3://imura81gt-text-bucket/sumomo.txt
$
$ aws s3 ls s3://imura81gt-text-bucket/
2015-08-01 00:58:21        334 sumomo.txt
$
```

形態素解析されたテキストが出力されていることを確認

```
$ aws s3 ls s3://imura81gt-text-bucket-toknized/
2015-08-01 00:58:33       3590 toknized-sumomo.txt
$
```

中身を表示してみる

```
$ aws s3 cp s3://imura81gt-text-bucket-toknized/toknized-sumomo.txt -
すもも	名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もも	名詞,一般,*,*,*,*,もも,モモ,モモ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
。	記号,句点,*,*,*,*,。,。,。
つまり	接続詞,*,*,*,*,*,つまり,ツマリ,ツマリ
、	記号,読点,*,*,*,*,、,、,、
スモモ	名詞,一般,*,*,*,*,スモモ,スモモ,スモモ
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
桃	名詞,一般,*,*,*,*,桃,モモ,モモ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
で	助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ
あり	助動詞,*,*,*,五段・ラ行アル,連用形,ある,アリ,アリ
桃	名詞,一般,*,*,*,*,桃,モモ,モモ
も	助詞,係助詞,*,*,*,*,も,モ,モ
もちろん	副詞,一般,*,*,*,*,もちろん,モチロン,モチロン
桃	名詞,一般,*,*,*,*,桃,モモ,モモ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
うち	名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
で	助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ
ある	助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル
、	記号,読点,*,*,*,*,、,、,、
という	助詞,格助詞,連語,*,*,*,という,トイウ,トユウ
事	名詞,非自立,一般,*,*,*,事,コト,コト
で	助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ
ある	助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル
。	記号,句点,*,*,*,*,。,。,。
実際	副詞,助詞類接続,*,*,*,*,実際,ジッサイ,ジッサイ
の	助詞,連体化,*,*,*,*,の,ノ,ノ
ところ	名詞,非自立,副詞可能,*,*,*,ところ,トコロ,トコロ
、	記号,読点,*,*,*,*,、,、,、
スモモ	名詞,一般,*,*,*,*,スモモ,スモモ,スモモ
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
バラ	名詞,一般,*,*,*,*,バラ,バラ,バラ
科	名詞,接尾,一般,*,*,*,科,カ,カ
サクラ	名詞,一般,*,*,*,*,サクラ,サクラ,サクラ
属	名詞,サ変接続,*,*,*,*,属,ゾク,ゾク
で	助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ
あり	助動詞,*,*,*,五段・ラ行アル,連用形,ある,アリ,アリ
、	記号,読点,*,*,*,*,、,、,、
別名	名詞,一般,*,*,*,*,別名,ベツメイ,ベツメイ
プラム	名詞,一般,*,*,*,*,プラム,プラム,プラム
と	助詞,格助詞,引用,*,*,*,と,ト,ト
いう	動詞,自立,*,*,五段・ワ行促音便,基本形,いう,イウ,イウ
。	記号,句点,*,*,*,*,。,。,。
なので	接続詞,*,*,*,*,*,なので,ナノデ,ナノデ
桃	名詞,一般,*,*,*,*,桃,モモ,モモ
で	助詞,格助詞,一般,*,*,*,で,デ,デ
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
ない	助動詞,*,*,*,特殊・ナイ,基本形,ない,ナイ,ナイ
。	記号,句点,*,*,*,*,。,。,。
この	連体詞,*,*,*,*,*,この,コノ,コノ
よう	名詞,非自立,助動詞語幹,*,*,*,よう,ヨウ,ヨー
な	助動詞,*,*,*,特殊・ダ,体言接続,だ,ナ,ナ
文章	名詞,一般,*,*,*,*,文章,ブンショウ,ブンショー
の	助詞,連体化,*,*,*,*,の,ノ,ノ
こと	名詞,非自立,一般,*,*,*,こと,コト,コト
を	助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
言葉	名詞,一般,*,*,*,*,言葉,コトバ,コトバ
遊び	名詞,一般,*,*,*,*,遊び,アソビ,アソビ
と	助詞,格助詞,引用,*,*,*,と,ト,ト
言う	動詞,自立,*,*,五段・ワ行促音便,基本形,言う,イウ,イウ
。	記号,句点,*,*,*,*,。,。,。
$
```

### まとめ

Amazon API Gateway と組み合わせれば
形態素解析APIが作れそう！



[lambda-hanson]:http://www.slideshare.net/AmazonWebServicesJapan/aws-lambda-2tier

[lambda-update]:https://aws.amazon.com/jp/blogs/aws/aws-lambda-update-run-java-code-in-response-to-events/