PythonでCSVから入力してjsonに出力してexeにする時の注意点

　タイトルに通り、PythonでCSVから入力してjsonに出力してexeにする時の注意点を備忘録がてら記しておきます。実装例は完全に個人用のツールなので、概要だけ話します。重要なのはエラーメッセージと対処法。

# デコードエラーを起こすCSV

　まずcsvを読み込みます。`input.csv`というcsvにデータを用意しておいて、それを読み取ります。

```python
import csv

csv_file = open("./input.csv", "r")
c = csv.DictReader(csv_file)
```

　いきなりですが、ここでcsvの中身を見ようとすると（たぶん）以下のようなエラーになります。

```
Exception has occurred: UnicodeDecodeError
'cp932' codec can't decode byte 0xef in position 0: illegal multibyte sequence
  File "C:\Users\hoge\fuga.py", line 19, in <module>
    for row in c:
```

　デコードできないと叱られます。これはpythonを`utf-8`のファイルを`shift-jis`にしようとするからです。以下のように修正する必要があります。

```python
csv_file = open("input.csv", "r",encoding="utf-8-sig")
```

# 全て同じデータになるjson

　今回いちばん面食らった部分。

```python
#csvからパラメータだけ抜粋
params = []
for row in c:
    points.append(row)
n = len(params)

#jsonを読み込んでパラメータの枠を確保する
import json
json_open = open('sample.json', 'r')
j = json.load(json_open)
for i in range(n-1):
    j["Movements"].append(j["Movements"][0])
```

　下の段で少し不格好なことをしてまして、それがトラブルの元だったのですが、`sample.json`にはデータの雛形がありまして、それをパラメータの行数`n`まで拡張するために、雛形をn-1回コピーするという方法を取りました。この結果、無事に`j`というjsonデータの`Movements`というキーに`n`個の枠ができたので、後は`for`文をひたすら回してデータを入れていきます。

　……が、データを入れ終わった後、**なぜかすべての行が同じデータになる**という謎の結果に悩まされました。

　少しデバッグした結果、あらゆる入力ですべての行が書き換えられてしまうということがわかり、これは**すべての行が同じデータを参照している**ということがわかりました。見かけのデータは`n`行でも、すべて1つのデータを参照しているだけであり、どれか1つに改変を加えたらすべてのデータが影響を受けるのです。

　なぜこんなことになってしまったのか。

　`int`や`str`のような型の場合、`=`で変数をコピーしても、なにか変更を加えた時点で「別の変数」と見なされてIDが割り振られますが、`list`は`=`で変数をコピー（実はコピーできていない）した後に編集したらオリジナルデータが編集されるのです。……というのは知識としては知っていましたが、`dict`もそうでしたね。解決策としては、ハードコピーという手法で変数をコピーするしかないです。

```python
#csvからパラメータだけ抜粋
params = []
for row in c:
    points.append(row)
n = len(params)

#jsonを読み込んでパラメータの枠を確保する
import json
import copy
json_open = open('sample.json', 'r')
j = json.load(json_open)
for i in range(n-1):
    new_j = copy.deepcopy(j["Movements"][0])
    j["Movements"].append(new_j)
```

　今回の事例では、ただの`copy`（浅いコピー）ではなく`deepcopy`（深いコピー）でないとダメでした。

# 動かないexe

　Pythonを持ってない人でも動かせるようにexe化してくれるモジュールで、`pyinstaller`というものがあります。これを`pip`しようと思ったらなぜかエラーでインストールできず、ぐぐったらpipのバージョンダウン（`20`→`18`）すれば良いということなのでそれで成功しました。今それを再現しようとしたらなぜか最新のpipでも成功してしまったのでエラー再現不可能です。申し訳ない。

　それで無事`pyinstaller`を使ってexe化したのですが、実行してもすぐに終了してしまう。デバッグ用に入れた先頭の`print`すらも吐かないので、どうやら`import`の時点で問題が起こっているようです。なので、実際に入れた以下のモジュール群を順々に魔女狩りしていく。

```python
import json
import pprint
import csv
import cmath
import math
import numpy as np
import copy
import os
```

　結果的には`numpy`が原因でした。幸いにも、numpyが必要な部分はプログラムで使わなかったので（`np.pi`を使っていたが必要なくなった）このままモジュールを削除。もしnumpyを使ってゴリゴリに計算するプログラムをexe化しなければいけない場合は再度考えましょう……。

# まとめ
　
　「なんとかなったけど、ようわからんかった」という内容が多くて恐縮ですが、こういうのを積み重ねていかないとどんどん忘れてしまうので、自分のために書いておきました。今までこういうのをサボっていてふと不安になったので、これからはこういう日記みたいなプログラム記録も積極的に書いていこうと思います。

# 一応コード

　完全に個人用のツールなので誰の役にも立たないと思いますが、一応。

```python

import json
import pprint
import csv
import cmath
import math
import copy
import os

json_open = open('./sample.json', 'r')
j = json.load(json_open)

print("sample.jsonを読み込みました")

csv_file = open("./input.csv", "r",encoding="utf-8-sig")
c = csv.DictReader(csv_file)

points = []

for row in c:
    points.append(row)

n = len(points)

for i in range(n-1):
    new_j = copy.deepcopy(j["Movements"][0])
    j["Movements"].append(new_j)

for i,p in enumerate(points):
    x = float(p['X'])
    y = float(p['Y'])
    z = float(p['Z'])
    duration = float(p['Duration'])
    j["Movements"][i]["Duration"] = duration
    j["Movements"][i]["StartPos"]["x"] = x
    j["Movements"][i]["StartPos"]["y"] = y
    j["Movements"][i]["StartPos"]["z"] = z
    j["Movements"][i-1]["EndPos"]["x"] = x
    j["Movements"][i-1]["EndPos"]["y"] = y
    j["Movements"][i-1]["EndPos"]["z"] = z
    y -= 1.5
    c = complex(-x,-z)
    rad = cmath.phase(c)
    deg = -math.degrees(rad)+90
    #print(deg)
    j["Movements"][i]["StartRot"]["y"] = deg 
    j["Movements"][i-1]["EndRot"]["y"] = deg 
    c2 = complex(abs(c),y)
    rad2 = cmath.phase(c2)
    deg2 = math.degrees(rad2)
    #print(-deg2)
    j["Movements"][i]["StartRot"]["x"] = deg2 
    j["Movements"][i-1]["EndRot"]["x"] = deg2

pprint.pprint(j)

with open('output.json', 'w') as f:
    json.dump(j, f, indent=4)

print("作業を完了しました")
end = input()
```