note.nkmk.me

PythonでJSONファイル・文字列の読み込み・書き込み

Date: 2018-05-16 / tags: Python, JSON
このエントリーをはてなブックマークに追加

Pythonの標準ライブラリのjsonモジュールを使うとJSON形式のファイルや文字列をパースできる。整形してファイルや文字列として出力・保存することも可能。

基本的な使い方を説明する。

データ分析ライブラリpandasのpandas.DataFrameとして読み書きする場合は以下の記事を参照。

ここでは、以下のようにモジュールをインポートする。いずれも標準ライブラリに含まれているので追加でインストールする必要はない。

import json
from collections import OrderedDict
import pprint

pprintは結果を見やすくするためにインポートしている。JSONの処理自体には使わない。

以下の内容について説明する。

  • JSON文字列を辞書に変換: json.loads()
    • 順番を保持: 引数object_pairs_hook
    • バイト列を変換
  • JSONファイルを辞書として読み込み: json.load()
  • 読み込んだ辞書の値の取得・変更・削除・追加
  • 文字列として整形して出力: json.dumps()
    • 区切り文字を指定: 引数separators
    • インデントを指定: 引数indent
    • キーでソート: 引数sort_keys
    • Unicodeエスケープ指定: 引数ensure_ascii
  • ファイルとして保存: json.dump()
  • JSONファイルの新規作成・更新(修正・追記)
  • JSONファイル・文字列を扱う上での注意点
    • Unicodeエスケープ
    • 引用符
スポンサーリンク

JSON文字列を辞書に変換: json.loads()

JSON文字列を辞書に変換するにはjson.loads()関数を使う。

以下のように第一引数に文字列を指定すると辞書に変換される。

s = r'{"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}'

print(s)
# {"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

d = json.loads(s)

pprint.pprint(d, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

print(type(d))
# <class 'dict'>

文字列の\u3042はUnicodeエスケープシーケンス。JSONでは全角文字などの非ASCII文字がUnicodeエスケープされている場合がある。

例ではraw文字列を使ってバックスラッシュを\\ではなく\で記述している。なお、ややこしいが、Unicodeエスケープシーケンスとraw文字列で無効化するエスケープシーケンスは別物。

json.loads()とこのあと説明するjson.load()では、特になにも設定しなくてもUnicodeエスケープシーケンスが対応する文字列に変換される。

読み込んだ辞書の値の取得や変更については後述。

順番を保持: 引数object_pairs_hook

Python3.6までは言語仕様として辞書(dict型オブジェクト)は要素の順序を保持していない(保持している実装もある)。

このため、変換された辞書では元のJSON文字列での順番が保持されない場合がある。

引数object_pairs_hookに順序付き辞書であるOrderedDict型を指定すると、返り値がOrderedDict型となり、順序が保持されたまま変換される。OrderedDictをインポートしておく必要があるので注意。

od = json.loads(s, object_pairs_hook=OrderedDict)

pprint.pprint(od)
# OrderedDict([('C', 'あ'),
#              ('A', OrderedDict([('i', 1), ('j', 2)])),
#              ('B',
#               [OrderedDict([('X', 1), ('Y', 10)]),
#                OrderedDict([('X', 2), ('Y', 20)])])])

OrderedDictについては以下の記事を参照。

バイト列を変換

Python3.6から、json.loads()の第一引数として文字列だけでなくバイト列(bytes型)を指定できるようになった。

b = s.encode()

print(b)
# b'{"C": "\\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}'

print(type(b))
# <class 'bytes'>

db = json.loads(b)

pprint.pprint(db, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

print(type(db))
# <class 'dict'>

以前のバージョンではバイト列は指定できないので、decode()メソッドで文字列に変換してjson.loads()に渡す。

sb = b.decode()

print(sb)
# {"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

print(type(sb))
# <class 'str'>

dsb = json.loads(sb)

pprint.pprint(dsb, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

print(type(dsb))
# <class 'dict'>

最終的にjson.loads()に渡すのであれば特に気にしなくてもいいが、decode()メソッドの第一引数encoding'unicode-escape'を指定すると、Unicodeエスケープシーケンスが変換された文字列を取得できる。

sb_u = b.decode('unicode-escape')

print(sb_u)
# {"C": "あ", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

print(type(sb_u))
# <class 'str'>

dsb_u = json.loads(sb_u)

pprint.pprint(dsb_u, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

print(type(dsb_u))
# <class 'dict'>

json.loads()の出力は同じ。

Unicodeエスケープについての詳細は以下の記事を参照。

JSONファイルを辞書として読み込み: json.load()

JSONファイルを辞書として読み込むにはjson.load()関数を使う。

第一引数にファイルオブジェクトを指定する以外はjson.loads()と同じ。

ファイルオブジェクトは組み込み関数open()などで取得できる。

json.loads()の例と同じ文字列が書き込まれたファイルを使う。

with open('data/src/test.json') as f:
    print(f.read())
# {"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

json.load()でファイルから読み込み。

with open('data/src/test.json') as f:
    df = json.load(f)

pprint.pprint(df, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

print(type(df))
# <class 'dict'>

読み込んだ辞書の値の取得・変更・削除・追加

json.load(), json.loads()でJSONファイル・文字列の内容を辞書として取得できる。

辞書オブジェクトの操作を例とともに示す。

なお、順序付き辞書OrderedDictは辞書dictのサブクラスなので、以下の操作がそのまま使える。

値の取得

辞書の値は[キー]で指定して取得できる。ネストした要素の値は[キー][キー]のように連続して記述すればOK。

pprint.pprint(d, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

print(d['A'])
# {'i': 1, 'j': 2}

print(d['A']['i'])
# 1

辞書のリストが要素となっている場合は、[インデックス]で指定。

print(d['B'])
# [{'X': 1, 'Y': 10}, {'X': 2, 'Y': 20}]

print(d['B'][0])
# {'X': 1, 'Y': 10}

print(d['B'][0]['X'])
# 1

当然ながら、別の変数に代入することも可能。

value = d['B'][1]['Y']
print(value)
# 20

存在しないキーを指定するとエラーになるが、get()メソッドを使うと存在しないキーを指定してもOK。

# print(d['D'])
# KeyError: 'D'

print(d.get('D'))
# None

値の変更

[キー]で指定して新たな値を代入できる。

d['C'] = 'ん'
pprint.pprint(d, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'ん'}

要素の削除

pop()メソッドでキーを指定して要素(キーと値のペア)を削除できる。

d.pop('C')
pprint.pprint(d, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}]}

pop()のほかdel文でも削除可能。

要素の追加

[キー]で新たなキー(存在しないキー)を指定して値を代入すると、新たなキーと値が要素として追加される。

d['C'] = 'あ'
pprint.pprint(d, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

他の辞書を連結したり、複数の要素をまとめて追加したい場合はupdate()メソッドを使う。以下の記事を参照。

そのほかの辞書操作

辞書をforループでまわしたり、キーの存在を確認したりしたい場合は、以下の記事を参照。

またOrderedDictの場合に、任意の位置に新たな要素を追加したり、要素を順番を並べ替えたりする方法については以下の記事を参照。

辞書関連の記事は以下から。

文字列として整形して出力: json.dumps()

辞書をJSON形式の文字列として出力するにはjson.dumps()を使う。

第一引数に辞書を指定すると、JSON形式の文字列が取得できる。

pprint.pprint(d, width=40)
# {'A': {'i': 1, 'j': 2},
#  'B': [{'X': 1, 'Y': 10},
#        {'X': 2, 'Y': 20}],
#  'C': 'あ'}

sd = json.dumps(d)

print(sd)
# {"A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}], "C": "\u3042"}

print(type(sd))
# <class 'str'>

OrderedDict型もそのまま指定可能。

pprint.pprint(od)
# OrderedDict([('C', 'あ'),
#              ('A', OrderedDict([('i', 1), ('j', 2)])),
#              ('B',
#               [OrderedDict([('X', 1), ('Y', 10)]),
#                OrderedDict([('X', 2), ('Y', 20)])])])

sod = json.dumps(od)

print(sod)
# {"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

print(type(sod))
# <class 'str'>

引数を設定することによって整形できる。

区切り文字を指定: 引数separators

デフォルトではキーと値の間が:、要素間が,で区切られる。

引数separatorsでそれぞれの区切り文字をタプルで指定でき、空白を無くして切り詰めたり、まったく違う文字にしたりできる。

print(json.dumps(d, separators=(',', ':')))
# {"A":{"i":1,"j":2},"B":[{"X":1,"Y":10},{"X":2,"Y":20}],"C":"\u3042"}

print(json.dumps(d, separators=(' / ', '-> ')))
# {"A"-> {"i"-> 1 / "j"-> 2} / "B"-> [{"X"-> 1 / "Y"-> 10} / {"X"-> 2 / "Y"-> 20}] / "C"-> "\u3042"}

インデントを指定: 引数indent

引数indentをにインデント幅(文字数)を指定すると、要素ごとに改行・インデントされる。

print(json.dumps(d, indent=4))
# {
#     "A": {
#         "i": 1,
#         "j": 2
#     },
#     "B": [
#         {
#             "X": 1,
#             "Y": 10
#         },
#         {
#             "X": 2,
#             "Y": 20
#         }
#     ],
#     "C": "\u3042"
# }

デフォルトはindent=Noneで改行なし。indent=0とすると、インデントはなしで改行だけされる。

キーでソート: 引数sort_keys

引数sort_keys=Trueとすると、辞書の要素がキーによってソートされる。

print(json.dumps(od))
# {"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

print(json.dumps(od, sort_keys=True))
# {"A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}], "C": "\u3042"}

Unicodeエスケープ指定: 引数ensure_ascii

これまでの例のように、デフォルトでは全角文字などの非ASCII文字がUnicodeエスケープされて出力される。

引数ensure_ascii=Falseとすると、Unicodeエスケープされない。

print(json.dumps(od, ensure_ascii=False))
# {"C": "あ", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

ファイルとして保存: json.dump()

辞書をJSON形式のファイルとして保存するにはjson.dump()を使う。

第二引数にファイルオブジェクトを指定する以外はjson.dumps()と同じ。第一引数に辞書を指定し、indentなど他の引数も同様に使える。

open()でファイルを開く際に書き込みモード'w'を指定する必要があるので注意。

with open('data/dst/test2.json', 'w') as f:
    json.dump(d, f, indent=4)

結果を確認。

with open('data/dst/test2.json') as f:
    print(f.read())
# {
#     "A": {
#         "i": 1,
#         "j": 2
#     },
#     "B": [
#         {
#             "X": 1,
#             "Y": 10
#         },
#         {
#             "X": 2,
#             "Y": 20
#         }
#     ],
#     "C": "\u3042"
# }

既存ファイルのパスを指定すると上書き、存在しないファイルのパスを指定すると新規作成になる。ただし、ファイルの直上までのディレクトリ(フォルダ)は存在していなければエラー(FileNotFoundError例外)となる。

JSONファイルの新規作成・更新(修正・追記)

JSONファイルを新規作成・更新(修正・追記)する流れを例とともに説明する。

JSONファイルの新規作成

辞書オブジェクトを作成。

d_new = {'A': 100, 'B': 'abc', 'C': 'あいうえお'}

open()でパスを指定、json.dump()で適宜整形して保存。

with open('data/dst/test_new.json', 'w') as f:
    json.dump(d_new, f, indent=2, ensure_ascii=False)

結果を確認。

with open('data/dst/test_new.json') as f:
    print(f.read())
# {
#   "A": 100,
#   "B": "abc",
#   "C": "あいうえお"
# }

JSONファイルの更新(修正・追記)

open()でパスを指定しjson.load()で既存のJSONファイルを読み込み。元の並びを保持したい場合は、引数object_pairs_hook=OrderedDictとする。

with open('data/dst/test_new.json') as f:
    d_update = json.load(f, object_pairs_hook=OrderedDict)

print(d_update)
# OrderedDict([('A', 100), ('B', 'abc'), ('C', 'あいうえお')])

辞書オブジェクトを適宜更新。

d_update['A'] = 200
d_update.pop('B')
d_update['D'] = 'new value'

print(d_update)
# OrderedDict([('A', 200), ('C', 'あいうえお'), ('D', 'new value')])

open()でパスを指定しjson.dump()で適宜整形して保存。便宜上、例では新しいパスを指定しているが、元のファイルパスを指定すると上書きとなる。

with open('data/dst/test_new_update.json', 'w') as f:
    json.dump(d_update, f, indent=2, ensure_ascii=False)

結果を確認。

with open('data/dst/test_new_update.json') as f:
    print(f.read())
# {
#   "A": 200,
#   "C": "あいうえお",
#   "D": "new value"
# }

JSONファイル・文字列を扱う上での注意点

jsonモジュールを使っている場合は特に気にしなくてもうまいこと処理してくれるが、jsonモジュールを使わずに処理するときに注意しなければならない点を挙げる。

Unicodeエスケープ

特にWeb APIで取得できるJSONはセキュリティのため全角文字などの非ASCII文字がUnicodeエスケープされている場合がある。

json.load(), json.loads()ではUnicodeエスケープシーケンス\uXXXXを適切に処理してくれるが、テキストファイルとして読み込む場合はエンコーディングに'unicode-escape'を設定しないとUnicodeエスケープされたままの文字列となる。

JSONファイルを読み込んだときに日本語が文字化けしたようになっている場合はこれが原因。

エンコーディングはopen()関数の引数で指定する。

with open('data/src/test.json') as f:
    print(f.read())
# {"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

with open('data/src/test.json', encoding='unicode-escape') as f:
    print(f.read())
# {"C": "あ", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

上でも書いたが、Unicodeエスケープされたバイト列を文字列にデコードする場合も注意が必要。

Python標準ライブラリのurllib.request.urlopen()はバイト列を返すので、Web APIにアクセスして取得したバイト列をエンコーディングを指定せずにデコードするとUnicodeエスケープされたままの文字列となる。

エンコーディングはdecode()メソッドの引数で指定する。

print(b)
# b'{"C": "\\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}'

print(b.decode())
# {"C": "\u3042", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

print(b.decode(encoding='unicode-escape'))
# {"C": "あ", "A": {"i": 1, "j": 2}, "B": [{"X": 1, "Y": 10}, {"X": 2, "Y": 20}]}

なお、バイト列の場合も最終的にjson.loads()で辞書に変換するのであれば、json.loads()の内部でUnicodeエスケープシーケンスが処理されるので特に問題ない。

Unicodeエスケープについての詳細は以下の記事を参照。

引用符

JSONではキーや文字列を囲む引用符はダブルクォート"でなければならない。

json.dump(), json.dumps()では適切に処理してくれるが、独自に文字列を生成して引用符がシングルクォート'になっていると、json.load(), json.loads()でエラーとなる。

例えば、辞書オブジェクトをstr()で文字列に変換すると引用符としてシングルクォート'が使われるので、json.loads()でエラーとなる。

d = {"A": 100, "B": 'abc', "C": 'あいうえお'}

print(str(d))
# {'A': 100, 'B': 'abc', 'C': 'あいうえお'}

# print(json.loads(str(d)))
# JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

json.dumps()で文字列に変換すればOK。

print(json.dumps(d))
# {"A": 100, "B": "abc", "C": "\u3042\u3044\u3046\u3048\u304a"}

print(json.loads(json.dumps(d)))
# {'A': 100, 'B': 'abc', 'C': 'あいうえお'}

print(type(json.loads(json.dumps(d))))
# <class 'dict'>
スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事