月初になると、ストアの管理画面から売上の CSV を落として、表計算ソフトに貼る作業が待っています。アプリが一本なら、それで足ります。ところが本数が増えると、ストアごとに列の名前も日付の書き方も違うことが、じわじわと効いてきます。
ある月、貼り付けた合計と、ストアの画面に出ている合計が合いませんでした。差は小さく、どこで消えたのか見当がつきません。結局、返金の行を「マイナスだから」と手で除いていたことが原因でした。合っているかどうかを確かめる手段を持たないまま足し算をすると、間違いは数字の見た目だけでは分かりません。
その反省から組んだ集計の手順を、ここに書き残します。ストアごとの CSV を共通の4列に寄せ、月末の為替で円に直し、元の通貨のままの合計を並べて突き合わせるところまでです。ストアの CSV の列名は変わることがあるので、スクリプトの中身は「あなたの手元の CSV を開いて書き換える」前提にしています。
共通の4列に寄せる、という割り切り
集計に必要なのは、突き詰めると四つだけです。いつ(月)、どのアプリか、いくらか、何の通貨か。ストアの CSV にはほかにも多くの列がありますが、月別・アプリ別の収益を知るだけなら、この四つで足ります。
寄せ方は単純です。ストアごとに「うちの CSV では、日付はこの列、アプリ名はこの列」という対応表を持ちます。新しいストアを足すときは、対応表に1行足すだけです。
| 共通の列 | 寄せるときの注意 |
|---|---|
| 月 | 日付の先頭7文字だけ使います。2026/09/05 と 2026-09-05 の両方を 2026-09 にそろえます |
| アプリ | ストアによって表示名が違うことがあります。違うなら対応表で名前を寄せます |
| 金額 | 桁区切りのカンマを外し、浮動小数点でなく Decimal で持ちます。返金はマイナスのまま残します |
| 通貨 | 大文字にそろえます。小文字で入っている CSV がありました |
金額を Decimal で持つのは、円に直したあとの端数が、月ごとにずれていくのを避けるためです。数十行なら誤差は目に見えませんが、あとで「合わない」と気づいたときに原因が追えなくなります。
スクリプト全体
次のスクリプトを merge_sales.py という名前で置きます。これは、列名の対応表と、為替の固定値を渡すだけで動くように作ってあります。MAPPING の列名は例です。 手元の CSV の1行目を見て、そのまま書き換えてください。
"""store ごとに形式の違う売上 CSV を共通 4 列に寄せ、アプリ別・月別に集計する。"""
import csv, sys, json
from collections import defaultdict
from decimal import Decimal, InvalidOperation
# store ごとの列名の対応は、手元の CSV を開いて自分で書き換える(ここは例)
MAPPING = {
"storeA": {"date": "Date", "app": "App", "amount": "Net", "currency": "Cur"},
"storeB": {"date": "Order Charged Date", "app": "Product", "amount": "Payout", "currency": "Currency"},
}
def load(path, store):
m = MAPPING[store]
rows, bad = [], []
with open(path, newline="", encoding="utf-8-sig") as f:
for n, r in enumerate(csv.DictReader(f), start=2):
try:
rows.append({
"month": r[m["date"]].strip()[:7].replace("/", "-"),
"app": r[m["app"]].strip(),
"amount": Decimal(r[m["amount"]].replace(",", "")),
"currency": r[m["currency"]].strip().upper(),
"store": store,
})
except (KeyError, InvalidOperation) as e:
bad.append((path, n, repr(e))) # 黙って捨てず、行番号ごと残す
return rows, bad
def main(rates_path, *pairs):
rates = json.load(open(rates_path)) # {"2026-09": {"USD": "150.0", "JPY": "1"}}
totals, raw_sum, bad_all = defaultdict(Decimal), defaultdict(Decimal), []
for p in pairs:
store, path = p.split("=", 1)
rows, bad = load(path, store)
bad_all += bad
for r in rows:
rate = Decimal(rates[r["month"]][r["currency"]]) # 無ければ KeyError で止める
totals[(r["month"], r["app"])] += r["amount"] * rate
raw_sum[(store, r["currency"])] += r["amount"]
for (month, app), v in sorted(totals.items()):
print(f"{month} {app:<12} {v:>12,.0f} JPY")
print("-- 元通貨のままの合計(ストア別・突き合わせ用)")
for (store, cur), v in sorted(raw_sum.items()):
print(f"{store} {cur} {v}")
print(f"-- 読めなかった行: {len(bad_all)}")
for b in bad_all: print(" ", *b)
if __name__ == "__main__":
main(*sys.argv[1:])為替の固定値は、月ごとに1本の JSON にまとめます。取引日ごとに変えず、月末の1回だけ決める理由は、このあと書きます。
{"2026-09": {"USD": "150", "JPY": "1"}}走らせるときは、為替の JSON と、「ストア名=CSV のパス」の組を渡します。
python3 merge_sales.py rates.json storeA=a.csv storeB=b.csv