AWS Textractを使用したS3アップロードトリガーの自動テキスト抽出

  この記事では、AWS SAMを使用してS3バケットにファイルがアップロードされた際に自動的にAWS Textractを利用してテキストを抽出するLambda関数を作成する方法を説明します。
このテンプレートは以下のリソースを作成します。

S3バケット: ファイルのアップロードをトリガーするための入力バケットです。
バケット名は{AccountId}-{Region}-input-bucketという形式で生成されます。

Lambda関数: S3バケットにファイルがアップロードされるとトリガーされる関数です。
この関数はPython 3.11で動作し、Textractを使用してアップロードされたファイルからテキストを抽出します。

IAMロール: Lambda関数が適切な権限を持つためのロールです。
このロールには、Lambdaの基本実行権限、S3へのフルアクセス権限、Textractへのフルアクセス権限が含まれます。

このテンプレートの構成と実装方法を解説し、最後に、実際に動作確認を行い、S3バケットにファイルをアップロードしてTextractによるテキスト抽出が成功することを確認します。

### AWS SAM をインストール
```ターミナル
> pip install aws-sam-cli 
```
### SAM プロジェクトの作成
```
> sam init
Which template source would you like to use?
        1 - AWS Quick Start Templates

Choose an AWS Quick Start application template
        1 - Hello World Example

Use the most popular runtime and package type? (Python and zip) [y/N]: y

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: N

Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: N  

Would you like to set Structured Logging in JSON format on your Lambda functions?  [y/N]: N

Project name [sam-app]: 好きなプロジェクト名
``` 

### Lambda関数のコードを修正
S3バケットにアップロードしたファイルを textract.detect_document_text に渡して結果を extracted フォルダ内に出力します。
```Python:hello_world/app.py
# S3にファイルがアップロードされたのを検知してTextractを実行する
import boto3
import json


def lambda_handler(event, context):
    s3 = boto3.client("s3")
    textract = boto3.client("textract")

    for record in event["Records"]:
        bucket = record["s3"]["bucket"]["name"]
        key = record["s3"]["object"]["key"]

        print(f"record: {record}")
        print(f"bucket: {bucket}")
        print(f"key: {key}")

        if "/" not in key:
            response = textract.detect_document_text(
                Document={"S3Object": {"Bucket": bucket, "Name": key}}
            )

            textracted_text = ""
            for item in response["Blocks"]:
                if item["BlockType"] == "LINE":
                    textracted_text += item["Text"] + "\n"

            output_key = f"extracted/{key.split('/')[-1]}.txt"
            print(f"output_key: {output_key}")
            s3.put_object(Bucket=bucket, Key=output_key, Body=textracted_text)

```

template.yamlファイル修正
Role: !GetAtt LambdaExecutionRole.Arn で S3 と Textract の権限を Lambdaに設定します。
```yaml:template.yaml
# S3にファイルがアップロードされたのを検知してLambdaからTextractを実行する
AWSTemplateFormatVersion: '2010-09-09'  # テンプレートのバージョンを指定
Transform: 'AWS::Serverless-2016-10-31'  # AWS SAM を使用するための変換ルール
Description:  'S3 upload trigger for AWS Textract text extraction' # テンプレートの説明文を指定

Resources:
  MyBucket:
    Type: 'AWS::S3::Bucket'  # S3 バケットのリソースタイプを指定
    Properties:
      BucketName: !Sub '${AWS::AccountId}-${AWS::Region}-input-bucket'  # バケット名を指定

  S3EventProcessorFunction:
    Type: 'AWS::Serverless::Function'  # サーバーレスの Lambda 関数を指定
    Properties:
      Handler: app.lambda_handler  # Lambda 関数のハンドラ名を指定
      Runtime: python3.11  # Lambda 関数の実行ランタイムを指定
      Role: !GetAtt LambdaExecutionRole.Arn  # Lambda 関数の実行ロールを参照
      CodeUri: hello_world/  # Lambda 関数のコードが配置されているディレクトリを指定
      Timeout: 30  # Lambda 関数のタイムアウトを指定
      Events:
        S3Event:
          Type: S3  # S3 イベントトリガーを指定
          Properties:
            Bucket: !Ref MyBucket  # トリガーする S3 バケットを参照
            Events: s3:ObjectCreated:*  # オブジェクトが作成されたときにトリガーするイベントを指定

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
        - arn:aws:iam::aws:policy/AmazonTextractFullAccess
```

### ビルド実行
```:ターミナル
> sam build
```
以下のファイルができればOK
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517045/50166936-803a-165a-dd5c-1b44ef2953bd.png)

### デプロイ実行
デプロイの注意点として、Textract は日本のリージョンには対応していない為、今回はバージニア北部リージョンを選択して実行しています。
```:ターミナル
> sam deploy --guided
...
AWS Region [us-east-1]: us-east-1
...
```

### S3へファイルのアップロード
問題なくデプロイが完了したらS3へファイルをアップロードして動作確認を行います。
日本語はうまく変換できない為、今回はコードのスクリーンショットを使います。
![test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517045/cfbc7cb6-14db-9a4f-18d1-f13c9aa4d981.png)

バケットが2つできていると思いますが、[us-east-1-input-bucket] とついている方にアップロードしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517045/2d71ac9c-b05c-2117-a5c5-3c60374907c3.png)

少し待ってからオブジェクトの更新を行ってください。
extracted/ フォルダが自動で作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517045/c0f38746-354a-9bb0-bd2b-aa0a572b926e.png)

中にテキストファイルが作成されているので、ダウンロードして開いてみます。
```text
import boto3
import json
Codeium: Refactor | Explain I Generate Docstring IX
def lambda_handler (event, context):
s3 = boto3.client("s3")
textract = boto3.client("textract")
-
for record in event["Records"]:
bucket = record["s3"]["bucket"]["name"]
key = record["s3"]["object"]["key"]
'
response = textract.detect_document_text(
I
Document={"S30bject": {"Bucket": bucket, "Name": key}}
textracted_text="
for item in response["Blocks"]
ifitem["BlockType"] == "LINE":
|
textracted_text +=item["Text"] + "\n"
output_key = f"extracted/{key.split('/")[-1]}.txt"
s3.put_object(Bucket=bucket,Key=output_key,Body=textracted_text)
```
一部縦線等が変換されてしまいましたが、ほぼ同じ内容が出力されました。

### ログの確認
最後にCloudWatchでログの確認を行います。
ロググループの中に/aws/lambda/~が作成されているので中を確認すると、先ほどダウンロードしたファイル名が確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3517045/7e29ef3c-cbab-96d8-dd0b-f9edef0679a9.png)
エラーが出た場合も同様にログを確認してください。
※ログが出力されていない場合は、S3のputイベントに対しLambdaが動作していないので、template.yaml の設定を確認してください。

### まとめ
以前の記事で紹介したS3へのアップロードイベントに対してLambda関数を実行させる方法は、非常に便利であると改めて感じました。
Lambda関数やtemplate.yamlの一部を変更するだけで、さまざまな処理を柔軟に実行できることは大きな利点で、多くのユースケースに簡単に対応できると思います。

https://qiita.com/kutinasi_hobby/items/456271f643dc625abf5b