🍊

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