ChatGPTやClaude Codeに「毎朝届くExcelの売上表をCSVにまとめて」と頼むと、pd.read_excel(engine="xlrd")を含むコードが出てくることがあります。この書き方ではxlrd 2.0以降は.xlsxを読めません。さらにexcept: passと組み合わさると、エラーも出さずにCSVが空のまま終わります。この記事では、read_excelをxlrdからopenpyxlに直させるAIへの指示文を実物で載せます。あわせて、直ったコードを毎日止めずに動かす置き場所まで説明します。
AIが出す pd.read_excel(engine="xlrd") が .xlsx で動かない理由と、今の書き方
以前の書き方と今の書き方
以前は、Excelファイルを読むライブラリとしてxlrdが広く使われていました。そのため古いコードを学習した生成AIは、今でもengine="xlrd"を付けがちです。しかしxlrdは2.0で旧形式の.xlsしか読まない方針に変わりました。現在、.xlsx(Excel 2007以降の形式)を読む役割はopenpyxlが担っています。
以前の書き方(AIが出しがち) | 今の書き方 | |
|---|---|---|
.xlsx を読む |
|
|
.xls(旧形式)を読む |
|
|
必要なパッケージ |
|
|
xlrd 2.0以降の環境で.xlsxをengine="xlrd"で読むと、次のエラーで止まります。このエラーは「xlrdではもう読めない形式だ」と教えているだけです。xlrdを古いバージョンに戻しても根本的な解決にはなりません。
xlrd.biffh.XLRDError: Excel xlsx file; not supported実行環境
- Python 3.10以上を想定しています。
python3 -Vで確認してください。 - パッケージは
pip install pandas openpyxlで入れます。 - バージョンを固定する必要は特にありません。ただし毎日動かすサーバーと手元で挙動を揃えるために、
pip show pandas openpyxlで入ったバージョンを控えておきます。控えたものはrequirements.txtに書いておくと、置き場所を移すときに困りません。 - .xlsxしか扱わないのであれば、xlrdは入れる必要がありません。
ChatGPT・Claude Codeへの最初の指示と、出てきたコードの5つの問題点
よくある最初の指示
毎朝フォルダに届く売上のExcelファイルを全部読んで、
1つのCSVにまとめるPythonスクリプトを書いてください。出てきがちなコード
import glob
import pandas as pd
files = glob.glob("C:/Users/username/Desktop/売上/*.xlsx")
for f in files:
try:
df = pd.read_excel(f, engine="xlrd")
df.to_csv("sales_all.csv", mode="a", index=False)
except:
passこのコードは短くて、一見すると正しく動きそうに見えます。しかし毎日の定期実行に載せると、少なくとも次の5点で壊れます。
- 古いエンジン指定:
engine="xlrd"では.xlsxを読めません。そのため、すべてのファイルで失敗します。 - 例外を黙って捨てている:
except: passがあるため、1の失敗が画面に何も出ずに終わります。「動いたのにCSVができない」状態になり、原因に気づけません。 - 毎回全ファイルを追記する:取り込み済みかどうかを記録していません。2日目には1日目の分がもう一度CSVに足され、売上が二重に計上されます。
- パスの決め打ちと相対パス:読み込み元は個人のデスクトップに直書きされています。出力先の
sales_all.csvは相対パスです。cronやsystemdから起動すると作業ディレクトリが変わるため、出力先が意図しない場所になります。 - 見出し行の重複と型の崩れ:追記のたびに見出し行もCSVに書き込まれます。また「00123」のような商品コードは数値として読まれ、先頭の0が消えて「123」になります。
直させる指示(そのまま使える形)
「動かないので直して」とだけ伝えると、AIはxlrdを古いバージョンに戻す案を出すことがあります。そうならないよう、直す内容を箇条書きで具体的に指定します。
次の条件でスクリプトを書き直してください。
1. .xlsx は pd.read_excel(..., engine="openpyxl") で読む。xlrd は使わない
2. except: pass は禁止。失敗したファイル名と例外の内容を logging でログに出し、
失敗が1件でもあれば終了コード1で終わる
3. 取り込み済みのファイル名を別ファイルに記録し、同じファイルを二度取り込まない
4. 入力フォルダと出力CSVのパスは環境変数から読む。相対パスは使わない
5. CSVの見出し行は、ファイルを新規作成するときだけ書く
6. 「商品コード」列は文字列として読む(先頭の0を残す)
7. 必須列(日付・商品コード・数量・金額)が無いファイルはエラーとして扱う
8. Excelが開いている間にできる「~$」で始まる一時ファイルは対象外にする
9. if __name__ == "__main__": で実行部分を囲む次にAIへ頼むときは、最初からこの条件を付けておくのが近道です。特に2の「失敗したら終了コード1で終わる」は欠かせません。後で説明する定期実行の失敗通知が、この終了コードを頼りに動くからです。
openpyxl前提に直った取り込みスクリプトをブロックごとに読む
import logging
import os
import sys
from pathlib import Path
import pandas as pd
INBOX = Path(os.environ.get("SALES_INBOX", "/home/username/sales/inbox"))
OUTPUT = Path(os.environ.get("SALES_CSV", "/home/username/sales/sales_all.csv"))
DONE_LIST = OUTPUT.with_name("imported_files.txt")
REQUIRED_COLUMNS = ["日付", "商品コード", "数量", "金額"]
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
log = logging.getLogger("sales_import")設定とログの準備:入力フォルダと出力先は環境変数で受け取ります。指定が無いときだけ既定値を使います。取り込み済みの記録ファイルは、出力CSVと同じフォルダに置きます。ログは標準出力に出します。こうしておくと、cronならリダイレクト先のファイルに、systemdならjournalに、そのまま残ります。
def load_done() -> set[str]:
if DONE_LIST.exists():
return set(DONE_LIST.read_text(encoding="utf-8").splitlines())
return set()取り込み済みの読み込み:1行に1ファイル名を記録した一覧を集合として返します。ファイル名に空白が含まれても崩れないよう、split()ではなくsplitlines()で行ごとに分けています。
def read_sales(path: Path) -> pd.DataFrame:
df = pd.read_excel(
path,
engine="openpyxl",
sheet_name=0,
dtype={"商品コード": str},
)
missing = [c for c in REQUIRED_COLUMNS if c not in df.columns]
if missing:
raise ValueError(f"{path.name}: 列が見つかりません {missing}")
df = df[REQUIRED_COLUMNS].dropna(how="all")
df["日付"] = pd.to_datetime(df["日付"]).dt.strftime("%Y-%m-%d")
df["取込元"] = path.name
return dfExcelの読み込みと検査:この記事の主役はengine="openpyxl"の部分です。sheet_name=0は先頭のシートを指定しています。dtypeで商品コードを文字列として読むため、先頭の0が消えません。
読み込んだ後は、必須列がそろっているかを確かめます。足りなければ例外を投げます。取引先がExcelの列名を変えた日に、黙って空のデータを取り込まないためです。最後に空行を落とし、日付の形式を揃え、どのファイルから来た行かを「取込元」列に残します。
def main() -> int:
done = load_done()
targets = sorted(
p for p in INBOX.glob("*.xlsx")
if p.name not in done and not p.name.startswith("~$")
)
if not targets:
log.info("新しいファイルはありません")
return 0
failed = 0
for path in targets:
try:
df = read_sales(path)
except Exception:
log.exception("読み込みに失敗: %s", path.name)
failed += 1
continue
write_header = not OUTPUT.exists()
df.to_csv(OUTPUT, mode="a", index=False, header=write_header, encoding="utf-8")
with DONE_LIST.open("a", encoding="utf-8") as f:
f.write(path.name + "\n")
log.info("%s から %d 行を追記", path.name, len(df))
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())本体:まず、未取り込みの.xlsxだけを名前順に処理対象として集めます。Excelの一時ファイル(~$で始まるもの)は除外します。
読み込みに失敗したファイルはlog.exceptionでスタックトレースごとログに残し、次のファイルへ進みます。except: passとの違いは、失敗が必ず記録に残り、最後に終了コード1が返ることです。
成功したファイルだけをCSVに追記し、その直後に取り込み済みとして記録します。順番を逆にすると、途中で止まったときに「記録済みなのにCSVに入っていない」ファイルが生まれます。見出し行は、CSVが存在しないときだけ書きます。
CSVをExcelで開くと文字化けする場合は、追記用のCSVはUTF-8のまま保ってください。Excelで開く用のファイルは別に書き出す方が安全です(追記のたびに文字コードの印が入るのを避けるため)。CSV出力の引数まわりはline_terminator削除とCSV文字化け|26/10で詳しく書いています。
AIが出したコードで自分で確認すべき箇所
コードを読み慣れていなくても、次の語を検索するだけで危ない箇所を見つけられます。
検索する語 | 見つかったら | 直させる指示 |
|---|---|---|
| .xlsxを読めない古いAPIです | 「.xlsxはopenpyxlで読んで」 |
| 失敗が黙って消えます | 「loggingで記録して終了コード1で終わって」 |
| APIキーの直書きです | 「環境変数から読んで」 |
| 相手が応答しないと永遠に待ちます | 「timeoutを付けて」 |
ループ内に | Webから取得する場合、連続アクセスになります | 「1件ごとに数秒空けて」 |
| 別の場所で動かすと壊れます | 「パスは環境変数で」 |
Excelを管理画面から取ってくる場合の、取得先サイトへの配慮
売上表がメールやフォルダに届くのではなく、ECサイトや業務システムの管理画面からダウンロードする形の場合もあります。そのときは、取得部分もAIに書かせることになります。その際に守ることを挙げます。
- 公式のエクスポート機能やAPIを先に探す:画面を操作する自動化より壊れにくく、相手にとっても負担が少ない方法です。
- 利用規約で自動取得が許されているかを確かめる:robots.txt(クローラーへのアクセス方針を書いたファイル)も確認します。禁止されていれば自動化しません。
- アクセスは1日1回、必要なファイルだけにする:複数ページを辿る場合は、間に数秒以上の間隔を空けます。
requestsには必ずtimeoutを付けます。 - ログインの制限・画像認証・アクセス制限を回避するコードは書かせない:AIがそうした手口を提案してきても採用しません。
- ログイン情報は環境変数に置く:コードやGitには入れません。
ブラウザ操作が避けられない場合は、Seleniumの書き方も古いAPIが出やすい箇所です。find_element_by_xpath削除|2026年10月版を参照してください。
毎朝止めずに動かす場所:手元PC・共有サーバーのcron・VPSのsystemd timer
止まる条件の比較
置き場所 | 向いている条件 | 止まる条件 |
|---|---|---|
手元のPC(タスクスケジューラ・launchd) | Excelがそのまま手元に届く。毎日PCを使う | スリープ・シャットダウン中は動かない。出張や休みの日にPCを閉じていると、その日の分が飛ぶ |
共有レンタルサーバーのcron | Excelをサーバーに置ける(アップロードや転送の手段がある)。処理が数秒で終わる | Pythonのバージョンや |
VPS(systemd timer) | 毎日確実に動かしたい。失敗したら通知が欲しい | OSの更新・ディスク容量の管理を自分でやらないと、いずれ止まる。契約が切れれば止まる |
共有サーバーのcronに置く場合
パスはすべて絶対パスで書きます。仮想環境のPythonも、フルパスで直接指定します。
30 8 * * * SALES_INBOX=/home/username/sales/inbox SALES_CSV=/home/username/sales/sales_all.csv /home/username/sales/.venv/bin/python /home/username/sales/sales_import.py >> /home/username/sales/import.log 2>&1cronは失敗しても誰にも知らせてくれません。ログファイルにERRORが出ていないかを見る習慣が必要です。
VPSのsystemd timerに置く場合
/home/username/.config/systemd/user/sales-import.serviceに次の内容を書きます。
[Unit]
Description=毎朝の売上Excel取り込み
[Service]
Type=oneshot
Environment=SALES_INBOX=/home/username/sales/inbox
Environment=SALES_CSV=/home/username/sales/sales_all.csv
ExecStart=/home/username/sales/.venv/bin/python /home/username/sales/sales_import.py同じフォルダのsales-import.timerには次の内容を書きます。
[Unit]
Description=毎朝8:30に売上取り込み
[Timer]
OnCalendar=*-*-* 08:30:00
Persistent=true
[Install]
WantedBy=timers.target書いたらsystemctl --user enable --now sales-import.timerで有効にします。ログアウト後も動かすにはloginctl enable-linger usernameも必要です。
Persistent=trueを付けると、サーバーが止まっていた時間帯の回を、起動時に取り戻して実行します。スクリプトが失敗時に終了コード1を返すので、systemdはその回を「失敗」として記録します。失敗したときにSlackへ通知する方法はsystemd OnFailureでSlack通知|26年10月にまとめています。
見落としがちな「書き込み途中のファイル」
取り込みの時刻に、Excelがまだコピーの途中だと、壊れたファイルとして読み込みに失敗します。この場合も、上のスクリプトは記録を付けないまま失敗としてログに残します。そのため、翌日の実行で自動的に取り込み直されます。
ファイルが届く時刻が決まっているなら、取り込みはその30分ほど後に設定しておくと確実です。
関連する選択肢
毎日決まった時刻にスクリプトを動かすだけなら、共有のレンタルサーバーでも足ります。cronが使えるプランを選べば、自分でOSを管理する必要はありません。
※ 広告を含みます(A8.net)。リンク経由でお申し込みがあった場合、手数料を受け取ることがあります。
まとめ:read_excelはopenpyxl、失敗は黙らせず、止まらない場所に置く
生成AIが出すpd.read_excel(engine="xlrd")は、xlrd 2.0以降では.xlsxを読めません。今の書き方はengine="openpyxl"です。
ただ、エンジンを直すだけでは毎日の運用に耐えません。AIへの指示には次の条件も明記してください。
except: passを禁止し、失敗したら終了コード1で終わる- 取り込み済みのファイルを記録して、二重に取り込まない
- パスは環境変数から読み、相対パスを使わない
- 見出し行は新規作成時だけ書く
- 商品コードは文字列として読む
AIが出したコードは、xlrd・pass・api_key・timeoutの有無を検索するだけでも危ない箇所が見つかります。
置き場所は条件で選びます。PCを毎日開くなら手元のPC、数秒で終わる処理なら共有サーバーのcronで足ります。休みの日も確実に取り込み、失敗に気づきたいなら、VPSのsystemd timerにPersistent=trueと失敗通知を組み合わせるのが確実です。