売上集計は動いていたのに数字が古かった|前回のファイルを使い続けていた失敗

売上集計は動いていたのに数字が古かった|前回のファイルを使い続けていた失敗 経理・数字の自動化

毎月決まった時間に、Googleスプレッドシートから売上を取り込んで集計する。エラーは出ない。Slackにもちゃんと表が届く。それでも、その数字が先月のものだったことがあります。

結論を先に書きます。取り込みが失敗しても、前回ダウンロードしたファイルがそのまま残っているので、集計は普通に成功します。止まらないし、赤い文字も出ません。だから「動いている」ことは「数字が新しい」ことの証明になりません。集計が見ているのはスプレッドシートではなく、手元のファイルだからです。

私は清掃業を経営していて、プログラマーではありません。この記事は、うちのMacに実際に残っている当時のスクリプトと、その後どう直したかを読み返して書いています。


当時、3か所とも「古い」に気づけなかった

うちの月末着地予想は、3つの部品が順番に動くだけの単純なものです。取り込み → 計算 → Slackに投稿。この3つが、そろって古いデータを素通りさせていました。1か所ずつ見ていきます。

① 取り込み:成功しても失敗しても同じ文が出ていた

当時の取り込みスクリプトは、こういう作りでした。バックアップに残っているものです。

curl -sL -o "利益表.xlsx" "【ダウンロード用のURL】"
curl -sL -o "売上帳R8.xlsx" "【ダウンロード用のURL】"

echo "最新版を取得しました: $(date '+%Y-%m-%d %H:%M')"

最後の行を見てください。取れても取れなくても、必ず「最新版を取得しました」と表示されます。curlがどうだったかを一度も見ていないからです。

しかも -s が付いています。これは「余計な表示を出すな」という指定で、エラーの説明まで一緒に消します。静かに失敗して、そのうえで「取得しました」と報告する。いちばんたちの悪い組み合わせでした。

② 呼び出し側:結果を捨てていた

その取り込みを呼んでいた側は、こう書いてありました。

# 1) Googleスプレッドシートの最新版を取得
./sync.sh > /dev/null 2>&1

> /dev/null 2>&1 は「表示も、エラーの文章も、全部捨てる」という意味です。成功したか失敗したかも確かめていません。次の行では、何ごともなかったように計算が始まります。

③ 計算:ファイルの日付を見ていない

計算する側は、いまもこの1行でファイルを開いています。

wb = openpyxl.load_workbook(book, data_only=True)

これは「そこにあるファイルを読む」だけです。そのファイルがいつ落ちてきたものか、中身が今月のものか、確かめる機能はありません。当然です。頼まれていないのですから。

つまり、取り込みは嘘の成功報告を出し、呼び出し側はそれすら見ず、計算は何も疑わない。この3つがそろうと、古い数字が正しい顔をして最後まで通り抜けます。


「空っぽ」より「前回のファイル」のほうが危ない

取り込みの失敗には、大きく2種類あります。怖いのは後者です。

起きること 集計の結果 気づけるか
中身が空のファイルや、ログイン画面のHTMLが保存される エラーで止まる。または売上0円 気づける。明らかに変だから
取りに行けず、前回のファイルがそのまま残る もっともらしい表が普通に出る まず気づけない

空ファイルで「売上0円」が出たら、誰でも異常だと分かります。ところが前回のファイルが残っていた場合、出てくるのは先月の、正しかった数字です。桁も並びも自然です。おかしいと思う理由がどこにもありません。

ダウンロードの成否を確かめるときに if [ ! -s ファイル ](中身が空でないか)だけを見る書き方をよく見ます。これでは前者しか防げません。前回のファイルは、空ではないからです。


終了コードを見ても、まだ足りなかった

「呼び出し側で終了コードを確かめればいい」と考えるのが自然です。ところが当時の作りでは、確かめていたとしても、やはり気づけませんでした。理由は2つあります。

curlは、404でも「成功」で終わる

curl は -f を付けない限り、相手から「そんなファイルはありません」という画面が返ってきても「ちゃんと受け取った」として正常終了します。受け取ったのがエラー画面なだけで、通信自体は成功しているからです。

スクリプトの終了コードは、最後の1行で決まる

当時の取り込みスクリプトの最後は、ファイル一覧を表示する ls -l でした。シェルスクリプトの合否は最後に実行したコマンドの結果になります。

つまり、古いファイルが残ってさえいれば ls は成功し、スクリプト全体も「成功」で終わります。取り込みが1バイトも動いていなくても、です。

ここが、この失敗のいちばん意地の悪いところでした。チェックを1つ足しただけでは足りず、順番と場所が合っていないと意味がありません。


失敗を記録していなかったので、後から調べられなかった

うちには実行の記録を残すファイルがあります。中身は今こうなっています。

[2026-06-21 22:48] 着地予想を … に保存しました
[2026-06-22 15:04] 着地予想を … に保存・Slack投稿しました
[2026-08-12 17:01] 着地予想を … に保存・Slack投稿しました
[2026-08-15 08:01] 着地予想を … に保存・Slack投稿しました

この処理は毎月15日の朝8時に動く設定です。7月15日の記録がありません。7月分のレポートファイルも残っていません。

では7月に何が起きたのか。正直に言うと、いまとなっては分かりません。当時のスクリプトには「成功したときに1行書く」処理しかなく、失敗したときに書く処理が一行も無かったからです。動かなかったのか、動いて取り込みに失敗したのか、区別する材料が残っていません。

成功だけを記録する仕組みは、記録が無い理由を説明できません。後から原因を追いたいなら、失敗したときこそ書き残す必要があります。


いまはこう直している

取り込みの部分はPythonに移しました。やっていることは4つです。順番に意味があります。

  1. 取りに行って、失敗したらはっきり「失敗」として終わる
  2. 受け取ったものが本当にExcelファイルか中身を見て確かめる
  3. 合格したものだけを、一時ファイルから本番のファイルに入れ替える
  4. 呼び出し側は、取り込みが失敗したら計算に進まずに中止する

2番の「本当にExcelか」は、こう判定しています。Excelのファイルは必ず PK という2文字で始まります(中身が圧縮されているためです)。ログイン画面のHTMLを掴んでいたら、ここで落ちます。

if not data or len(data) < 1000 or data[:2] != b"PK":
    raise RuntimeError("中身が正しくありません")

3番も大事です。いきなり本番のファイルに上書きしない。まず .tmp という別名で書き、全部そろって初めて入れ替えます。こうすると、途中で電源が落ちても中途半端なファイルが本番の場所に残りません。

curlのままでも同じことはできる

Pythonに移さなくても、考え方は同じです。そのまま貼って使える形にしておきます。

#!/bin/zsh
FOLDER="$HOME/作業フォルダ"
URL="https://docs.google.com/spreadsheets/d/【ID】/export?format=xlsx"
HONBAN="$FOLDER/売上帳.xlsx"
TMP="$FOLDER/売上帳.xlsx.tmp"

# 1) 取りに行く。失敗したらここで止める(前回のファイルには触らない)
if ! curl -fsSL -o "$TMP" "$URL"; then
  rm -f "$TMP"
  echo "取得に失敗しました。前回のファイルはそのままです。"
  exit 1
fi

# 2) 本当にExcelか確かめる(Excelは必ず PK で始まる)
if [ "$(head -c 2 "$TMP")" != "PK" ]; then
  rm -f "$TMP"
  echo "Excelではないものが返ってきました(ログイン画面など)。"
  exit 1
fi

# 3) ここで初めて本番と入れ替える
mv "$TMP" "$HONBAN"
echo "取得しました: $(date '+%Y-%m-%d %H:%M')"

変えたのは -sL を -fsSL にしたところと、保存先を一時ファイルにしたところの2点です。-f が「エラー画面が返ってきたら失敗として扱え」、-S が「失敗したときは理由を出せ」という指定です。

そして呼び出す側では、取り込みが失敗したら計算に進ませません。うちの場合はここで止めて、エラー音つきのMac通知を出し、記録ファイルに理由を書いて終わります。止まったことが人に伝わるところまでが1セットです。


それでも残っている穴を、正直に書いておく

ここまでで防げるのは「取りに行って失敗した」場合だけです。防げていないものが2つあります。

① 元のシートが更新されていない場合

ダウンロードは成功し、中身もExcelで、けれど現場が今月まだ入力していない——このとき処理は完全に正常終了します。うちの仕組みは、いまこれを自動では止めません。

元のシートが最後に更新された日は、確認用の実行で見られるようにしてあります。うちでは --確認 を付けて実行すると、ファイルを書き換えずに最終更新日だけが出ます。ただしこれは、人が思い出して打つ確認です。自動で見張っているわけではありません。

手元のファイルがいつのものかは、これで分かります。そのまま貼れます。

stat -f "%Sm  %N" -t "%Y-%m-%d %H:%M" ~/作業フォルダ/売上帳.xlsx

② レポートに「データの日付」が入っていない

うちのSlack投稿には (8月 / 8月15日時点) のような日付が入ります。ですがこれは、処理を実行した日です。データがいつのものかではありません。取り込みに失敗して古いファイルで計算しても、この日付だけは今日の日付で表示されます。

ここはまだ直していません。次に直すならここだと思っています。数字を人に配るなら、その数字がいつ時点のものかを必ず一緒に配る。読んだ人が唯一おかしいと気づける場所が、そこだからです。


明日やるなら、この順番で

  1. 取り込みスクリプトを開いて、最後の行が何かを見る。無条件に「取得しました」と出していないか
  2. curl に -f が付いているか確かめる。無ければ付ける
  3. 保存先を一時ファイルにして、確かめてから本番と入れ替える形に変える
  4. 呼び出し側で、取り込みが失敗したら計算に進まないようにする
  5. 失敗したときに1行書き残す処理を足す。成功だけ記録していると、後から何も分からない
  6. いまあるファイルの日付を stat で1回見てみる。思っているより古いかもしれません

1から4までは30分もかかりません。いちばん効くのは5番です。これが無いと、次に何かあったときも同じように「たぶん失敗したんだと思う」で終わります。


まとめ

  • 取り込みが失敗しても前回のファイルが残るので、集計は普通に成功する。エラーは出ない
  • 「空でないか」の確認では防げない。前回のファイルは空ではない
  • curl は -f が無いとエラー画面を受け取っても成功扱いで終わる
  • シェルスクリプトの合否は最後の1行で決まる。最後が ls なら、古いファイルがある限り必ず成功する
  • 本番のファイルに直接上書きしない。一時ファイルに書き、中身を確かめてから入れ替える
  • Excelファイルは必ず PK で始まる。これでログイン画面のHTMLを掴んだことが分かる
  • 失敗したときこそ記録を残す。成功しか書いていない記録は、記録が無い理由を説明できない
  • 数字を配るときは「いつ時点のデータか」を一緒に配る。実行した日ではなく、データの日

そもそもスプレッドシートをどうやって自動で手元に落とすか、公開設定をどう考えるかは Googleスプレッドシートを自動でダウンロードして集計する方法 に書いています。これから取り込みを作るなら、先にそちらを読んでから、この記事の対策を最初から入れておいてください。後から足すより、はるかに楽です。

コメント

タイトルとURLをコピーしました