Ubuntu 22.04で、OS標準の python3 にスクレイピングのcronジョブを載せたままにしていませんか。Python 3.10は2026年10月1日にEOL(サポート終了)を迎え、同日公開の3.10.22を最後に上流の修正は止まります。この記事では、uvの uv python install 3.13・uv python pin・uv sync で作った .venv を、systemd timerの ExecStart から絶対パスで呼ぶ形に移します。OSのPythonに依存せず、止まらずに動かし続ける書き方です。

Python 3.10のEOLで「venvを作ってあるから大丈夫」が通らない理由

2026年10月時点のサポート状況

調査時点(2026年10月)の各バージョンの状況です。定期実行のスクリプトを載せ替えるなら、securityフェーズ(セキュリティ修正だけが出る期間)が長く残っているものを選びます。

バージョン

状況

直近のリリース

3.10

2026年10月1日にEOL

3.10.22(2026年10月1日・ソースコードのみ)

3.11

securityフェーズ(2027年10月まで)

3.11.17(2026年10月1日)

3.12

securityフェーズ(2028年10月まで)

3.12.15(2026年9月30日)

3.13

securityフェーズ(2029年10月まで)

3.13.16(2026年9月30日)

3.14

securityフェーズ(2030年10月まで)

3.14.8(2026年9月30日)

この記事では3.13を使います。3.14でも手順は同じで、3.13 の部分を書き換えるだけです。

以前の書き方:OSのpython3に直接載せたcron

よくある構成は次のどちらかです。

# 書き方A:OSのpython3に pip で直接入れて cron から呼ぶ
0 7 * * * /usr/bin/python3 /home/username/scraper/main.py

# 書き方B:venv を作ってから cron で呼ぶ
python3 -m venv /home/username/scraper/venv
0 7 * * * /home/username/scraper/venv/bin/python /home/username/scraper/main.py

見落とされやすいのが書き方Bです。python3 -m venv は元になったPythonをそのまま使うため、Ubuntu 22.04のOS標準 python3 から作ったvenvの中身も3.10のままです。ライブラリを隔離できても、インタプリタ(Python本体)は隔離できていません。手元で確かめるには次を実行します。

/home/username/scraper/venv/bin/python -V

ここに Python 3.10.x と出たら、そのジョブはEOLのPythonで動いています。なお、OS側のパッケージにいつまで修正が入るかはディストリビューションの方針によります。上流(python.org)の修正が止まった事実とは別に、OSのサポート情報を確認してください。

今の書き方:Python本体ごとプロジェクトに固定する

uvは、Python本体をOSのパッケージとは別に取得し、プロジェクトごとにバージョンを固定できます。OSのpython3を一切触らずに3.13へ移れるのが要点です。OSのPythonを差し替えると、OS付属のツールが壊れるおそれがあります。uv本体のインストールは公式ドキュメントの手順に従い、uv --version で入ったことを確認しておいてください。

uv python install 3.13・uv python pin・uv sync で.venvを作る

手順1:プロジェクトの定義を書く

作業ディレクトリは /home/username/scraper/ とします。まず pyproject.toml に、必要なPythonとパッケージを書きます。

[project]
name = "scraper"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
    "requests",
    "beautifulsoup4",
    "pandas>=3.0",
]
  • requires-python:このプロジェクトが3.13以上でしか動かないことを宣言します。誤って古いPythonで環境を作るのを防ぎます。
  • pandas>=3.0:pandas 3.0(2026年1月21日リリース)からCopy-on-Write(コピーの挙動を変える仕組み)が既定になりました。2系と3系では代入まわりの挙動が変わるため、手元とサーバーで別の系統が入らないよう下限を指定しています。調査時点の最新は3.0.6(2026年9月17日)です。
  • requests・beautifulsoup4は、2026年8月以降に破壊的変更の報告が確認できなかったため、バージョンを固定していません。入ったバージョンは後で uv.lock に記録されます。

手順2:Pythonを入れて固定し、環境を作る

cd /home/username/scraper
uv python install 3.13
uv python pin 3.13
uv sync
  1. uv python install 3.13:3.13をuvの管理下に取得します。sudoは要らず、ユーザーのホーム配下に入ります。
  2. uv python pin 3.13:このディレクトリで使うPythonを3.13に固定します。指定は .python-version というファイルに書かれるので、gitで一緒に管理しておくと、別のマシンでも同じバージョンで作り直せます。
  3. uv sync:pyproject.toml を読んで .venv を作り、依存パッケージを入れます。解決したバージョンは uv.lock に記録されます。

できあがったら、実際にどのPythonが入ったかを確かめます。

.venv/bin/python -V
readlink -f .venv/bin/python

1行目が Python 3.13.x であること、2行目が /usr/bin/python3.10 ではなくuvの管理下を指していることを見ます。.venv/bin/python の実体はuvが入れたPythonへのリンクなので、そのユーザーのホームにあるuv管理のPythonを消すと .venv も動かなくなります。別ユーザーでジョブを動かす場合は、そのユーザーで手順2をやり直してください。

PEP 723の形式で1ファイルに依存を書き込む方法もあります。スクリプトが1本だけなら PEP723とuv run --script 2026年10月版 の形のほうが手軽です。

手順3:定期実行するスクリプト

一覧ページから見出しとリンクを取り、日付付きのCSVに保存する最小構成です。取得先は example.com に置き換えてあります。

import sys
import time
import urllib.robotparser
from datetime import date
from pathlib import Path

import pandas as pd
import requests
from bs4 import BeautifulSoup

BASE = "https://example.com"
TARGET = f"{BASE}/news/"
USER_AGENT = "my-scraper/1.0 (+https://example.com/contact)"
OUT_DIR = Path(__file__).resolve().parent / "data"
WAIT_SECONDS = 5


def is_allowed(url):
    rp = urllib.robotparser.RobotFileParser()
    rp.set_url(f"{BASE}/robots.txt")
    rp.read()
    return rp.can_fetch(USER_AGENT, url)


def fetch(session, url):
    res = session.get(url, timeout=30)
    res.raise_for_status()
    time.sleep(WAIT_SECONDS)
    return res.text


def parse(html):
    soup = BeautifulSoup(html, "html.parser")
    rows = []
    for a in soup.select("article h2 a"):
        rows.append({"title": a.get_text(strip=True), "url": a.get("href")})
    return rows


def main():
    print(f"python {sys.version.split()[0]}")

    if not is_allowed(TARGET):
        print("robots.txt で許可されていないため中止")
        return 1

    with requests.Session() as session:
        session.headers["User-Agent"] = USER_AGENT
        html = fetch(session, TARGET)

    rows = parse(html)
    if not rows:
        print("0件。ページの構造が変わった可能性あり")
        return 1

    OUT_DIR.mkdir(exist_ok=True)
    out = OUT_DIR / f"news_{date.today():%Y%m%d}.csv"
    pd.DataFrame(rows).to_csv(out, index=False, encoding="utf-8-sig")
    print(f"{len(rows)}件を保存: {out}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
  • 冒頭の定数:取得先・連絡先入りのUser-Agent・保存先・待ち時間をまとめています。保存先を Path(__file__) 基準にしているのは、systemdから起動したときの作業ディレクトリに左右されないようにするためです。
  • is_allowed():標準ライブラリの urllib.robotparser で robots.txt を読み、このURLを取ってよいかを毎回確かめます。運営者が方針を変えたら、翌日の実行から自動で止まります。
  • fetch():timeout=30 を必ず付けます。付けないと相手の応答が返らないときに永久に待ち、タイマーの次回実行まで詰まります。raise_for_status() で4xx・5xxを例外にし、取得後に5秒待ちます。
  • parse():select()(CSSセレクタで要素を探す関数)で見出しのリンクを集めます。BeautifulSoupの古い書き方 findAll からの移行は BeautifulSoup findAll非推奨2026年9月版 で扱っています。
  • main():最初に実行中のPythonのバージョンをログへ出します。移行後に本当に3.13で動いているかを、ログだけで確かめられます。0件や取得拒否のときは 終了コード1 を返すので、systemd側で「失敗」として記録されます。黙って空のCSVを作り続ける事故を防ぐためです。

手元で一度動かして確認します。

.venv/bin/python main.py

systemd timerのExecStartに.venv/bin/pythonを絶対パスで書く

cronからsystemd timerへ移す理由

項目

cron(以前)

systemd timer(今)

使うPython

/usr/bin/python3 などOS依存になりやすい

.venv/bin/python を絶対パスで固定

電源オフ中に逃した回

実行されない

Persistent=true で起動後に1回実行

ログ

自分でリダイレクトを書く

journalctl で日時付きで見られる

失敗の記録

終了コードは残らない

終了コード1で failed と記録

サービスとタイマーを書く

sudo不要のユーザー単位で登録します。~/.config/systemd/user/scraper.service:

[Unit]
Description=Daily scraping job

[Service]
Type=oneshot
WorkingDirectory=/home/username/scraper
ExecStart=/home/username/scraper/.venv/bin/python /home/username/scraper/main.py
  • Type=oneshot:一度実行して終わる処理であることを示します。
  • ExecStart:systemdはログインシェルの PATH や ~/.bashrc を読みません。python3 と書くと、どのPythonが選ばれるかが環境次第になります。.venv/bin/python を絶対パスで直接呼べば、source .venv/bin/activate も不要です。~ も展開されないので、パスは省略せずに書きます。
  • uv run で起動する書き方もできますが、実行のたびに環境の確認が入ります。毎日決まった処理を回すだけなら、uv sync は更新するときだけ手で行い、実行時は .venv のPythonを直接呼ぶほうが、起動時の失敗要因が減ります。

~/.config/systemd/user/scraper.timer:

[Unit]
Description=Run scraper daily at 07:00

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

[Install]
WantedBy=timers.target
  • OnCalendar:毎日7:00に起動します。
  • Persistent=true:電源が落ちていて逃した回を、起動後に1回だけ実行します。
  • RandomizedDelaySec=300:最大5分ずらして起動します。毎日きっかり同じ時刻に大勢のジョブが集中するのを避けるためです。

有効化と確認

systemctl --user daemon-reload
systemctl --user start scraper.service      # まず手動で1回
journalctl --user -u scraper.service -n 20  # 「python 3.13.x」と出ているか
systemctl --user enable --now scraper.timer
systemctl --user list-timers
sudo loginctl enable-linger username

最後の enable-linger を忘れると、ログアウトした時点でユーザー単位のタイマーが止まります。SSHで設定して抜けたら翌朝動いていなかった、という典型的な原因です。

動いたら、古いcronの行を必ず消します(crontab -e で該当行を削除)。両方残すと同じサイトへ1日2回アクセスすることになり、相手の負荷が倍になります。ブラウザ操作が要るジョブを同じ形で回す話は Playwright定期実行をsystemdで 2026年10月 にまとめています。

次のバージョンへ上げるとき

3.13からさらに上げるときも、ユニットファイルは書き換えません。

cd /home/username/scraper
uv python install 3.14
uv python pin 3.14
uv sync
.venv/bin/python -V

ExecStart は .venv/bin/python を指しているだけなので、中身のPythonが変わっても設定はそのままです。requires-python も合わせて上げておきます。パッチ版(3.13.16のような3桁目)の更新や、その他のオプションは uv python --help・uv sync --help と公式ドキュメントで確認してください。移行直後は journalctl の1行目のバージョン表示で切り替わりを確かめます。

取得先サイトへの配慮:robots.txt・利用規約・アクセス間隔

定期実行は「毎日、無人で、同じサイトへ行く」仕組みです。設定を誤ったまま放置すると、その誤りも毎日繰り返されます。

  • robots.txt:AIクローラーの増加を受けて、robots.txtで収集を拒否するサイトが増えています。日本新聞協会も2025年6月、AI事業者にrobots.txtを守るよう求める声明を出しました。法律で直接義務づけられてはいませんが、運営者の明確な意思表示として重みを増しています。上のコードのように毎回の実行時に読み直すのが確実です。
  • 利用規約:Amazon・X(旧Twitter)・YouTube・楽天・Yahoo!ファイナンスなど、多くの大手サービスは規約で自動収集を禁じています。ログインが必要なページの収集は規約違反になりやすいので、公式APIがあればそちらを使います。
  • アクセス間隔:「1秒に1回なら安全」という基準はありません。2010年の岡崎市立中央図書館事件では、1秒に1回程度のアクセスで逮捕者が出ています(後に起訴猶予)。問題になったのは相手のシステムに障害が出たことでした。取得件数を必要最小限に絞り、待ち時間は余裕を持たせます。
  • やらないこと:CAPTCHAの自動突破やIPブロックの回避は、不正アクセス禁止法に触れるおそれがあります。ブロックされたら、それは相手の意思表示として受け取り、収集をやめるか運営者に問い合わせます。
  • 定期実行ならではの注意:Persistent=true は逃した回をまとめて1回だけ実行しますが、cronとの二重登録や、テストで start を何度も叩くとアクセスが増えます。手動実行は最小限にします。

毎日動かし続けるには:手元のPC・共有レンタルサーバーのcron・VPSの止まる条件

書き方を3.13に移しても、動かす場所が止まればジョブも止まります。どこで回すかは、次の止まる条件で選びます。

動かす場所

向いている条件

止まる条件

手元のPC(Linuxならsystemd timer、Macならlaunchd)

1日1回で、数日遅れても困らない

スリープ・電源オフ中は動かない。Persistent=true で起動後に追いつくが、時刻はずれる。ノートPCは蓋を閉じると止まる

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

requests+BeautifulSoupで数分で終わる処理

systemdは使えないことが多い。uvでPythonを入れられるか、長時間のプロセスやブラウザの起動が許されるかは契約先の仕様次第。事業者がOSを更新するとパスが変わることがある

VPS

毎日確実に回したい/Selenium・Playwrightを使う

OSの更新や再起動の後に enable-linger やタイマーの有効化が抜けると黙って止まる。ディスクの空き不足、メモリ不足(ブラウザ系は特に消費が大きい)、契約の更新忘れ

共有レンタルサーバーでcronを使う場合も、考え方は同じです。uv sync で作った .venv/bin/python を絶対パスで書きます。

0 7 * * * /home/username/scraper/.venv/bin/python /home/username/scraper/main.py >> /home/username/scraper/cron.log 2>&1

どこで動かす場合でも、止まったことに気づける仕組みを一緒に用意します。最低限、次の3点を週に一度は見ます。

  • systemctl --user list-timers:次回の実行予定が入っているか。
  • systemctl --user status scraper.service:直近の結果が failed になっていないか。上のスクリプトは0件のときも失敗として記録します。
  • data/ の最新ファイルの日付:今日の分ができているか。

関連する選択肢

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

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

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

まとめ

Python 3.10は2026年10月1日にEOLとなり、Ubuntu 22.04のOS標準 python3 に載せたcronジョブは古いPythonに取り残されます。python3 -m venv で作ったvenvも中身は3.10のままなので、それだけでは移行になりません。今の書き方は、uv python install 3.13 と uv python pin 3.13 でPython本体をプロジェクトに固定し、uv sync で .venv を作ります。そのうえでsystemd timerの ExecStart から .venv/bin/python を絶対パスで呼びます。OSのPythonに触れずに済み、次の版へ上げるときもユニットファイルはそのまま使えます。実行時にバージョンをログへ出し、0件を失敗として扱えば、移行の成否も故障も journalctl で確かめられます。最後に、enable-linger の設定と古いcron行の削除を忘れないこと。robots.txtを毎回読み直し、待ち時間を取ることも続けてください。動かす場所の止まる条件を把握しておけば、毎日止まらずに回り続けます。