BeautifulSoup 4.13以降、find_all(text=...)を使うとDeprecationWarning(非推奨の警告)が出ます。手元で気づいても、cronやsystemd timerで毎日動かしているスクリプトでは警告がログに埋もれます。そのまま放置すると、将来の更新で動かなくなった日に初めて気づくことになります。この記事では、find_allのtext=をstring=へ書き換える今の書き方を示します。あわせて、サーバー上で警告を見逃さない設定と、bs4の版を固定して安全に上げる手順までを、動くコードで説明します。
find_all(text=...)はstring=に書き換える:BeautifulSoup 4.13以降の書き方
BeautifulSoupでは、文字列で要素を絞り込む引数の名前がtextからstringに変わっています。4.13からは、旧名のtext=を使うたびにDeprecationWarningが出るようになりました。2026年10月時点のPyPI最新版は4.15.0(2026年6月7日公開、Python 3.7以上が必要)です。今から新しく入れる環境なら、確実にこの警告の対象になります。
以前の書き方と今の書き方
# 以前の書き方(4.13以降は DeprecationWarning が出る)
links = soup.find_all("a", text="詳細")
notes = soup.find_all(text=re.compile("在庫"))
first = soup.find("span", text="送料無料")
# 今の書き方
links = soup.find_all("a", string="詳細")
notes = soup.find_all(string=re.compile("在庫"))
first = soup.find("span", string="送料無料")
機械的に置き換えるだけなら、引数名をtext=からstring=に変えれば済みます。find・find_all・find_nextなど、find_*系はすべて同じ扱いです。リポジトリ内に残っている箇所は、次のコマンドで洗い出せます。
grep -rnE "find(_all|_next|_previous|_parent)?\(.*\btext=" --include="*.py" .
なお、取得した要素から本文を取り出す.get_text()や.stringは非推奨になっていません。変わったのは検索条件として渡す引数名だけです。
string= の挙動で引っかかりやすい3点
引数名を変えるだけで結果は同じになります。ただし、string=の動きを正しく理解していないと「取れているはずが0件」になりがちです。押さえておくべき点を表にまとめました。
書き方 | 返ってくるもの | 注意点 |
|---|---|---|
| タグではなく文字列(NavigableString) | タグが欲しいときは |
|
| 子要素が複数あるタグは |
| 正規表現に一致する文字列 | 部分一致させたいときはこちらを使う |
特に2行目でつまずく人が多いです。<a>詳細 <span>NEW</span></a>のように子要素が混ざると、.stringはNoneになります。その結果、string="詳細"では拾えません。この場合は、関数で条件を書くのが確実です。
def is_detail_link(tag):
return tag.name == "a" and "詳細" in tag.get_text()
links = soup.find_all(is_detail_link)
find_allに関数を渡すと、すべてのタグに対してその関数が呼ばれ、Trueを返したものだけが集まります。get_text()は子孫要素の文字列をすべて連結するため、装飾用のspanが混ざっていても判定できます。
cronやsystemd timerでDeprecationWarningを見逃さない設定
「手元で一度見たから大丈夫」と油断できない理由があります。DeprecationWarningは、Pythonの既定では表示されない場合があるからです。
警告が出るファイルと出ないファイルがある理由
Python 3.7以降は、DeprecationWarningの表示条件が決まっています。表示されるのは、警告の発生元が直接実行したスクリプト(__main__)のコードであるときだけです。python3 scraper.pyの中で直接find_all(text=...)を呼んでいれば、警告が出ます。一方、parsers.pyのような別モジュールに抽出処理を分け、それをimportして呼んでいる場合は、既定では何も表示されません。
つまり、処理を関数やモジュールに整理した「きちんとした」スクリプトほど、警告に気づけません。さらに、cronでは標準エラー出力がメール送信先に流れるか捨てられます。systemd timerではjournal(systemdのログ置き場)に入ります。どちらも、自分から見に行かない限り目に入りません。
警告をログに必ず残すコード
対策は2つです。1つは、DeprecationWarningを常に表示する設定にすること。もう1つは、警告をloggingに流して、普段のログと同じ場所に残すことです。次のスクリプトは、取得先のrobots.txtを確認したうえで1ページを取得し、商品名を取り出します。
import logging
import time
import warnings
from urllib import robotparser
import requests
from bs4 import BeautifulSoup
URL = "https://example.com/products"
UA = "example-price-checker/1.0 (+https://example.com/contact)"
# --- 1. 警告をログに流す設定 ---
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
logging.captureWarnings(True)
warnings.simplefilter("default", DeprecationWarning)
log = logging.getLogger("scraper")
# --- 2. robots.txt で取得してよいか確認する ---
def allowed(url):
rp = robotparser.RobotFileParser()
rp.set_url("https://example.com/robots.txt")
rp.read()
return rp.can_fetch(UA, url)
# --- 3. 取得と抽出 ---
def fetch_items(url):
res = requests.get(url, headers={"User-Agent": UA}, timeout=30)
res.raise_for_status()
soup = BeautifulSoup(res.text, "html.parser")
items = []
for label in soup.find_all("span", string="在庫あり"):
card = label.find_parent("li")
if card is None:
continue
name = card.find("h3")
items.append(name.get_text(strip=True) if name else "")
return items
def main():
if not allowed(URL):
log.warning("robots.txt で許可されていないため取得しません: %s", URL)
return 1
items = fetch_items(URL)
log.info("在庫あり %d 件", len(items))
time.sleep(5) # 続けて別ページを取る場合に備え、間隔を空ける
return 0
if __name__ == "__main__":
raise SystemExit(main())
各ブロックで行っていることは次のとおりです。
- 1. 警告をログに流す設定:
warnings.simplefilter("default", DeprecationWarning)は、どのモジュールから出たDeprecationWarningでも、発生箇所ごとに1回は表示する設定です。logging.captureWarnings(True)は、警告をpy.warningsという名前のロガーにWARNINGレベルで渡します。これで、通常のログと同じ形式・同じ出力先に記録されます。 - 2. robots.txtの確認:標準ライブラリの
urllib.robotparserで、自分のUser-Agentに取得が許されているかを確かめます。許可されていなければ取得せずに終了します。 - 3. 取得と抽出:
string="在庫あり"で在庫表示のspanを探し、find_parent("li")で商品カード全体までさかのぼってから商品名を取ります。timeout=30を付けておくと、相手が応答しないときに永久に待ち続けることがありません。
コードを変えずに済ませたい場合は、環境変数でも同じ効果が得られます。systemdのserviceファイルに1行足すだけです。
[Service]
Type=oneshot
WorkingDirectory=/home/username/scraper
Environment=PYTHONWARNINGS=default::DeprecationWarning
ExecStart=/home/username/scraper/.venv/bin/python scraper.py
あとは、journalに警告が出ていないかを定期的に検索します。
journalctl --user -u scraper.service --since "7 days ago" | grep DeprecationWarning
cronで動かしている場合は、標準エラー出力もファイルに残すようにしてください(>> /home/username/scraper/run.log 2>&1)。そうしないと、同じ検索ができません。cronとsystemd timerの違いや、マシンが止まっていた間の取りこぼし対策については、systemd timerでcron取りこぼし対策|26年10月にまとめています。
bs4の版を固定して、警告を「上げる前」に確認する手順
警告に気づけるようになっても、サーバー上で勝手にbs4が上がる運用のままでは意味がありません。毎日動かすスクリプトは、版を固定し、上げるときだけ検証するのが基本です。
実行環境と、インストールするパッケージ
- Python:3.10以上を想定(Python 3.9は2025年10月にEOL=サポート終了)。手元の版は
python3 -Vで確認する pip install beautifulsoup4 requests:パッケージ名はbeautifulsoup4と正確に指定する。beautifulsoupと打つと、Python 2.7向けの古いものが入る可能性がある- パーサーは標準の
html.parserで足りる。速度が必要ならlxmlを追加する
requirements.txt に今の版を書き出す
python3 -m venv .venv
. .venv/bin/activate
pip install beautifulsoup4 requests
pip show beautifulsoup4 | grep Version # 実際に入った版を確認
pip freeze > requirements.txt
pip freezeは、入っているパッケージをすべて==付きで書き出します。bs4本体だけでなく、bs4が内部で使うsoupsieve(CSSセレクタの処理を担う依存パッケージ)の版も固定されます。サーバー側ではpip install -r requirements.txtで同じ構成を再現します。こうしておけば、ある朝突然bs4だけが新しくなっていた、という事態を防げます。
版を上げるときは、警告をエラー扱いにして一度通す
版を上げる前に、別のvenvで試します。-W error::DeprecationWarningを付けると、DeprecationWarningが例外になります。非推奨の書き方が1つでも残っていれば、その行で止まります。
python3 -m venv /tmp/try-bs4
/tmp/try-bs4/bin/pip install -r requirements.txt
/tmp/try-bs4/bin/pip install -U beautifulsoup4
/tmp/try-bs4/bin/python -W error::DeprecationWarning scraper.py
- 今の
requirements.txtで試験用のvenvを作る - bs4だけを最新版に上げる
- 警告をエラー扱いにして実行する。止まったら、表示された行を
string=などの今の書き方に直す - 最後まで通ったら、本番のvenvで
pip freeze > requirements.txtを取り直し、サーバーへ配る
本番のserviceに-W errorを付けるのは避けてください。依存パッケージの内部で出た無関係な警告でも、毎日の取得が止まってしまいます。本番では「記録する」、更新時は「止める」と使い分けるのが現実的です。非推奨の警告を放置して後から困る例としては、sqlite3 datetime非推奨と重複通知|26年10月も同じ構図です。
取得先サイトへの配慮:robots.txt・利用規約・アクセス間隔
書き方を直したついでに、取得の仕方も見直しておきます。
- robots.txt:上のコードのように、取得前に
can_fetchで確認する。ただし、robots.txtはクローラー向けのアクセス規則です。データの取得や利用そのものを許諾するものではありません。 - 利用規約:「自動化されたアクセス」や「データの無断取得」を禁じる条項がないかを事前に読む。禁止されていれば、公式APIがあるかを先に探す。
- アクセス間隔:複数ページを取るときは、1件ごとに数秒以上空ける。並列化して同じサイトに同時に何本も接続を張るのは避ける。
- 取得結果の扱い:日本では著作権法(複製権・公衆送信権など)や不正アクセス禁止法が関わる。取得したデータを再配布するかどうかで、判断が大きく変わります。
- User-Agent:問い合わせ先を含む、正直なUser-Agentを名乗る。ブラウザを装ってブロックをすり抜けるような使い方はしない。
毎日動かし続ける場所:手元のPC・レンタルサーバーのcron・VPSが止まる条件
警告の監視と版の固定は、「どこで動かすか」とセットで考えます。それぞれの場所で、スクリプトが止まる条件が異なるためです。
実行場所 | 定期実行の仕組み | 止まる条件 | 警告・版の管理 |
|---|---|---|---|
手元のPC(Mac・Windows) | launchd/タスクスケジューラ | スリープ中・フタを閉じている間・電源オフ。OS更新後の再起動で設定が外れることもある | 自分で管理できるが、PCを開いていない日はログ自体が残らない |
共有レンタルサーバー | cron | サーバー側のPython更新・利用規約上の実行時間や負荷の制限。Seleniumなどブラウザを使う処理は動かせないことが多い | Pythonの版は事業者が決めるため、venvとrequirements.txtでbs4側を固定する |
VPS・自宅サーバー | systemd timer | OSの更新・再起動、ディスクの空き不足。自宅サーバーは停電も | journalで警告を検索でき、版も完全に自分で固定できる |
requestsとBeautifulSoupだけで済む軽い取得を1日数回動かすだけなら、cronが使える共有レンタルサーバーで足ります。JavaScriptで描画されるページのためにPlaywrightやSeleniumが必要な場合や、常駐させたい場合は、VPSか常時稼働させる自宅のマシンが必要になります。いずれの場所でも、確認すべきことは同じです。「止まったときに気づけるか」と「勝手に版が変わらないか」の2点を押さえてください。
関連する選択肢
毎日決まった時刻にスクリプトを動かすだけなら、共有のレンタルサーバーでも足ります。cronが使えるプランを選べば、自分でOSを管理する必要はありません。
※ 広告を含みます(A8.net)。リンク経由でお申し込みがあった場合、手数料を受け取ることがあります。
BeautifulSoup text=非推奨への対応手順のまとめ
BeautifulSoup 4.13以降、find_all(text=...)はDeprecationWarningを出します。今の書き方はstring=です。引数名を変えるだけで動きますが、子要素が混ざったタグは.stringがNoneになって一致しません。その場合は、関数とget_text()で条件を書きます。毎日動かすスクリプトでは、2つの点に注意が必要です。まず、DeprecationWarningは別モジュールからだと既定で表示されません。次に、cronやsystemd timerの出力は見に行かないと気づけません。warnings.simplefilter("default", DeprecationWarning)とlogging.captureWarnings(True)(または環境変数PYTHONWARNINGS)で警告をログに残しましょう。bs4の版はpip freezeで固定し、上げるときだけ-W error::DeprecationWarningで一度通してから本番に配ります。最後に、動かす場所ごとの止まる条件を把握しておけば、警告から本番停止までの間に必ず気づけます。