1. 本稿の概要
背景とゴール
AWSなどを触っているとjsonフォーマットやそれをPythonで扱う際のデータ構造=dict型を操作することが増えてきます。
なんとなく分かるんだけど本当に 目的の設定値を参照できてるんかこれ? ってのが多いなと感じています。
もう一度、ステップを踏んでdict型の『読み方』学んでいきたいので同時にアウトプットします。
pythonスクリプトでdictデータ構造の定義および目的のkey:valueの参照をしていきます。単純なものから始め、徐々に入れ子構造を含む複雑な形を解析していきます。
本稿では一般的なdict型のデータ操作やメソッド等は説明せず、あくまで知らぬ誰かが定義したdictのデータの読み解き方に着目しています。dict型の設計や操作のあるべき論などは触れないのであしからず。
2. dict型の読み解き方
Lv.0 dict型とは
そもそもdict型ってどこでどう定義されてんの?って話から始めるとPython.orgで各種タイプのドキュメントがあります。まずはこちらを一読することをおススメします。
dict型は超ざっくりいうと、「「key」と「value」の組み合わせ」を複数内包できるデータ構造です。例えば、商品の情報としてkeyに果物の品目、valueにそれぞれの値段を入れて「Apple:200」「Banana:150」「Cherry:780」などの関係性をマッピングして様々な取り扱いが可能となります。
Lv.1 シンプルなdict型
例えば目次として第一章が10ページ、第二章が25ページから始まるという情報をpage_dictという変数に入れると以下のpythonのように定義できます。chapter_oneのページを教えてと参照するとそのページ値が返ってきます。
page_dict = {"chapter_one": 10, "chapter_two": 25}
page_dict["chapter_one"]
page_dict["chapter_two"]
list(page_dict)) #keyのlistが出力される
"""実行結果
py dict_lv1.py
10
25
["chapter_one", "chapter_two"] # このlistからkeyを指定する
"""
さて、ここでdictは定義は{ }で参照のときは[ ]を使っているのはなぜだっけ?という疑問があります。こちらもクラス内部や深い仕組みの話はさておき、以下のようなざっくり理解で進めます。
-
dict: { }でkey:valueのペアを入れていくデータ構造。要素には特定のKey値でアクセスする。
-
list: [ ]で要素を列挙ししていくデータ構造。要素には0から始まるインデックスでアクセスする。
dictを定義した時点でKey値のlist([Key1, Key2,...])が自動で生成されています。
そのためdictからあるkeyのvalueの情報を取り出したいとなったら以下のような仕組みとなります。
- dictの"keyのlist"から目的のkeyを指定する
- 目的のKeyが参照され、その対応するvalue値が得られる
Lv.2 「valueがlist」のdict型
ここからdict型のvalueがlistになっているケースを見ていきましょう。ある一つのKeyに対して、複数の要素の集合をlist形式で対応付け格納するということになります。
例えば、Keyとしてチーム名"sales"と"developers"を用意し、それぞれに適当な所属者名のlistを渡して参照してみたいと思います。
team_dict = {"sales": ["sato", "kobayashi", "suzuki"], "developers":["tanaka", "kato", "nakamura", "yamada"]}
team_dict["developers"]
type(team_dict)
type(team_dict["sales"])
team_dict["sales"][0]
team_dict["developers"][3]
{"sales": "[sato, kobayashi, suzuki]", "developers":"[tanaka, kato, nakamura, yamada]"}と書かないようにしよう。
"""実行結果
>>> team_dict = {"sales": ["sato", "kobayashi", "suzuki"], "developers":["tanaka", "kato", "nakamura", "yamada"]}
>>> team_dict["sales"]
['sato', 'kobayashi', 'suzuki']
>>> type(team_dict)
<class 'dict'>
>>> type(team_dict["sales"])
<class 'list'>
>>> team_dict["sales"][0]
'sato'
>>> team_dict["developers"][3]
'yamada'
"""
以上のように、dict内のvalueに持たせたlistも参照や多重list形式でのインデックス指定での値の取得が可能です。
Lv.3 「valueがdict」のdict型
続いてdict型のvalueがdictで入れ子構造になっているケースを見ていきましょう。とはいえlistの入れ子とほぼ同じで、インデックス参照がキー値参照になったと思えば大丈夫です。
同じく例として、Keyとしてチーム名"sales"と"developers"を用意し、それぞれの入れ子のdictで各所属者の情報を持たせて参照してみたいと思います。各チームKeyのvalue値としてさらに「Keyが社員名でvalueがその社員の役職」のdictとしてみます。ちなみにこうなってくるとワンライナーで書くと分かりづらいため、ヒトが見る場合はインデントを入れて階層構造で記述します。
employee_dict = {
"sales": {
"sato": "staff" ,
"kobayashi": "chief",
"suzuki": "manager"
},
"developers":{
"tanaka": "staff",
"kato": "supervisor",
"nakamura": "chief",
"yamada":"maneger"
}
}
# 1liner => employee_dict = {"sales": {"sato": "staff" , "kobayashi": "chief", "suzuki": "manager"}, "developers":{"tanaka": "staff", "kato": "supervisor", "nakamura": "chief", "yamada":"maneger"}}
employee_dict["sales"]
type(employee_dict)
type(employee_dict["sales"])
employee_dict["sales"]["suzuki"]
employee_dict["developers"]["kato"]
>>> employee_dict = {"sales": {"sato": "staff" , "kobayashi": "chief", "suzuki": "manager"}, "developers":{"tanaka": "staff", "kato": "supervisor", "nakamura": "chief", "yamada":"maneger"}}
>>> employee_dict["sales"]
{'sato': 'staff', 'kobayashi': 'chief', 'suzuki': 'manager'}
>>> type(employee_dict)
<class 'dict'>
>>> type(employee_dict["sales"])
<class 'dict'>
>>> employee_dict["sales"]["suzuki"]
'manager'
>>> employee_dict["developers"]["kato"]
'supervisor'
Lv.4 Mix多重入れ子・本当の半構造データ
Lv.3まではデータの階層や種類がシンプルで直感的に納得できるものが多かったと思います。半構造化データといいつつ、かなり構造を決めてしまっているデータでした。Lv.4ではdictやlistが入り乱れていたり、単位や種類が異なるいろいろなデータがはいっていたりする半構造化データのパターンを見てみましょう。
今回の例では実践的な例として、aws-cliで実際に出力されうるフォーマットを見てみます。AWSのVPCの一覧を表示するdescribe-vpcsでは以下のようなリターンが得られます。要素数やデータ長が大きくなりデータタイプもさまざまありますが、こういったものから好きなデータを得られるようになりましょう。
aws_ec2_describe_vpcs = {
"Vpcs": [ ★(1)
{ ★(2-1)
"CidrBlock": "30.1.0.0/16",
"DhcpOptionsId": "dopt-19edf471",
"State": "available",
"VpcId": "vpc-0e9801d129EXAMPLE",
"OwnerId": "111122223333",
"InstanceTenancy": "default",
"CidrBlockAssociationSet": [
{
"AssociationId": "vpc-cidr-assoc-062c64cfafEXAMPLE",
"CidrBlock": "30.1.0.0/16",
"CidrBlockState": {
"State": "associated"
}
}
],
"IsDefault": false,
"Tags": [
{
"Key": "Name",
"Value": "Not Shared"
}
]
}, /★(2-1)
{ ★(2-2)
"CidrBlock": "10.0.0.0/16",
"DhcpOptionsId": "dopt-19edf471",
"State": "available",
"VpcId": "vpc-06e4ab6c6cEXAMPLE",
"OwnerId": "222222222222",
"InstanceTenancy": "default",
"CidrBlockAssociationSet": [
{
"AssociationId": "vpc-cidr-assoc-00b17b4eddEXAMPLE",
"CidrBlock": "10.0.0.0/16",
"CidrBlockState": {
"State": "associated"
}
}
],
"IsDefault": false,
"Tags": [
{
"Key": "Name",
"Value": "Shared VPC"
}
]
} /★(2-2)
] /★(1)
}
それではpythonでデータ参照をする前にデータの構造解析をしてみます。文字ベースになるので読み飛ばして先にpythonでのデータ取得実例をご覧になるでもOKです。
Lv4. 第一階層
このdictはまず第一階層で「key=Vpcs,value=VPCリスト」が来ています(★(1))。"describe_vpcs"なので第一階層のdict構造としてはVpcsのみで他のKey値はありません。
他の例えばdescribe-subnetsで得られる"Subnets"等のフォーマットも同様の形式なので、必要に応じ第一階層でオブジェクト単位のリスト結合をしやすい形にしているのでしょう。
Lv4. 第二階層
第二階層では"Vpcs"のlistの要素として、2つのvpcのdictが存在します( ★(2-1)と★(2-2))。Vpcs = [ {Vpc1のdict} , {Vpc2のdict} ] というような構造です。dict中の変数の"VpcId"で言えばvpc-0e9801d129EXAMPLE"と"vpc-06e4ab6c6cEXAMPLE"の2つのvpcの情報が得られたということです。
vpcが増えていけばこのリストの要素内のdictが3つ4つと増えていきます。
二つのvpcのdictは同様の変数で構成されています。必須変数は両者にあるはずですが、任意の変数次第では必ずしも構造がすべて一致するとは限りません。プログラム内で片方のdictにあるキー値を指定した後に、別のdictで同じキー値参照したのにこちらで存在しない場合エラーになってしまいます。それが半構造化データの良いところでも悪いところでもあります。
Lv.4 第三階層以降
第三階層以降は各vpcに投入された具体的な設定値が列挙されています。VPCの状態やID系など一意に決めなければいけない設定はそのままシンプルなkey-valueで格納されています。
一方で"CidrBlockAssociationSet"や"Tags"は入れ子でさらにlistをvalueにもち、そのlist内の要素をdictとして各種詳細を格納できる形になっています。これらの変数はあるvpcに対して複数設定されうるため、まずlistで自由に要素数を更新できる形にして、具体的な内容をdictで入れていく仕組みです。
余談ですがtuppleだと更新できないのでmutableなlistを使っているってことでしょうか。
それでは前述の構造解析を踏まえてpythonで参照してみると以下のようになります。
>>> aws_ec2_describe_vpcs = {'Vpcs': [{'CidrBlock': '30.1.0.0/16', 'DhcpOptionsId': 'dopt-19edf471', 'State': 'available', 'VpcId': 'vpc-0e9801d129EXAMPLE', 'OwnerId': '111122223333', 'InstanceTenancy': 'default', 'CidrBlockAssociationSet': [{'AssociationId': 'vpc-cidr-assoc-062c64cfafEXAMPLE', 'CidrBlock': '30.1.0.0/16', 'CidrBlockState': {'State': 'associated'}}], 'IsDefault': 'false', 'Tags': [{'Key': 'Name', 'Value': 'Not Shared'}]}, {'CidrBlock': '10.0.0.0/16', 'DhcpOptionsId': 'dopt-19edf471', 'State': 'available', 'VpcId': 'vpc-06e4ab6c6cEXAMPLE', 'OwnerId': '222222222222', 'InstanceTenancy': 'default', 'CidrBlockAssociationSet': [{'AssociationId': 'vpc-cidr-assoc-00b17b4eddEXAMPLE', 'CidrBlock': '10.0.0.0/16', 'CidrBlockState': {'State': 'associated'}}], 'IsDefault': 'false', 'Tags': [{'Key': 'Name', 'Value': 'Shared VPC'}]}]}
>>> type(aws_ec2_describe_vpcs)
<class 'dict'>
>>> type(aws_ec2_describe_vpcs["Vpcs"])
<class 'list'>
>>> aws_ec2_describe_vpcs["Vpcs"][0] #一つ目のvpcのdictの内容
{'CidrBlock': '30.1.0.0/16', 'DhcpOptionsId': 'dopt-19edf471', 'State': 'available', 'VpcId': 'vpc-0e9801d129EXAMPLE', 'OwnerId': '111122223333', 'InstanceTenancy': 'default', 'CidrBlockAssociationSet': [{'AssociationId': 'vpc-cidr-assoc-062c64cfafEXAMPLE', 'CidrBlock': '30.1.0.0/16', 'CidrBlockState': {'State': 'associated'}}], 'IsDefault': 'false', 'Tags': [{'Key': 'Name', 'Value': 'Not Shared'}]}
>>> list(aws_ec2_describe_vpcs["Vpcs"][1]) #「二つ目のvpcのdict」の「Keyのリスト」
['CidrBlock', 'DhcpOptionsId', 'State', 'VpcId', 'OwnerId', 'InstanceTenancy', 'CidrBlockAssociationSet', 'IsDefault', 'Tags']
>>> aws_ec2_describe_vpcs["Vpcs"][0]["CidrBlock"] #一つ目のvpcのCidrBlockの値
'30.1.0.0/16'
>>> list(aws_ec2_describe_vpcs["Vpcs"][1]["CidrBlockAssociationSet"][0])
['AssociationId', 'CidrBlock', 'CidrBlockState']
>>> aws_ec2_describe_vpcs["Vpcs"][1]["CidrBlockAssociationSet"][0]["CidrBlock"]
'10.0.0.0/16'
>>> aws_ec2_describe_vpcs["Vpcs"][0]["Tags"][0]["Value"]
'Not Shared'
上記の例のように複雑長大のlistやdictも一つ一つ構造を理解すれば各種データを一発で参照できるようになります。以下のことは注意して意識しましょう。
- listは一つの要素しかない場合もままある(ただ角カッコがあるだけ)
- list内の要素が一つしかなくても当然インデックス指定[0]は必要
- dictのキーやバリューはきちんとクォーテーションで囲む
- 詰まったら適宜typeやlistで型やキーを確認
Lv.5 jsonを参照したプログラミング応用
こちらではjsonを参照したpythonプログラミングをします。といっても私は本職のプログラマではないのでAPIでかえってきたデータから抽出するスクリプトレベルです。もっと適したjsonの扱いはあると思うので参考程度に。
こちらはboto3でAWSのEC2インスタンスの一覧を取得し、リソース所有者を識別するOwnerタグが付与されているかどうかを評価するスクリプトです。ResponseSyntaxの構造はドキュメントとして整備されているので、それらを見ながら必要な変数の参照や制御を実施していきましょう。とはいえEC2 instancesのレスポンスの1台当たりのデータ構造もまた相当な質と量の上に、EC2 instanceは比較的多く存在するリソースだと思うのでその数だけIncetances list は長くなります。フィルタもかけていない生データを眺めるのは大変です。
import boto3 # pip install boto3
def check_owner_tag_ec2(arg_region = 'ap-northeast-1'):
print('region: ' + arg_region)
ec2client = boto3.client(service_name = 'ec2', region_name=arg_region)
resp_data = ec2client.describe_instances()
for num, ec2_reservation in enumerate(resp_data['Reservations'], 1):
for ec2_instance in ec2_reservation['Instances']:
if 'Tags' not in ec2_instance.keys():
print(str(num) + ',' + str(arg_region) + ',' + 'Nameタグなし' + ',' + 'Ownerタグなし')
else:
ec2_tags = dict([(tag['Key'], tag['Value']) for tag in ec2_instance['Tags']])
ec2_name = ec2_tags.get('Name', 'Nameタグなし')
ec2_owner = ec2_tags.get('Owner', 'Ownerタグなし')
print(str(num) + ',' + str(arg_region) + ',' + ec2_name + ',' + ec2_owner)
if __name__ == "__main__":
check_owner_tag_ec2()
py .\check_owner_tag_ec2.py
1,ap-northeast-1,poc-EC2,Ownerタグなし
2,ap-northeast-1,web-server,Satoak
3,ap-northeast-1,gitlab-test,Tanakas
...
生の返答データはresp_dataに格納されます。Lv4までやってきた1ライナーでの参照と違って、細かくreservationのlistやInstancesのdictをローカル変数に格納しています。そうしたほうが配列のループ処理など制御がやりやすい場合もあります。ただ結局やっていることは構造を把握して、適切なインデックスや各種key-valueを参照するということです。
boto3を動かすにはAWSなどのそれなりの準備が必要なので、知らない人はこれを単にコピペしても動かないと思います。こんなこともやれるんだというイメージ程度でお願いします。
まとめ
dict/list型の複雑怪奇なデータ構造の読み解き方を整理しました。
本職でもっと適切な常識やうまいやり方があるかもしれませんが、私のようななんちゃってITさんは最初まったくjsonを紐解けませんでした。
同じような人は0では無いと思っているのでその人の助けの一つになれたら幸いです。特にとっかかりがないとなかなかイメージも持てないので、本稿を見て手を動かして半構造データを分解してひとつひとつ把握していただくとよいかと思います。一つ一つ手を動かすのは大事です。
取り急ぎ自分が便利なように作って書きっぷりや例示が雑なのは自覚しているのでそのうち再整理するかもしれないししないかもしれない。