ChatGPTやClaude Codeに「新着求人を集めてOpenAI APIで要約し、Slackへ送るPython」を書かせたのに、動かない。原因の多くは、AIがopenai.ChatCompletion廃止前の書き方(openai.ChatCompletion.create・openai.api_key =)をまだ出してくることです。この記事では、AIに今の OpenAI() クライアントの書き方へ直させる指示文を実物で示します。あわせて、APIキーの置き場所と課金の上限を決め、毎日止めずに動かすところまで書きます。

AIが出すopenai.ChatCompletion.createは、openai 1.0(2023年11月)で使えなくなった

最初の指示と、AIが返してきたコード

コードを書き慣れていない人がAIに最初に送る指示は、だいたい次のようなものです。

求人サイト https://example.com/jobs の新着求人を取得して、
1件ずつOpenAI APIで3行に要約し、Slackに送るPythonスクリプトを書いてください。

これに対して、今でも次のような書き方が返ってくることがあります。

import openai
import requests
from bs4 import BeautifulSoup

openai.api_key = "sk-xxxxxxxxxxxxxxxx"

def summarize(text):
    res = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "user", "content": "3行で要約して:\n" + text}],
    )
    return res["choices"][0]["message"]["content"]

def main():
    html = requests.get("https://example.com/jobs").text
    soup = BeautifulSoup(html, "html.parser")
    for a in soup.select("a.job-link"):
        try:
            body = requests.get(a["href"]).text
            summary = summarize(body)
            requests.post("https://hooks.slack.com/services/XXXX", json={"text": summary})
        except Exception:
            pass

main()

一見それらしく見えますが、今の openai パッケージを入れた環境では、openai.ChatCompletion に触れた時点で次のように止まります。

You tried to access openai.ChatCompletion, but this is no longer supported in openai>=1.0.0

openai の Python SDK は 1.0(2023年11月)で書き方が変わり、モジュールに直接キーを入れて関数を呼ぶ形は削除されました。AIは学習データにこの古い書き方が大量に含まれているため、指示を工夫しないと何度でもこれを出してきます。

このコードで危ういのはAPIの古さだけではない

動かない原因はAPIの古さですが、仮に動いたとしても、毎日動かすと事故になる箇所が他に5つあります。自分で見つけられるよう、確認する箇所を表にしました。

確認する箇所

このコードでの状態

何が起きるか

openai.ChatCompletion / openai.api_key =

使っている

openai 1.0以降では動かない

"sk- で始まる文字列

コードに直書き

ファイルを共有・公開した瞬間にキーが漏れ、他人の利用分まで課金される

model="..." の値

固定の古いモデル名

モデルの提供終了でその日から失敗する

time.sleep

無い

詳細ページへ間髪入れずにアクセスし、取得先に負荷をかける

except Exception: pass

全件これで囲んでいる

失敗しても何も出ず、「動いているのに何も届かない」状態が続く

件数の上限

無い

一覧に100件出れば100回APIを呼び、その分だけ課金される

モデル名の固定は今まさに問題になります。調査時点(2026年10月)で、OpenAIは o4-mini のAPIを2026年10月23日にシャットダウン、GPT-5.4 Nano を2027年4月1日にシャットダウンすると告知しています。モデル名をコードに埋め込んだスクリプトは、その日から毎朝失敗するようになります。

Stack Overflow の開発者調査(2025年)でも、開発者の66%が「AI生成コードはほぼ正しいが完全ではない」と答えています。AIは「もっともらしいコード」を出すのは得意ですが、「このコードが壊れる条件」までは検証してくれません。上の表は、その検証を人間側でやるためのチェックリストです。

今のOpenAI()とclient.responses.createへ直させる指示文

直させるときの指示文(そのまま使える形)

「直して」とだけ送ると、AIは表面だけを直して別の古い書き方に置き換えることがあります。どの書き方を使うか・何を禁止するか・毎日動かす前提を、最初から言葉にして渡すのがコツです。

次のスクリプトを、openai パッケージ 1.0 以降の書き方に書き直してください。

【必ず守ること】
- from openai import OpenAI で client = OpenAI() を作り、
  client.responses.create を使う(openai.ChatCompletion と openai.api_key = は使わない)
- APIキーはコードに書かない。環境変数 OPENAI_API_KEY から読む
- モデル名はコードに書かない。環境変数 OPENAI_MODEL から読み、無ければエラーで止める
- Slack の Webhook URL も環境変数 SLACK_WEBHOOK_URL から読む
- 取得先には robots.txt を確認してからアクセスし、ページごとに3秒以上空ける
- 1回の実行で要約するのは最大20件。max_output_tokens も指定する
- except Exception: pass は禁止。失敗は logging でエラーとして残し、
  1件でも失敗したら終了コード1で終わる
- 送信に成功した求人だけを処理済みとしてファイルに記録し、翌日は送らない
- requests には timeout を必ず付ける

【実行環境】
Python 3.10 以上、毎朝1回の定期実行(cron または systemd timer)

書き直した後、変更点を箇条書きで説明してください。

最後の「変更点を箇条書きで説明」は省かないでください。説明に「ChatCompletion を responses に置き換えた」「キーを環境変数に移した」と出てこなければ、指示が反映されていない箇所があるということです。

直った後のコード

実行環境は Python 3.10 以上を想定します。パッケージは次の3つです。

pip install openai requests beautifulsoup4

openai のバージョンに数値の指定はしませんが、1.0 より古いものが入っていると上の古い書き方しか通らず、逆に新しすぎる書き方も通りません。pip show openai で Version: を確認し、client.responses が無いと言われたら pip install -U openai で上げてください。

import json
import logging
import os
import sys
import time
from pathlib import Path
from urllib.parse import urljoin
from urllib.robotparser import RobotFileParser

import requests
from bs4 import BeautifulSoup
from openai import OpenAI, APIError

LIST_URL = "https://example.com/jobs"
SEEN_FILE = Path("/home/username/jobs/seen.json")
MAX_ITEMS = 20       # 1回に要約する上限(課金の上限を兼ねる)
WAIT_SEC = 3         # 取得先へのアクセス間隔(秒)
USER_AGENT = "job-digest/1.0 (contact: admin@example.com)"

logging.basicConfig(level=logging.INFO,
                    format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("job-digest")

設定のブロック。取得先URL・処理済みの記録先・1回あたりの件数上限・アクセス間隔を冒頭にまとめています。MAX_ITEMS は「APIを呼ぶ回数の上限」でもあるので、課金を抑える一番確実な仕組みになります。ログは時刻付きで出し、cron や systemd のログに残るようにしています。

session = requests.Session()
session.headers["User-Agent"] = USER_AGENT

robots = RobotFileParser(urljoin(LIST_URL, "/robots.txt"))
robots.read()

def get(url):
    if not robots.can_fetch(USER_AGENT, url):
        raise RuntimeError(f"robots.txt で取得が許可されていません: {url}")
    time.sleep(WAIT_SEC)
    res = session.get(url, timeout=30)
    res.raise_for_status()
    return res.text

取得のブロック。すべてのアクセスをこの get() に通します。robots.txt(サイトが「ここは機械で取らないで」と示すファイル)で禁止されているURLには行かず、毎回3秒待ってから取得します。timeout=30 が無いと、相手が応答しないときに永久に待ち続け、翌日の実行と重なることがあります。raise_for_status() は、404や500のような失敗を例外として表に出すためのものです。

def fetch_jobs():
    soup = BeautifulSoup(get(LIST_URL), "html.parser")
    links = [urljoin(LIST_URL, a["href"]) for a in soup.select("a.job-link")]
    if not links:
        raise RuntimeError("求人リンクが0件です。ページの構造が変わった可能性があります")
    return links

def fetch_body(url):
    soup = BeautifulSoup(get(url), "html.parser")
    title = soup.select_one("h1").get_text(strip=True)
    body = soup.select_one(".job-body").get_text("\n", strip=True)
    return {"url": url, "title": title, "body": body}

解析のブロック。a.job-link や .job-body は例なので、実際のサイトに合わせて書き換えてください。大事なのはリンクが0件のときに例外にしていることです。サイトの構造が変わると、エラーは出ずに「新着0件」として静かに終わります。毎日「0件」が続いても誰も気づかないので、0件は異常として扱います。

def summarize(client, job):
    res = client.responses.create(
        model=os.environ["OPENAI_MODEL"],
        instructions="求人を日本語3行で要約する。報酬・期限・必要なスキルを必ず含める。",
        input=f"タイトル: {job['title']}\n本文: {job['body'][:3000]}",
        max_output_tokens=300,
    )
    return res.output_text.strip()

要約のブロック。ここが「以前の書き方」から一番変わった部分です。以前はモジュール関数 openai.ChatCompletion.create を呼び、戻り値を res["choices"][0]["message"]["content"] と辞書のように辿っていました。今は OpenAI() で作ったクライアントの client.responses.create を呼び、結果の文章は res.output_text で取り出します。instructions がAIへの役割指示、input が要約させる本文です。本文は3,000文字で切り、出力も max_output_tokens=300 で抑えて、1件あたりの料金の上限を決めています。

すでに会話形式(messages)で書かれた既存コードを生かしたい場合は、Chat Completions の今の書き方でも動きます。

res = client.chat.completions.create(
    model=os.environ["OPENAI_MODEL"],
    messages=[
        {"role": "system", "content": "求人を日本語3行で要約する。"},
        {"role": "user", "content": job["body"][:3000]},
    ],
)
text = res.choices[0].message.content

違いは戻り値の取り出し方で、res.choices[0].message.content と属性で辿ります。辞書の書き方(res["choices"])のまま残っていると、ここで止まります。

def post_slack(text):
    res = requests.post(os.environ["SLACK_WEBHOOK_URL"],
                        json={"text": text}, timeout=30)
    res.raise_for_status()

def main():
    seen = set(json.loads(SEEN_FILE.read_text())) if SEEN_FILE.exists() else set()
    client = OpenAI(timeout=60, max_retries=2)   # キーは環境変数 OPENAI_API_KEY から読まれる

    new_urls = [u for u in fetch_jobs() if u not in seen][:MAX_ITEMS]
    log.info("新着 %d 件を処理します", len(new_urls))

    failed = 0
    for url in new_urls:
        try:
            job = fetch_body(url)
            summary = summarize(client, job)
            post_slack(f"*{job['title']}*\n{summary}\n{url}")
            seen.add(url)
        except (requests.RequestException, APIError, RuntimeError, AttributeError) as e:
            failed += 1
            log.error("失敗: %s (%s)", url, e)

    SEEN_FILE.parent.mkdir(parents=True, exist_ok=True)
    SEEN_FILE.write_text(json.dumps(sorted(seen), ensure_ascii=False))

    if failed:
        log.error("%d 件失敗しました", failed)
        sys.exit(1)

if __name__ == "__main__":
    main()

実行のブロック。OpenAI() は引数を渡さなければ環境変数 OPENAI_API_KEY を自動で読むので、コードにキーは一切出てきません。timeout=60 と max_retries=2 で、一時的な通信エラーは再試行し、それでも駄目なら例外として返します。

例外は種類を絞って受け、1件の失敗で全体を止めずに続ける一方、失敗はログに残し、最後に終了コード1で終わるようにしています。終了コードが1になると、cron のメールや systemd の状態に「失敗」として現れるので、翌朝気づけます。Slack への送信に成功したURLだけを処理済みにしているため、失敗した求人は翌日もう一度試されます。

Slackへの送り方そのものをAIに書き直させる手順は、LINE Notify代替をAIで書き直す|26年10月でも扱っています。

APIキーの置き場所と課金の上限は「コードの外」で決める

APIキーは環境変数で渡し、ファイルの権限を絞る

毎日の定期実行では、キーを1つのファイルにまとめ、cron や systemd からそのファイルを読み込ませるのが扱いやすい方法です。

# /home/username/.config/job-digest/env
OPENAI_API_KEY=ここに発行したキー
OPENAI_MODEL=ここに使うモデルのID
SLACK_WEBHOOK_URL=ここにWebhookのURL
chmod 600 /home/username/.config/job-digest/env
  • スクリプトと同じフォルダに置かない。フォルダごとAIに読ませたり、共有したりしたときに一緒に渡ってしまいます
  • 権限は600(自分だけ読み書き可)。同じマシンの他のユーザーから読めないようにします
  • このスクリプト専用のキーを発行する。漏れたときに、そのキーだけを無効にすれば済みます
  • AWS Secrets Manager・Google Secret Manager・Azure Key Vault のような専用サービスもありますが、個人の日次スクリプトなら上のファイル+権限で十分なことが多いです

モデルの選び方で1日の料金は100倍変わる

調査時点(2026年10月15日)のOpenAIの料金で、「1日20件、1件あたり入力1,000トークン・出力300トークン」を要約した場合を計算すると次のとおりです(入力2万トークン・出力6,000トークン)。

モデル

入力(1Mトークンあたり)

出力(1Mトークンあたり)

1日あたりの目安

GPT-6 Luna(2026年9月22日リリース)

$0.10

$0.50

約$0.005

GPT-6.1 Sol(2026年9月29日リリース)

$2.00

$10.00

約$0.10

GPT-6 Astra(2026年9月3日リリース)

$10.00

$50.00

約$0.50

求人の要約のように「決まった形で短くまとめる」仕事なら、最も安い GPT-6 Luna でまず試し、物足りなければ上げる順番で十分です。AIにコードを書かせると、知っている中で一番高性能なモデル名を入れてくることがあるので、モデルは必ず自分で決めて環境変数に入れます。モデルのIDは公式ドキュメントのモデル一覧で確認してください(この記事ではIDの表記を推測で書きません)。

また、調査結果によれば、OpenAIの GPT-5.6 Sol は以前のモデルより入力単価が上がっています。新しいモデルほど安いとは限らないので、モデルを切り替えるときは料金表を見直してください。

課金の上限は二重にかける

  1. コード側の上限:MAX_ITEMS(呼ぶ回数)と max_output_tokens・本文の文字数カット(1回あたりの量)。バグで無限ループしても、1回の実行で使える額に天井ができます
  2. 管理画面側の上限:OpenAIの管理画面で、利用額の上限や通知が設定できるかを確認し、設定しておきます。コードの上限が効かない経路(キーの漏えいなど)への備えです

取得先のサイトに迷惑をかけないための3つの確認

要約を自動化すると、取得そのものが「おまけ」に見えて雑になりがちです。毎日動かすなら、取得先から見て問題のない形にしておく必要があります。

  • robots.txt:上のコードは RobotFileParser で禁止されたURLに行きません。2026年7月7日に欧州データ保護会議(EDPB)が採択したガイドライン03/2026では、robots.txt や CAPTCHA、ログインの壁といった技術的な信号が、スクレイピングの正当性を評価する際の材料として扱われるとされています。無視してよい形式的なファイルではなくなっています
  • 利用規約:求人サイトには自動取得や転載を禁じているものがあります。公式のAPIやRSSが用意されていればそちらを使います。ログインが必要なページや、CAPTCHA・アクセス制限を回避して取りに行くことはしません
  • アクセス間隔:1ページごとに3秒以上空け、1回の件数も絞ります。User-Agent に連絡先を入れておくと、問題があったときに相手が連絡できます

AIが書いたコードには、time.sleep が無い・並列で一斉に取りに行く・ブラウザのふりをするヘッダーを入れる、といった書き方が混ざることがあります。見つけたら「アクセス間隔を3秒以上空け、並列取得はしないでください」と指示して直させてください。

毎日止めずに動かすなら、手元PC・共有サーバーのcron・VPSのどれか

実行場所ごとの「止まる条件」

実行場所

向いている条件

止まる条件

手元のPC

まず数日試したい

スリープ・電源オフ・ログアウト中は実行されない。閉じた日はその日の分が丸ごと飛ぶ

共有レンタルサーバーのcron

1回数分で終わり、追加のパッケージを入れられる

Pythonのバージョンが古い・pipで入れられない・実行時間や外部への通信に制限がある場合は動かない。事前にサーバーの仕様を確認する

VPS(systemd timer)

毎日確実に時刻通り動かしたい

サーバーの更新・監視は自分の責任。失敗を検知する仕組みが無いと、止まっていても気づけない

手元にサーバーが無く、無料で試したいなら GitHub Actions の定期実行という手もあります。手順はGitHub Actions cronスクレイピング|26年10月にまとめています。

cronで動かす場合

0 8 * * * set -a; . /home/username/.config/job-digest/env; set +a; /home/username/jobs/.venv/bin/python /home/username/jobs/digest.py >> /home/username/jobs/digest.log 2>&1

set -a は、読み込んだ値を環境変数としてPythonに渡すための指定です。これが無いと、ファイルを読んでもPythonからは見えず、OPENAI_API_KEY が無いという理由で毎朝失敗します。Pythonは仮想環境(.venv)のものをフルパスで指定してください。cron は普段のターミナルと違う環境で動くので、python3 とだけ書くと別のPython(openai が入っていない方)が使われることがあります。

VPSでsystemd timerを使う場合

# ~/.config/systemd/user/job-digest.service
[Unit]
Description=job digest

[Service]
Type=oneshot
EnvironmentFile=/home/username/.config/job-digest/env
ExecStart=/home/username/jobs/.venv/bin/python /home/username/jobs/digest.py
# ~/.config/systemd/user/job-digest.timer
[Unit]
Description=job digest daily

[Timer]
OnCalendar=*-*-* 08:00:00
Persistent=true

[Install]
WantedBy=timers.target
systemctl --user daemon-reload
systemctl --user enable --now job-digest.timer
loginctl enable-linger username

EnvironmentFile でキーのファイルを読み込みます。Persistent=true は、サーバーが止まっていた時間の分を起動時に実行する指定です。enable-linger を入れないと、ログアウトした時点でタイマーも止まります。ログは journalctl --user -u job-digest.service で見られます。

「止まったことに気づける」ところまでが定期実行

  • 終了コードで失敗を外に出す:上のコードは失敗があれば終了コード1で終わるので、systemctl --user status job-digest.service に失敗として残ります
  • 「成功したのに何も届かない」を疑う:サイトの構造が変わると、エラーにならずに0件で終わることがあります。上のコードでは0件を例外にしていますが、Slackに数日何も来なければ、まずログを見てください
  • モデルの提供終了に備える:モデル名を環境変数に出しておけば、o4-mini のように提供が終わったときも、コードを触らずにファイルの1行を書き換えるだけで済みます
  • パッケージを更新したら一度手で動かす:pip install -U openai の後は、定期実行に任せる前に手動で1回実行して、結果がSlackに届くことを確かめます

関連する選択肢

毎日決まった時刻にスクリプトを動かすだけなら、共有のレンタルサーバーでも足ります。cronが使えるプランを選べば、自分でOSを管理する必要はありません。

レンタルサーバー エックスサーバー

※ 広告を含みます(A8.net)。リンク経由でお申し込みがあった場合、手数料を受け取ることがあります。

まとめ:AIに直させるときは「使う書き方」と「禁止する書き方」を指示文に書く

ChatGPTやClaude Codeに書かせた要約スクリプトが動かないとき、まず疑うのは openai.ChatCompletion.create と openai.api_key = です。openai 1.0(2023年11月)以降は、OpenAI() でクライアントを作り、client.responses.create(結果は output_text)か client.chat.completions.create(結果は choices[0].message.content)を使います。

AIに直させるときは「直して」だけで済ませず、使う書き方・禁止する書き方・キーとモデル名を環境変数から読むこと・件数とアクセス間隔の上限・except Exception: pass の禁止を、指示文に書き切ります。料金はモデルで大きく変わり、同じ20件でも GPT-6 Luna と GPT-6 Astra では約100倍の差です。上限はコードと管理画面の二重でかけます。

毎日動かすなら、手元のPCはスリープで止まり、共有サーバーは仕様次第、VPSは確実ですが監視は自分持ちです。どこで動かすにしても、失敗を終了コードとログで外に出し、止まったことに気づける形にしてから任せてください。