Amazon S3 Select で扱う JSON データの形式と Type 指定について

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに こんばんは、菅野です。
先日のブログエントリーで、Amazon S3 Select を使って CSV ファイルを取得してみました。
今回は JSON データを対象にしてみたいと思います。

データが JSON の場合に指定するパラメータ

データが CSV の場合には存在しなかったパラメータとして「InputSerialization.JSON.Type」が必要となります。

Type:"Lines"

  • 1行が1レコードになっているタイプのデータを扱います
  • このタイプのデータを「JSON Lines」と呼びます(http://jsonlines.org/
{"id":1,"name":"りんご","price":90}
{"id":2,"name":"みかん","price":30}
{"id":3,"name":"柿","price":60}
{"id":4,"name":"キウイ","price":70}
{"id":5,"name":"梨","price":120}

Type:"Document"

  • JSON Lines 以外の JSON を扱う時はこちらを指定します
  • 以下のようにトップレベルがオブジェクトになっている JSON ファイルを扱う事ができます
{
  "Records": [
    {"id":1,"name":"りんご","price":90},
    {"id":2,"name":"みかん","price":30},
    {"id":3,"name":"柿","price":60},
    {"id":4,"name":"キウイ","price":70},
    {"id":5,"name":"梨","price":120}
  ]
}

トップレベルが配列になっている JSON ファイルについて

  • 以下のような JSON ファイルはどちらのタイプを使っても結果が空となります(2018年4月27日現在)
[
  {"id":1,"name":"りんご","price":90},
  {"id":2,"name":"みかん","price":30},
  {"id":3,"name":"柿","price":60},
  {"id":4,"name":"キウイ","price":70},
  {"id":5,"name":"梨","price":120}
]

どちらのタイプを使うべきか(2018年4月27日現在)

「JSON Lines」を対象とし、Type:"Lines" で使いましょう。
JSON Lines 形式以外のデータに対して Type:"Document" でアクセスすると、以下のように全体で1レコードとして扱われますので where による絞り込みができません。

{"Records":[{"id":1,"name":"りんご","price":90},{"id":2,"name":"みかん","price":30},{"id":3,"name":"柿","price":60},{"id":4,"name":"キウイ","price":70},{"id":5,"name":"梨","price":120}]}

また、Type:"Lines" だと CSV 形式での出力も可能です。

取得用のスクリプト

準備については先日のブログエントリーを読んでください。
今回は「JSON Lines」に対して S3 Select を実行する python スクリプトを用意しました。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import boto3
s3 = boto3.client( 's3', 'ap-northeast-1' )


response_s3select = s3.select_object_content(
    Bucket  = 'sugano-test-2',
    Key     = 'test-data/price-list-1.json',
    ExpressionType  = 'SQL',
    Expression      = 'Select * from S3Object',
#  Expression      = 'Select s.id, s.name, s.price from S3Object s',
#  Expression      = 'Select count(*) as data_count from S3Object s',
#  Expression      = 'Select s.id, s.name, s.price from S3Object s where cast(s.id as int) > 3',
#  Expression      = 'Select sum(cast(s.price as int)) as price_sum from S3Object s',
#  Expression      = 'Select max(cast(s.price as int)) as price_max, cast(avg(cast(s.price as int)) as int) as price_ave, min(cast(s.price as int)) as price_min from S3Object s',
    InputSerialization  = {
        'CompressionType': 'NONE',
        'JSON'    : {
            'Type'    : 'Lines'
        }
    },
    OutputSerialization = {
        'CSV' : {
            'RecordDelimiter' : '\n',
            'FieldDelimiter'  : ','
        }
    }
#  OutputSerialization = {
#      'JSON'    : {
#          'RecordDelimiter' : '\n'
#      }
#  }
)


for event in response_s3select[ 'Payload' ]:
    if 'Records' in event:
        records = event[ 'Records' ][ 'Payload' ].decode( 'utf-8' )
        print( records )

結果(CSV出力)

1,りんご,90
2,みかん,30
3,柿,60
4,キウイ,70
5,梨,120

JSON で出力する場合

ソースコードの中に JSON 形式で出力するための OutputSerialization もコメントアウトで埋めてあります。
以下のように CSV 用と切り替えてみましょう。

#  OutputSerialization = {
#      'CSV' : {
#          'RecordDelimiter' : '\n',
#          'FieldDelimiter'  : ','
#      }
#  }
    OutputSerialization = {
        'JSON'    : {
            'RecordDelimiter' : '\n'
        }
    }

結果は以下のようになります。

{"id":1,"name":"りんご","price":90}
{"id":2,"name":"みかん","price":30}
{"id":3,"name":"柿","price":60}
{"id":4,"name":"キウイ","price":70}
{"id":5,"name":"梨","price":120}

他の SQL 文も試してみる

  • Select count(*) as data_count from S3Object s
{"data_count":5}

 

  • Select s.id, s.name, s.price from S3Object s where cast(s.id as int) > 3
{"id":4,"name":"キウイ","price":70}
{"id":5,"name":"梨","price":120}

 

  • Select sum(cast(s.price as int)) as price_sum from S3Object s
{"price_sum":370}

 

  • Select max(cast(s.price as int)) as price_max, cast(avg(cast(s.price as int)) as int) as price_ave, min(cast(s.price as int)) as price_min from S3Object s
{"price_max":120,"price_ave":74,"price_min":30}

 

  • 以下の SQL 文のように平均を int に cast していない場合は指数表記となるようです
  • Select max(cast(s.price as int)) as price_max, avg(cast(s.price as int)) as price_ave, min(cast(s.price as int)) as price_min from S3Object s
{"price_max":120,"price_ave":74e0,"price_min":30}

まとめ

JSON Lines 形式のデータも、CSV 形式と同様に S3 Select で扱える事が確認できました。
Type:"Document" の場合の動作は 2018年4月27日現在だと残念な結果となりましたが、いつか改善されると思いますので待ちましょう。

さいごに

いかがでしたでしょうか。
少しのソース修正だけで JSON Lines 形式のデータに対応することができました。
本エントリーがを見た皆様が S3 Select を活用するための参考になれば幸いです。

参考ページ

以下のページを参考にさせていただきました。
ありがとうございました。
Amazon S3 Select および Amazon Glacier Select の SQL リファレンス - Amazon Simple Storage Service