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

Pythonで書いた価格監視スクリプトを5分おきに動かしたいとき、レンタルサーバーのcronで足りるのか、VPSを借りるべきかは迷いやすいところです。さくらのレンタルサーバ・ConoHa WING・ロリポップ!は、cronの最短間隔も1回あたりの実行時間の上限もプランごとに違います。この記事では、5分おきに止まらず動かすための書き方と、自分のスクリプトの実行時間を測る方法を紹介します。そのうえで、共有サーバーで続けるか、VPSに移るかを判断する基準を示します。

5分おきcronで動かす価格監視スクリプト(Python 3.10以上・requests+BeautifulSoup)

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

  • Python:3.10以上を想定しています(型ヒントの書き方に合わせた条件です)。サーバーでは python3 --version でバージョンを確認してください。
  • パッケージ:pip install requests beautifulsoup4。この記事で使う機能は基本的なものだけなので、バージョンの指定は不要です。導入されたバージョンは pip show requests beautifulsoup4 で確認できます。
  • OS:ファイルロックに fcntl を使うため、Linux(共有サーバー・VPS)が前提です。

共有サーバーによっては python3 -m venv(venv=プロジェクト専用のPython環境を作る機能)が使えない場合があります。そのときは pip install --user で入れてください。どちらが使えるかは各社のマニュアルで確認するのが確実です。

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

以前は、次の1行をcronに登録するだけで済ませる書き方が一般的でした。

*/5 * * * * python watch.py

この書き方は、5分おきという短い間隔で動かすと次の理由で止まりやすくなります。

  • cronはホームディレクトリから実行され、PATHも最小限です。そのため python や watch.py が見つからないことがあります。
  • requests.get() に timeout を付けないと、相手の応答が遅いときに処理が終わりません。共有サーバーでは実行時間の上限に達して強制終了されます。
  • 前の回が終わる前に次の回が始まると、プロセスが重なってサーバーのリソース制限に触れます。

今は、次の4点をスクリプト側で必ず入れる書き方にしています。絶対パスで書く・timeoutを付ける・二重起動を防ぐ・毎回の実行時間を記録する。

スクリプト全体(watch.py)

import fcntl
import json
import logging
import sys
import time
from pathlib import Path
from urllib import robotparser
from urllib.parse import urlparse

import requests
from bs4 import BeautifulSoup

BASE_DIR = Path(__file__).resolve().parent
STATE_FILE = BASE_DIR / "state.json"
LOCK_FILE = BASE_DIR / "watch.lock"
LOG_FILE = BASE_DIR / "watch.log"

TARGET_URL = "https://example.com/item/123"
PRICE_SELECTOR = ".price"
USER_AGENT = "pricewatch/1.0 (+https://example.com/contact)"
TIMEOUT = (5, 20)  # (接続, 読み込み) 秒
ROBOTS_TTL = 24 * 60 * 60  # robots.txt は1日1回だけ取り直す

logging.basicConfig(
    filename=LOG_FILE,
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
)


def acquire_lock():
    f = open(LOCK_FILE, "w")
    try:
        fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except BlockingIOError:
        logging.warning("前回の実行が終わっていないためスキップ")
        sys.exit(0)
    return f


def load_state() -> dict:
    if STATE_FILE.exists():
        return json.loads(STATE_FILE.read_text(encoding="utf-8"))
    return {}


def save_state(state: dict) -> None:
    tmp = STATE_FILE.with_suffix(".tmp")
    tmp.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
    tmp.replace(STATE_FILE)


def robots_allowed(session: requests.Session, state: dict) -> bool:
    cached = state.get("robots")
    if cached and time.time() - cached["checked_at"] < ROBOTS_TTL:
        return cached["allowed"]
    parsed = urlparse(TARGET_URL)
    r = session.get(f"{parsed.scheme}://{parsed.netloc}/robots.txt", timeout=TIMEOUT)
    rp = robotparser.RobotFileParser()
    if r.status_code in (401, 403):
        allowed = False
    else:
        rp.parse(r.text.splitlines() if r.status_code == 200 else [])
        allowed = rp.can_fetch(USER_AGENT, TARGET_URL)
    state["robots"] = {"checked_at": time.time(), "allowed": allowed}
    return allowed


def fetch(session: requests.Session, state: dict) -> str | None:
    headers = {}
    if etag := state.get("etag"):
        headers["If-None-Match"] = etag
    if last_modified := state.get("last_modified"):
        headers["If-Modified-Since"] = last_modified
    r = session.get(TARGET_URL, headers=headers, timeout=TIMEOUT)
    if r.status_code == 304:
        return None
    r.raise_for_status()
    state["etag"] = r.headers.get("ETag")
    state["last_modified"] = r.headers.get("Last-Modified")
    return r.text


def parse_price(html: str) -> int:
    soup = BeautifulSoup(html, "html.parser")
    el = soup.select_one(PRICE_SELECTOR)
    if el is None:
        raise ValueError("価格の要素が見つからない")
    return int("".join(ch for ch in el.get_text() if ch.isdigit()))


def main() -> None:
    lock = acquire_lock()
    started = time.monotonic()
    state = load_state()
    status = "ok"
    try:
        with requests.Session() as s:
            s.headers["User-Agent"] = USER_AGENT
            if not robots_allowed(s, state):
                status = "disallowed"
                return
            html = fetch(s, state)
            if html is None:
                status = "not_modified"
            else:
                price = parse_price(html)
                prev = state.get("price")
                if prev is not None and price != prev:
                    logging.info("価格変動 %s -> %s", prev, price)
                state["price"] = price
    except Exception:
        status = "error"
        logging.exception("取得失敗")
    finally:
        save_state(state)
        logging.info("status=%s elapsed=%.2f", status, time.monotonic() - started)
        lock.close()


if __name__ == "__main__":
    main()

ブロックごとの説明

  • 定数の部分:ファイルの置き場所はすべて Path(__file__) を基準にした絶対パスにしています。cronがどのディレクトリから起動しても、同じ場所を読み書きできます。TIMEOUT は「接続までの秒数」と「応答を読み終えるまでの秒数」の組です。これを付けておけば、相手が応答しないときも処理が必ず終わります。
  • acquire_lock:flock(ファイルを使った排他ロック)を待たずに取りにいきます。取れなかったときは「前の回がまだ動いている」と判断し、何もせずに終了します。5分おきの実行で処理が重なり、プロセスが積み上がるのを防ぐための処理です。
  • load_state / save_state:前回の価格・ETag・robots.txtの判定結果をJSONファイルに保存します。保存するときは一時ファイルに書いてから置き換えるので、途中で強制終了されてもファイルが壊れません。
  • robots_allowed:robots.txtを毎回取りにいくと、それだけでリクエスト数が倍になります。そこで1日1回だけ取得し、結果をキャッシュしています。401・403が返ってきたときは「取得禁止」として扱います。
  • fetch:前回受け取ったETag・Last-Modifiedを付けて送る条件付きリクエストです。ページが変わっていなければ相手は304(変更なし)を返すだけなので、本文の転送が発生しません。相手のサーバーへの負担を減らすための書き方です。
  • parse_price:CSSセレクタで価格の要素を探し、数字だけを取り出します。要素が見つからないときは例外にします。サイトの構造が変わったことにログで気づけるようにするためです。
  • main:成功・失敗・変更なしのどの場合でも、最後に status=… elapsed=… を1行記録します。この elapsed(実行にかかった秒数)が、次の節でサーバーを選ぶときの材料になります。

cronへの登録(共有サーバー・VPS共通)

*/5 * * * * /home/username/pricewatch/.venv/bin/python /home/username/pricewatch/watch.py >> /home/username/pricewatch/cron.log 2>&1

Pythonもスクリプトもフルパスで指定し、標準出力とエラー出力は cron.log に追記します。コントロールパネルの画面からcronを登録する形式のサーバーでも、入力するコマンドは同じです。venvを使わない場合は、which python3 で表示されたパスに置き換えてください。

さくらのレンタルサーバ・ConoHa WING・ロリポップ!のcron最短間隔・実行時間制限・月額の確認方法

この記事に数値を載せていない理由

3社のcronの最短間隔、1回あたりの実行時間の上限、2026年11月時点の月額は、プランや契約期間によって変わり、改定されることもあります。この記事では、公式ページで確認できていない数値を推測で書くことはしません。代わりに、何をどこで確認すればよいかを表にまとめました。契約する前に、この表の空欄を公式ページで埋めてください。

確認項目

さくらのレンタルサーバ

ConoHa WING

ロリポップ!

5分おき監視での判定

cronが使えるプラン

公式マニュアルで確認

公式マニュアルで確認

公式マニュアルで確認

契約予定のプランで使えること

cronの最短実行間隔

公式マニュアルで確認

公式マニュアルで確認

公式マニュアルで確認

5分以下であること

1回あたりの実行時間の上限

公式マニュアル・利用規約で確認

公式マニュアル・利用規約で確認

公式マニュアル・利用規約で確認

実測した最大値に十分な余裕があること

登録できるcronの件数

公式マニュアルで確認

公式マニュアルで確認

公式マニュアルで確認

監視する対象の数だけ登録できること

Pythonのバージョン・venvが使えるか

SSHで python3 --version

SSHで python3 --version

SSHで python3 --version

3.10以上

月額(税込・契約期間)

公式の料金ページ

公式の料金ページ

公式の料金ページ

VPSの月額と比べる

公式ページで調べるときのコツ

  • 各社のサポートサイト(マニュアル)で「cron」「定期実行」「スケジュール」と検索します。
  • 実行時間の上限は、cronの説明ページではなく、利用規約や「禁止事項」「リソース制限」のページに書かれていることがあります。
  • 月額は、契約期間(1か月・12か月・36か月など)やキャンペーンによって表示額が変わります。税込かどうか、どの契約期間の金額かまで書き取っておいてください。
  • 確認した日付をメモしておきます。仕様が改定されたときに、どの時点の情報かを判断できるからです。

実行時間の実測と、VPSへ移る境目(常駐プロセス・root権限・実行時間制限)

elapsedのログから最大値と95パーセンタイルを出す

少なくとも数日、できれば1週間動かしてから集計します。相手サイトが混み合う時間帯の遅さも含めた数値を取るためです。

grep -o 'elapsed=[0-9.]*' /home/username/pricewatch/watch.log \
  | cut -d= -f2 | sort -n \
  | awk '{a[NR]=$1} END {i=int(NR*0.95); if(i<1)i=1; print "count", NR; print "p95", a[i]; print "max", a[NR]}'
  • count:記録された実行回数です。5分おきなら1日288回が目安になります。これより明らかに少ない場合は、cronが止まっているか、ロックによるスキップが多発しています。
  • p95:95パーセンタイル(実行時間を短い順に並べたとき、95%目にあたる値)です。普段の重さを表します。
  • max:最も遅かった回です。実行時間の上限と比べるのはこの値です。

あわせて grep -c "スキップ" watch.log で、前の回と重なった回数も数えてください。スキップが出ているなら、5分以内に終わらない回があるということです。

共有サーバーで続けられる条件

  • cronの最短間隔が5分以下である
  • 実測の max が、実行時間の上限に対して十分に短い(相手サイトが一時的に遅くなっても上限に届かない)
  • 処理が「起動→取得→保存→終了」で完結し、常駐プロセス(ずっと動き続けるプログラム)が要らない
  • pip install --user かvenvで必要なパッケージがそろい、apt などroot権限(サーバー全体の管理者権限)が必要なインストールがない

VPSへ移る境目

起きること

共有サーバーで止まる理由

VPSでの扱い

5分より短い間隔で動かしたくなった

cronの最短間隔は事業者が決めており、利用者は変えられない

間隔は自分で決められる

JavaScriptで描画されるページを、ヘッドレスブラウザ(画面を出さずに動くブラウザ)で取得したい

ブラウザ本体や依存ライブラリの導入にroot権限が要ることが多い。メモリ・実行時間の制限にも触れやすい

root権限で必要なものを入れられる

監視対象が増えて max が伸びた

実行時間の上限を超えると、処理の途中で強制終了される

上限は自分で決める(後述の TimeoutStartSec)

WebSocketなどで変化を待ち続けたい

常駐プロセスは規約で禁止されているか、制限で止められる

systemdで常駐させられる

保存するデータが増えたときの容量の比較は、スクレイピング保存容量 共有サーバー・VPS比較|26年11月にまとめています。

VPSではcronよりsystemd timerで書く

以前はVPSでもcrontabに書いていました。今はsystemd timer(systemdに組み込まれた定期実行の仕組み)を使います。前の回が動いている間は次の回を起動しないこと、実行時間の上限、ログの保存を、設定だけで指定できるからです。

# /etc/systemd/system/pricewatch.service
[Unit]
Description=price watch

[Service]
Type=oneshot
User=username
WorkingDirectory=/home/username/pricewatch
ExecStart=/home/username/pricewatch/.venv/bin/python /home/username/pricewatch/watch.py
TimeoutStartSec=120

# /etc/systemd/system/pricewatch.timer
[Unit]
Description=run price watch every 5 minutes

[Timer]
OnCalendar=*:0/5
RandomizedDelaySec=60

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now pricewatch.timer
systemctl list-timers pricewatch.timer
journalctl -u pricewatch.service --since today
  • Type=oneshot:1回動いて終わる処理であることを示します。サービスの実行中にタイマーの時刻が来ても、二重には起動しません。
  • TimeoutStartSec:自分で決める実行時間の上限です。実測した max に余裕を持たせた値にします。
  • OnCalendar=*:0/5:毎時0分から5分おきに起動します。
  • RandomizedDelaySec:起動時刻を最大60秒ずらします。毎回きっちり同じ秒にアクセスしないようにするための設定で、相手サイトへの配慮になります。

自宅サーバーでの運用と、そこからVPSへ移す手順は自宅サーバーsystemd timer運用とVPS移行26年11月で詳しく書いています。

価格監視の取得先サイトへの配慮(robots.txt・利用規約・アクセス間隔)

  • robots.txt:スクリプトの robots_allowed で、取得してよいページかを判定しています。rp.crawl_delay(USER_AGENT) で Crawl-delay(アクセス間隔の指定)が取れる場合は、それが5分より長くないかも確認してください。長い場合は、実行間隔を指定に合わせます。
  • 利用規約:robots.txtで許可されていても、規約で自動取得を禁止しているサイトがあります。また、価格情報をAPIで提供しているサイトもあります。その場合はAPIを使ってください。
  • アクセス間隔:5分おきに1ページだけ取りにいくのがこの記事の前提です。監視する商品が増えても並列でアクセスせず、1件ずつ間隔を空けて取得します。対象が多い場合は、実行間隔のほうを広げてください。
  • 条件付きリクエスト:ETag・Last-Modifiedを使い、ページが変わっていないときは本文を転送させません。
  • 連絡先付きのUser-Agent:問題があったとき、相手が連絡できるようにしておきます。ログイン必須のページや、アクセス制限がかかっているページを回避して取得する書き方はしません。

5分おきの監視を毎日動かし続けるのは手元のPC・共有サーバー・VPSのどれか

それぞれが止まる条件

環境

向いている条件

止まる条件

手元のPC

試作中で、数日分のelapsedを測りたい段階

スリープ・電源断・OS更新による再起動。ネットワークが切り替わったとき

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

最短間隔が5分以下で、実測の max が実行時間の上限に十分収まり、root権限が要らない

実行時間の上限による強制終了。リソース制限。プラン変更や仕様改定による間隔・上限の変更。サーバー側のPython更新でvenvが壊れたとき

VPS

5分より短い間隔・ヘッドレスブラウザ・常駐プロセスが要る段階

OS更新を怠ったことによる不具合。ディスクがいっぱいになったとき(ログのローテーションが必要)。再起動後にtimerがenableされていなかったとき

「短い間隔で何度も動かす」だけで1つ選ぶなら

これから新しく契約するなら、VPSを選びます。共有サーバーでは、cronの最短間隔も実行時間の上限も事業者が決めるもので、利用者は変えられません。プランや仕様の改定で条件が変わることもあります。間隔を詰めたい、監視対象を増やしたいといった変更を、自分の判断で調整できるのはVPSだけです。

ただし、すでに共有サーバーを契約していて、前の節の表で「5分以下」「上限に十分な余裕あり」を確認できた場合は、そのまま共有サーバーで運用して問題ありません。count が1日288回に届かなくなったら、または max が上限に近づいてきたら、そのときが移行のタイミングです。

関連する選択肢

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

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

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

まとめ:5分おきの価格監視を止めずに動かすための要点

レンタルサーバーのcronで5分おきにPythonを動かすときは、絶対パス・timeout・flockによる二重起動防止・elapsedの記録を、スクリプトに最初から入れておきます。さくらのレンタルサーバ・ConoHa WING・ロリポップ!の最短間隔、実行時間の上限、月額は、契約前に公式ページで確認してください。そのうえで、自分のスクリプトを数日動かして測った max と比べます。上限に収まり、常駐もroot権限も要らなければ、共有サーバーで続けられます。間隔を詰めたい、ヘッドレスブラウザを使いたい、常駐させたいという段階になったらVPSへ移り、systemd timerで動かします。新しく1つだけ契約するなら、条件を自分で決められるVPSが確実です。どの環境でも、robots.txt・利用規約・アクセス間隔の確認を、動かし続けるための前提にしてください。