Bot対策に引っかからないように大量のWebページを保存するまでの試行錯誤
ある日、とあるWebサイト上に掲載されている日次のHTMLリストを一定期間まとめて取得したいと思い立ちました。対象ページには過去の日付ごとに情報が整理されていて、同じ構造のURLにアクセスすればHTMLが得られそうでした。
最初は requests × BeautifulSoup で
「これは簡単にできそうだ」と思い、まずは以下のようなPythonコードで取得を試みました。
import requests
from bs4 import BeautifulSoup
url = "https://example.com/2024/1/1"
headers = {"User-Agent": "Mozilla/5.0"}
res = requests.get(url, headers=headers)
soup = BeautifulSoup(res.content, "html.parser")
が、結果は 403 Forbidden。
つまり、アクセス拒否されてしまったのです。どうやらこのサイトは bot 対策がしっかりしていて、requests などの直アクセスはブロックされてしまうようです。
次に Selenium(ヘッドレス)を試す
「じゃあブラウザを操作したように見せかけよう」と思い、Selenium を導入。
ただし最初はブラウザを起動せずに済む ヘッドレスモード にしてみたのですが、これもやっぱり 403 Forbidden。
結局、普通にブラウザを立ち上げて操作するモードでないと突破できないことが分かりました。
Seleniumで普通に開けばアクセスはできたけど…
今度はうまくHTMLが取得できたので、安心してスレッドで並列にアクセスを開始。
……が、しばらくするとページがCAPTCHA画面に遷移してしまいました。
どうやら今度は「短時間に複数ページを巡回している=botでは?」と疑われてしまったようです。
最終的な解決策:1回だけ手動でCAPTCHAを突破し、Seleniumで順次アクセス
ということで、最終的には次のような戦略を取りました。
- 最初の1回だけ手動でページを開いて CAPTCHA を突破
- その後はその 同じブラウザインスタンスを使って、日付ごとのURLに1件ずつアクセス
- 各アクセス後は 10〜20秒ランダムに待機
- HTMLを保存して次へ
このアプローチにより、403もCAPTCHAも回避しつつ、負荷を抑えてHTMLを収集できるようになりました。
以下が実際のコードです。
コード全文(簡略化版)
import pandas as pd
import os
import time
import random
from datetime import datetime, timedelta
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
# === URLリスト生成 ===
start_date = datetime(2020, 1, 1)
end_date = datetime(2024, 12, 31)
url_patterns = [
"https://example.com/{y}/{m}/{d}",
]
records = []
date = start_date
while date <= end_date:
y, m, d = date.year, date.month, date.day
for pattern in url_patterns:
url = pattern.format(y=y, m=m, d=d)
filename = url.split("https://example.com/")[1].replace("/", "_") + ".html"
records.append({"url": url, "filename": filename})
date += timedelta(days=1)
df_urls = pd.DataFrame(records)
print(f"URLリスト作成完了({len(df_urls)}件)")
# === HTML保存 ===
output_dir = "./html_outputs"
os.makedirs(output_dir, exist_ok=True)
def create_driver():
options = Options()
options.add_argument("--window-size=1920,1080")
options.add_argument("--disable-gpu")
options.add_argument("--no-sandbox")
# CAPTCHA回避のため、ヘッドレスにはしない
return webdriver.Chrome(options=options)
def main():
driver = create_driver()
print("最初のページにアクセスしてCAPTCHAを解除してください(ブラウザが開きます)")
driver.get(df_urls.iloc[0]['url'])
input("CAPTCHA対応が終わったら Enter を押してください:")
for _, row in df_urls.iterrows():
url = row["url"]
filename = row["filename"]
filepath = os.path.join(output_dir, filename)
if os.path.exists(filepath):
print(f"{filename} ... スキップ(既に存在)")
continue
try:
driver.get(url)
time.sleep(5)
html = driver.page_source
with open(filepath, "w", encoding="utf-8") as f:
f.write(html)
wait = random.randint(10, 20)
print(f"{filename} ... 成功。{wait}秒待機中")
time.sleep(wait)
except Exception as e:
print(f"{filename} ... 失敗: {str(e)}")
driver.quit()
print("全件完了")
if __name__ == "__main__":
main()
モラルと実運用のバランスについて
本来、Webサイトに対して自動で大量アクセスを行うのはマナー違反ですし、
サイト側の利用規約で明示的に禁止されていることもあります。
今回はあくまで個人の学習・検証目的で、
- CAPTCHAの突破は人間が1度だけ実施
- その後はゆっくりと(1件ずつランダムにsleepを入れながら)アクセス
という配慮のもと、短時間に大量アクセスしないように注意して作業を行いました。
まとめ
「requests で弾かれ、Seleniumヘッドレスもダメ、CAPTCHA にも阻まれる」
そんな中での試行錯誤でしたが、Selenium + CAPTCHA手動突破 + 遅延制御 という
組み合わせで、なんとか目的のHTML取得にたどり着けました。
Discussion