本ページはプロモーションが含まれています。

Pythonで書いたスクレイピングや集計を「1日1回だけ」動かしたいとき、GitHub Actionsのschedule(cron構文)なら月額0円で始められます。ただ、実行時刻のずれ、private無料枠2,000分/月、60日無更新での自動停止など、レンタルサーバーやVPSにはない止まり方があります。この記事では、GitHub Actionsで毎日Pythonを動かす今の書き方をコード付きで示し、どの条件になったらレンタルサーバーcronやVPSを契約すべきかを料金つきで整理します。

GitHub Actionsのscheduleで1日1回Pythonを動かす今の書き方(Python 3.12)

実行環境とインストールするパッケージ

  • Python 3.12(GitHub Actions・レンタルサーバー・VPSで同じバージョンに揃える。レンタルサーバーはPythonのバージョンを自分で選べないことが多いので、先に python3 --version で確認し、Actions側をそれに合わせる)
  • pip install requests beautifulsoup4

GitHub Actionsは毎回まっさらな環境に pip install し直します。バージョンを固定しないと、ある日ライブラリが更新されて挙動が変わっても気づけません。手元で動作確認したら pip show requests と pip show beautifulsoup4 でバージョンを確認し、requirements.txt に == で書いておきます(下の例のバージョン番号は、ご自身の pip show の結果に置き換えてください)。

requests==(pip show requests の Version)
beautifulsoup4==(pip show beautifulsoup4 の Version)

Pythonスクリプト:どこで動かしても同じコードにする

ポイントは、GitHub Actions専用の書き方をしないことです。後でレンタルサーバーやVPSに移すとき、スクリプトを書き直さずに済みます。

import csv
import os
import sys
import time
from datetime import datetime, timedelta, timezone
from urllib import robotparser
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

BASE_URL = "https://example.com/"
TARGET_PATHS = ["/news/", "/news/page/2/"]
USER_AGENT = "daily-checker/1.0 (+https://example.com/contact)"
INTERVAL_SEC = 5
JST = timezone(timedelta(hours=9))


def load_robots():
    rp = robotparser.RobotFileParser()
    rp.set_url(urljoin(BASE_URL, "/robots.txt"))
    rp.read()
    return rp


def fetch_titles(session, url):
    resp = session.get(url, timeout=30)
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")
    return [a.get_text(strip=True) for a in soup.select("h2 a")]


def schedule_delay_minutes(now_utc):
    # schedule から起動されたときだけ、予定時刻との差を分で返す
    if os.environ.get("GITHUB_EVENT_NAME") != "schedule":
        return None
    hh, mm = map(int, os.environ["SCHEDULED_AT_UTC"].split(":"))
    scheduled = now_utc.replace(hour=hh, minute=mm, second=0, microsecond=0)
    if scheduled > now_utc:
        scheduled -= timedelta(days=1)
    return (now_utc - scheduled).total_seconds() / 60


def write_summary(lines):
    path = os.environ.get("GITHUB_STEP_SUMMARY")
    if not path:
        print("\n".join(lines))
        return
    with open(path, "a", encoding="utf-8") as f:
        f.write("\n".join(lines) + "\n")


def main():
    now_utc = datetime.now(timezone.utc)
    started = now_utc.astimezone(JST)
    rp = load_robots()
    rows = []

    with requests.Session() as session:
        session.headers["User-Agent"] = USER_AGENT
        for i, path in enumerate(TARGET_PATHS):
            url = urljoin(BASE_URL, path)
            if not rp.can_fetch(USER_AGENT, url):
                print(f"robots.txt で許可されていないためスキップ: {url}")
                continue
            if i > 0:
                time.sleep(INTERVAL_SEC)
            for title in fetch_titles(session, url):
                rows.append({"date": started.date().isoformat(), "url": url, "title": title})

    os.makedirs("output", exist_ok=True)
    out_path = f"output/titles_{started:%Y%m%d}.csv"
    with open(out_path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=["date", "url", "title"])
        writer.writeheader()
        writer.writerows(rows)

    delay = schedule_delay_minutes(now_utc)
    write_summary([
        f"- 開始(日本時間): {started:%Y-%m-%d %H:%M}",
        f"- 予定時刻からの遅れ: {delay:.1f} 分" if delay is not None else "- 手動実行",
        f"- 取得件数: {len(rows)}",
    ])

    if not rows:
        sys.exit("1件も取得できませんでした。ページ構造が変わった可能性があります。")


if __name__ == "__main__":
    main()

ブロックごとの役割は次のとおりです。

  • 定数部分:取得先、User-Agent(どこの誰のアクセスかを相手に伝える名乗り)、ページ間の待ち時間をまとめています。待ち時間は5秒にしています。
  • load_robots():標準ライブラリの urllib.robotparser で robots.txt(サイト側が「ここは機械で取らないで」と示すファイル)を読み、取得してよいURLかを判定します。1回の実行で1回だけ読みます。
  • fetch_titles():timeout=30 を必ず付けます。付けないと相手が応答しないときに処理が終わらず、Actionsの実行時間(無料枠)を食い続けます。
  • schedule_delay_minutes():予定時刻と実際の開始時刻の差を計算します。GitHub Actionsのscheduleは予定どおりに始まるとは限らないため、自分のリポジトリでどれだけ遅れているかを毎日記録します。
  • write_summary():環境変数 GITHUB_STEP_SUMMARY があればActionsの実行結果画面に表示し、無ければ標準出力に出します。レンタルサーバーやVPSではcronのログにそのまま残るので、同じコードが動きます。
  • 最後の sys.exit():0件だったら失敗として終了します。ページ構造が変わって空のCSVを毎日作り続ける、という気づきにくい壊れ方を防ぎます。Actionsは失敗すると通知メールが届きます。

ワークフロー定義:.github/workflows/daily.yml

name: daily-scrape

on:
  schedule:
    - cron: "17 22 * * *"   # UTC 22:17 = 日本時間 7:17
  workflow_dispatch:         # 画面から手動実行できるようにする

concurrency:
  group: daily-scrape
  cancel-in-progress: false

permissions:
  contents: read

jobs:
  run:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip
      - run: pip install -r requirements.txt
      - run: python daily_scrape.py
        env:
          SCHEDULED_AT_UTC: "22:17"
      - uses: actions/upload-artifact@v4
        with:
          name: titles-${{ github.run_id }}
          path: output/
          retention-days: 30
  • cron: "17 22 * * *":scheduleの時刻はUTC(協定世界時)です。日本時間から9時間引いて書きます。毎時0分は実行が集中しやすいとされているので、17分のような半端な分にずらします。
  • workflow_dispatch:翌朝まで待たずに画面から動作確認できるようにします。
  • concurrency:前回の実行が終わる前に次が始まって、同じサイトへ二重にアクセスするのを防ぎます。
  • permissions: contents: read:結果をリポジトリに書き込まないので、読み取り権限だけにします。
  • timeout-minutes: 10:何かで止まったときに無料枠を使い切らないよう、上限を切ります。
  • cache: pip:setup-pythonのpipキャッシュで、毎回のインストール時間を短くします。
  • upload-artifact:CSVを実行結果に添付して保存します。retention-days で保存日数を指定します。

アクションのメジャーバージョン(@v4 など)は、各アクションのGitHub Marketplaceのページ・リリースページで現行のものを確認してから書いてください。ubuntu-latest は中身のOSバージョンが時期によって切り替わるので、挙動を固定したい場合は ubuntu-24.04 のように明示します。

以前の書き方と今の書き方

用途

以前の書き方

今の書き方

結果ファイルの保存

Actionsから git commit・git push でリポジトリに積む(contents: write が必要、履歴が膨らむ)

actions/upload-artifact@v4 で実行結果に添付。v3は廃止されて動かない

ステップ間の値の受け渡し

echo "::set-output name=count::10"

echo "count=10" >> "$GITHUB_OUTPUT"

依存パッケージ

毎回 pip install requests(バージョン未指定)

requirements.txt で == 固定+ cache: pip

実行結果の確認

ログを開いて print を探す

GITHUB_STEP_SUMMARY に開始時刻・遅れ・件数を書き出す

60日停止対策

ダミーの更新を定期的にコミットして延命する

停止条件(次節)を理解したうえで、privateにする・通知を受けて手動で再有効化する

2,000分/月・実行遅延・60日停止:GitHub Actionsで足りる条件

無料枠2,000分/月は「1日1回」なら余る

privateリポジトリでは、GitHub Freeの無料枠は月2,000分です。実行時間はジョブ単位で分に切り上げて数えられるので、1回3分かかる処理を毎日動かしても、3分×31日=93分です。1日1回のスクレイピングで2,000分を超えるのは、1回の処理が60分を超えるような重い処理くらいです。

使用量は、GitHubのSettings → Billing(請求)の画面で確認できます。GitHub Actionsの料金体系は2026年1月に改定されているため、無料枠を超えた場合の単価は、本記事では具体的な数字を書かず、GitHub公式ドキュメントの「Billing for GitHub Actions」で最新の値を確認してください。publicリポジトリでGitHubが用意する標準のランナー(実行用の仮想マシン)を使う場合は、無料枠の分数を消費しません。

実行遅延:予定時刻に始まる保証はない

GitHub公式ドキュメントには、scheduleイベントは混雑時に遅れることがあり、混雑の度合いによっては実行されない場合がある、と書かれています。何分遅れるかの公式な数値はありません。先ほどのスクリプトで実行結果画面に「予定時刻からの遅れ」を毎日書き出しているのはこのためで、2〜3週間分を見れば、自分の処理にとって許容できる範囲かを判断できます。

  • 「朝のうちにCSVがあればよい」程度なら、遅れは問題になりにくい
  • 「7時ちょうどに取らないと意味がない」「1日でも抜けると困る」なら、GitHub Actionsは向かない

60日無更新での自動停止

GitHub公式ドキュメントでは、publicリポジトリで60日間リポジトリへの活動が無いと、scheduleのワークフローが自動で無効化されると説明されています。スクリプトが完成して触らなくなった頃に止まるのが厄介な点です。無効化されたワークフローは、リポジトリのActionsタブから再度有効にできます。

対処は次のどちらかです。

  • privateリポジトリにする:60日停止の説明はpublicリポジトリが対象。代わりに無料枠2,000分/月を消費するが、1日1回なら前述のとおり余る
  • publicのまま使う:60日以内に何らかの更新が入る運用にするか、無効化の通知を受けたら手動で再有効化する

適用条件は変わる可能性があるので、運用を始める前にGitHub公式ドキュメントの「Disabling and enabling a workflow」の記述を確認してください。

GitHub Actionsで足りるかの判定表

条件

GitHub Actionsで足りる

サーバーを検討する

実行頻度

1日1回〜数回

数分おき・常時監視

実行時刻のずれ

遅れても・まれに抜けても困らない

時刻厳守・抜けは許されない

1回の処理時間

数分〜数十分

長時間・常駐が必要

前回の結果の利用

使わない、またはartifactの保存期間内で足りる

前日との差分をずっと保持したい

取得先の条件

クラウド上の実行環境からのアクセスが利用規約上問題ない

利用規約・アクセス元の条件をActionsでは満たせない

月額

0円

レンタルサーバー550円〜/VPS687円〜(次節)

レンタルサーバーcron(550円〜)・VPS(687円〜)に移す条件と止まる条件

2026年11月時点の料金とcron・常駐の可否

以下は2026年11月時点の調査結果です。キャンペーンや契約期間で変わるため、契約前に各公式サイトで確認してください。

種別

サービス・プラン

月額(税込)

スペック

cron最短間隔

常駐プロセス

root権限

レンタルサーバー

ロリポップ! ハイスピード

550円(36ヶ月契約時)

Web容量300GB

5分

原則不可

なし

レンタルサーバー

シン・レンタルサーバー ベーシック

990円(キャンペーン適用)

10コア / 14GB / 300GB

5分

不可

なし

レンタルサーバー

エックスサーバー スタンダード

1,320円

10コア / 14GB / 300GB

5分

不可

なし

VPS

さくらのVPS 512プラン

687円(初期費用無料)

1vCPU / 512MB / 25GB

1分

可

あり

VPS

Ablenet VPS VZ4プラン

770円

1vCPU / 1GB / 20GB

1分

可

あり

VPS

Xserver VPS 1GBプラン

880円

2vCPU / 1GB / 50GB

1分

可

あり

VPS

ConoHa VPS 1GBプラン

990円(VPS割で670円〜)

1vCPU / 1GB / 50GB

1分

可

あり

無料期間は、さくらのVPSが初回2週間のお試し、ConoHa VPSが初回最大2ヶ月無料キャンペーン、シン・レンタルサーバーが初回2ヶ月無料と調査時点ではなっています。GitHub Actionsで回しながら遅延の記録を取り、同時にお試し期間中のサーバーでも同じスクリプトを動かして比べると、判断材料がそろいます。

レンタルサーバーcronに移す場合

「時刻どおりに動いてほしいが、処理は数分で終わる」ならレンタルサーバーで足ります。スクリプトは前節のものをそのまま使えます。GITHUB_STEP_SUMMARY が無い環境では標準出力に出るので、cronでログファイルに追記します。

python3 -m venv /home/username/venv
/home/username/venv/bin/pip install -r /home/username/scripts/requirements.txt
17 7 * * * cd /home/username/scripts && /home/username/venv/bin/python daily_scrape.py >> /home/username/logs/daily.log 2>&1
  • レンタルサーバーのcronは通常サーバーのタイムゾーンで動くので、日本時間で書けます(念のため date コマンドで確認)
  • venv(プロジェクト専用のPython環境)を作り、そのPythonをフルパスで指定する。cronは対話シェルと環境変数が違うため、python3 とだけ書くと別のPythonが動くことがある
  • 最後の 2>&1 でエラーもログに残す

止まる条件:常駐プロセスが不可で、長時間動く処理はサーバー側に打ち切られることがあります。Pythonのバージョンはサーバー側の提供範囲に縛られます。打ち切られる処理の分け方は レンタルサーバー cron 強制終了|30分バッチ 26年11月 で、失敗をメールで受け取る仕組みは cron メール通知 置き場所|25番ポートと26年11月料金 で扱っています。

VPSに移す場合

常駐が必要、ブラウザ自動操作のためにOSのパッケージを入れたい(root権限が要る)、数分おきに動かしたい、という条件が1つでもあればVPSです。月額はさくらのVPSの687円から、メモリ1GBが欲しければAblenet VPS 770円・Xserver VPS 880円・ConoHa VPS 990円(VPS割670円〜)が候補になります。

VPSではcronも使えますが、今はsystemdタイマーで書くと、サーバーが止まっていて実行できなかった回を起動後に取り戻せます。

# /etc/systemd/system/daily-scrape.service
[Unit]
Description=daily scrape

[Service]
Type=oneshot
User=username
WorkingDirectory=/home/username/scripts
ExecStart=/home/username/venv/bin/python daily_scrape.py
# /etc/systemd/system/daily-scrape.timer
[Unit]
Description=run daily scrape at 7:17

[Timer]
OnCalendar=*-*-* 07:17:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now daily-scrape.timer
systemctl list-timers daily-scrape.timer
journalctl -u daily-scrape.service
  • serviceファイル:何を・どのユーザーで・どのディレクトリで実行するかを書きます。Type=oneshot は「1回動いて終わる処理」の意味です。
  • timerファイル:OnCalendar で毎日7:17を指定し、Persistent=true で停止中に逃した回を次の起動時に実行します。
  • コマンド:有効化して次回の実行予定を確認し、出力は journalctl で読みます。ログファイルの置き場所を自分で管理しなくて済みます。

止まる条件:OSのアップデートやセキュリティ設定は自分の責任です。更新を放置して乗っ取られる、ディスクが満杯になる、再起動後にタイマーを有効化し忘れていた、というのがVPSの典型的な止まり方です。

手元のPCで動かす場合

月額0円で、取得先の条件を満たしやすいのが手元のPCです。ただし、スリープ・電源オフ・OS更新による再起動の間は動きません。「PCを使っている日だけ取れればよい」用途に限られ、毎日欠かさず動かす目的なら上の3つのどれかを選びます。

止まる条件の一覧

環境

月額

止まる条件

GitHub Actions

0円(private無料枠2,000分/月)

混雑時の遅延・未実行、publicで60日無活動による無効化、無料枠超過

レンタルサーバーcron

550円〜

長時間処理の打ち切り、常駐不可、Pythonのバージョンがサーバー依存

VPS

687円〜

OS管理の放置、ディスク満杯、再起動後の設定漏れ

手元のPC

0円(電気代のみ)

スリープ・電源オフ・更新による再起動

取得先サイトへの配慮:robots.txt・利用規約・5秒間隔

どの環境で動かすかより先に、取ってよいデータかを確かめます。1日1回の処理でも、取得先にとっては毎日のアクセスです。

  • robots.txt:前述のスクリプトのように urllib.robotparser で毎回確認し、許可されていないURLは取りに行かない。
  • 利用規約:自動取得の禁止、取得したデータの再配布・商用利用の制限が書かれていないかを読む。APIが提供されているなら、そちらを使う。
  • アクセス間隔:ページ間は数秒空ける(記事の例では5秒)。並列化してまとめて取りに行かない。必要なページだけに絞る。
  • 名乗る:User-Agentに用途と連絡先を書き、相手が問い合わせられるようにする。
  • 拒否されたら止める:アクセス制限やブロックを受けたら、アクセス元を変えるなどして回り込まない。取得をやめるか、運営者に許可を求める。GitHub Actionsのようなクラウド上の実行環境からのアクセスを規約で認めていないサイトでは、Actionsを使わない判断をする。

1日1回で足りる処理をわざわざ短い間隔で回さないことも配慮の一つです。VPSに移してcronが1分間隔で使えるようになっても、取得の頻度は必要な分だけにとどめます。

関連する選択肢

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

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

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

1日1回の処理はGitHub Actionsから始め、止まる条件で乗り換える

1日1回動かすだけのPythonなら、まずGitHub Actionsのscheduleで月額0円から始めるのが合理的です。privateリポジトリの無料枠2,000分/月は、数分で終わる処理なら大きく余ります。書き方は、UTCで半端な分を指定したcron、timeout-minutes、upload-artifact@v4 での保存、GITHUB_STEP_SUMMARY への遅延記録が今の基本です。乗り換えの判断は止まる条件で決めます。遅延や未実行が許されないならレンタルサーバーcron(ロリポップ!550円〜)、常駐・root権限・数分おきの実行が要るならVPS(さくらのVPS 687円〜)です。スクリプトをどの環境でも動く形で書いておけば、移行はcronかsystemdタイマーの設定を足すだけで済みます。