Python3 で JSON ファイル全体を読み込まずに JSON 要素を追加する

> ログや機械学習用のデータなど、JSON ファイルにデータを保存したいが、**ファイルに追記**で（ファイルの末尾に）追加したい。つまり、JSON 要素を JSON ファイルの末尾に挿入したい。
> 
> しかし、ファイルが肥大化していくため**ファイル全体をメモリに読み込まないで追記できないか**。

また、１行１JSON データで完結させて追記しないで JSON 配列に追加したいのです。

```json:これではない（一般的だけど）
{"key0": "one"}
{"key1": "two"}
{"key2": "new!"}
```
```json:これ
[{"key0": "one"}, {"key1": "two"}, {"key2": "new!"}]
```

## TL;DR

> ファイル末尾の「`]`」を削除して JSON 配列を開けてから、「`,`」と JSON 要素を追加後、配列を閉じる。

```json:original
[{"key0": "one"}, {"key1": "two"}]
```
```json:ステップ１）配列を開ける（"]"の削除）
[{"key0": "one"}, {"key1": "two"}
```
```json:ステップ２）追記
[{"key0": "one"}, {"key1": "two"}, {"key2": "new!"}
```
```json:ステップ３）配列を閉じる（"]"の追加）
[{"key0": "one"}, {"key1": "two"}, {"key2": "new!"}]
```

## TS;DR

### サンプル関数

```python:functions.py
import json

def append_json_to_file(data: dict, path_file: str) -> bool:
    with open(path_file, 'ab+') as f:              # ファイルを開く
        f.seek(0,2)                                # ファイルの末尾（2）に移動（フォフセット0）  
        if f.tell() == 0 :                         # ファイルが空かチェック
            f.write(json.dumps([data]).encode())   # 空の場合は JSON 配列を書き込む
        else :
            f.seek(-1,2)                           # ファイルの末尾（2）から -1 文字移動
            f.truncate()                           # 最後の文字を削除し、JSON 配列を開ける（]の削除）
            f.write(' , '.encode())                # 配列のセパレーターを書き込む
            f.write(json.dumps(data).encode())     # 辞書を JSON 形式でダンプ書き込み
            f.write(']'.encode())                  # JSON 配列を閉じる
    return f.close() # 連続で追加する場合は都度 Open, Close しない方がいいかも
```

### 実行例

```python:main.py（Python&gt;=3.6）
from functions import append_json_to_file

PATH_FILE = 'test.txt'

for i in range(10):
    data = { f'key{i}': i }              # サンプル辞書データ（>= Python3.6 f文字列）
    append_json_to_file(data, PATH_FILE) # 要素を追加

# 検証（保存ファイルのまるごと読み込み）
f_saved  = open(PATH_FILE, "r")
contents = f_saved.read()
print(contents)
f_saved.close()
```

```python:main.py（Python&lt;3.6）
from functions import append_json_to_file

PATH_FILE = 'test.txt'

for i in range(10):
    key  = 'key{num}'.format(num=i)
    data = { key: i }                    # サンプル辞書データ
    append_json_to_file(data, PATH_FILE) # 要素を追加

# 検証（保存ファイルのまるごと読み込み）
f_saved  = open(PATH_FILE, "r")
contents = f_saved.read()
f_saved.close()
print(contents)
```

### 実行結果

```json:出力結果
[{"key0": 0} , {"key1": 1} , {"key2": 2} , {"key3": 3} , {"key4": 4} , {"key5": 5} , 
{"key6": 6} , {"key7": 7} , {"key8": 8} , {"key9": 9}]
```

- [オンラインで動作確認する](https://paiza.io/projects/o9tvOdshC6t03RqnR8GE_A) @ paiza.IO （Python 3.6.6, Ubuntu 18.04.1 LTS）
- macOS HighSierra（Python 3.6.0, OSX 10.13.6）
- RaspberryPi（Python 3.4.2, Raspbian GNU/Linux 8 Jessie）

### `open(path_file, 'ab+')` の `ab+`

| 文字 | 意味 |
|:---:|:--------------------------------------------|
| 'r' | 読み込み用に開く (デフォルト)|
| 'w' | 書き込み用に開き、まずファイルを切り詰める |
| 'x' | 排他的な生成に開き、ファイルが存在する場合は失敗する |
| 'a' | 書き込み用に開き、ファイルが存在する場合は末尾に追記する |
| 'b' | バイナリモード |
| 't' | テキストモード (デフォルト) |
| '+' | ディスクファイルを更新用に開く (読み込み／書き込み) |

- [open 関数](https://docs.python.org/ja/3/library/functions.html#open)@ Python3 マニュアルより


### `Seek` メソッド

```python
fileObject.seek(offset[, from_what])
```
> ファイルオブジェクトの位置を変更するには、`f.seek(offset, from_what)` を使います。ファイル位置は基準点（reference point）にオフセット値 `offset` を足して計算されます。
> 参照点は `from_what` 引数で選びます。`from_what` の値が `0` ならばファイルの 先頭から測り、`1` ならば現在のファイル位置を使い、**`2` ならばファイルの終端を参照点として使います**。`from_what` は省略することができ、デフォルトの値は `0`、すなわち参照点としてファイルの先頭を使います。
> 
> （「[ファイルオブジェクトのメソッド](https://docs.python.org/ja/3/tutorial/inputoutput.html?highlight=seek#methods-of-file-objects)」| 入力と出力 @ Python 3.7 公式ドキュメントより）


## 参考文献

- 「[ファイルオブジェクトのメソッド](https://docs.python.jp/3/tutorial/inputoutput.html#methods-of-file-objects) | 入力と出力」@ Python3 公式ドキュメント
- 「[Appending data to a json file in Python](https://stackoverflow.com/questions/12994442/appending-data-to-a-json-file-in-python) @ StackOverflow