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 を読む

pd.read_excel(path, engine="xlrd")

pd.read_excel(path, engine="openpyxl")

.xls(旧形式)を読む

engine="xlrd"

engine="xlrd"(.xlsに限っては今もこれでよい)

必要なパッケージ

pip install xlrd

pip install pandas openpyxl

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点で壊れます。

  1. 古いエンジン指定:engine="xlrd"では.xlsxを読めません。そのため、すべてのファイルで失敗します。
  2. 例外を黙って捨てている:except: passがあるため、1の失敗が画面に何も出ずに終わります。「動いたのにCSVができない」状態になり、原因に気づけません。
  3. 毎回全ファイルを追記する:取り込み済みかどうかを記録していません。2日目には1日目の分がもう一度CSVに足され、売上が二重に計上されます。
  4. パスの決め打ちと相対パス:読み込み元は個人のデスクトップに直書きされています。出力先のsales_all.csvは相対パスです。cronやsystemdから起動すると作業ディレクトリが変わるため、出力先が意図しない場所になります。
  5. 見出し行の重複と型の崩れ:追記のたびに見出し行も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 df

Excelの読み込みと検査:この記事の主役は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が出したコードで自分で確認すべき箇所

コードを読み慣れていなくても、次の語を検索するだけで危ない箇所を見つけられます。

検索する語

見つかったら

直させる指示

engine="xlrd"

.xlsxを読めない古いAPIです

「.xlsxはopenpyxlで読んで」

except: の直後の pass

失敗が黙って消えます

「loggingで記録して終了コード1で終わって」

"sk-・api_key =・token =

APIキーの直書きです

「環境変数から読んで」

requests.get( に timeout が無い

相手が応答しないと永遠に待ちます

「timeoutを付けて」

ループ内に time.sleep が無い

Webから取得する場合、連続アクセスになります

「1件ごとに数秒空けて」

C:/Users/・相対パス

別の場所で動かすと壊れます

「パスは環境変数で」

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のバージョンやpip installの可否は事業者によって異なる。実行時間の上限を超えると強制終了される。cronは作業ディレクトリと環境変数が手元と異なる

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>&1

cronは失敗しても誰にも知らせてくれません。ログファイルに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と失敗通知を組み合わせるのが確実です。