Webスクレイピングは、ビジネスデータ収集から個人開発まで、様々なシーンで活用されています。しかし初心者から経験者まで、同じようなトラブルに何度も悩まされるケースが多いでしょう。本記事では、実務・個人開発を問わず頻発する9つのトラブルを「原因→症状→対策」の形で体系的にまとめました。これからスクレイピングを始める方も、既に取り組んでいる方も、必ず役立つ内容です。
HTMLの構造変更による要素取得失敗
【原因】スクレイピングコードが突然動かなくなるのはなぜ?
Webサイトは定期的にHTMLの構造やクラス名を変更します。特にUIのリデザインや大規模なシステム更新の際に、セレクタが無効になります。スクレイプ対象のサイトが過去3ヶ月以上更新されていなければ、構造変更のリスクは常に存在します。
【症状】HTMLの構造変更で起こるエラー
- NoneType object has no attribute エラーが急に発生
- 取得データが空になる
- 特定のページのみ失敗する
- 数日前は動いていたコードが急に動かなくなる
【対策】HTMLの構造変更に強いコード設計
- 複数セレクタの用意:
soup.select_one('.title, .product-title, h2.name')のように複数パターンを指定 - CSSセレクタの代わりにXPathを併用:より堅牢なセレクタを複合利用
- 動的な属性ではなくテキストコンテンツで判定:クラス名より要素の内容を基準に
- 定期的なテスト:CI/CDパイプラインでスクレイピングコードの動作確認を自動化
- アラート機構:取得件数が急減したら通知する仕組みを構築
ユーザーエージェント不正検出
【原因】ボット判定されてアクセスが拒否されるのはなぜ?
多くのWebサイトは、ユーザーエージェント(User-Agent)ヘッダーを確認して、ブラウザからのアクセスか自動スクレイプか判定しています。デフォルトのUA(python-requests/2.x.x など)や古いUAを使うと、ボット判定されてHTTP 403エラーやHTTP 401エラーを返されます。
【症状】ボット判定によるアクセス拒否エラー
- HTTP 403 Forbidden エラー
- HTTP 406 Not Acceptable エラー
- HTMLではなく「アクセスが拒否されました」というテキストを返す
- 同じURLでもたまに成功、たまに失敗
【対策】ブラウザに見せかけるヘッダー設定
- 現代的なUA設定:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36など実在するブラウザのUAを使用 - UAをランダムに変更:fake-useragent ライブラリで毎回異なるUAを使用
- その他の必須ヘッダー追加:Referer、Accept-Language、Accept-Encoding などを正当なブラウザのように設定
- リクエスト間隔を適切に設定:
time.sleep()で1~3秒のランダムな遅延を入れる - セッション情報の保持:Cookieを保存・送信することで継続的なセッションを確立
import requests
from fake_useragent import UserAgent
ua = UserAgent()
headers = {
'User-Agent': ua.random,
'Accept-Language': 'ja-JP,ja;q=0.9',
'Accept-Encoding': 'gzip, deflate',
'Referer': 'https://example.com'
}
response = requests.get(url, headers=headers)
JavaScriptで動的に生成されるコンテンツの取得失敗
【原因】ブラウザには見えるのにスクレイプすると空になるのはなぜ?
モダンなWebアプリケーション、特にReactやVueで構築されたサイトの場合、HTMLを返すだけでなくJavaScriptを実行してDOMを構築します。requests ライブラリで取得したHTMLはJavaScript実行前の空のHTMLなため、目的のデータが含まれていません。
【症状】JavaScriptレンダリングの未実行
requests.get()で取得したHTMLにデータが含まれていない- ブラウザでは表示されるが、スクレイプしたデータは空
- JavaScriptコンソール(F12)で見ると存在するが取得できない
- 「JavaScript is not enabled」というメッセージ
【対策】JavaScriptを実行してからコンテンツを取得
- Selenium の使用:JavaScriptを実行してから要素を抽出
- Puppeteer(Node.js)の活用:ヘッドレスブラウザでページを完全レンダリング
- Playwright の導入:Seleniumより高速で安定
- APIエンドポイントの直叩き:ブラウザのDevToolsで通信を確認し、JSON API を直接呼び出す
- APIの逆エンジニアリング:JavaScriptで実行されるAPI呼び出しを把握して直接クエリ
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
driver.get(url)
driver.implicitly_wait(10) # 要素出現まで最大10秒待機
html = driver.page_source
soup = BeautifulSoup(html, 'html.parser')
IP制限・レート制限によるブロック
【原因】短時間にたくさんリクエストを送るとなぜブロックされる?
同じIPアドレスから短時間に大量のリクエストを送ると、Webサイトはボット判定してIPをブロックします。クラウド環境やVPS上での大規模スクレイピング、または急いでスクレイプしようと無限ループで連続リクエストを送ると高確率で発生します。
【症状】IP制限とレート制限によるブロック
- HTTP 429 Too Many Requests エラー
- 一定時間後に HTTP 403 Forbidden に変わる
- ネットワークが遮断される(TCP接続がリセット)
- 同じIPからのアクセスがしばらく完全にブロック
【対策】リクエスト分散と間隔調整
- リクエスト間隔を増やす:最低でも1~3秒の遅延、大規模スクレイプなら5~10秒
- プロキシの使用:複数のプロキシサービスを利用して異なるIPで分散
- 遅延をランダム化:毎回同じ間隔では検出されやすいため、乱数で可変に
- User-Agent を変更:複数のUA を組み合わせる
- Robots.txt の確認:許可されているクローリング間隔を守る
- 分散スケジューリング:複数の時間帯に分割してスクレイプ
- プロキシプール管理:複数のプロキシを循環使用し、応答なしになったら除外
import time
import random
import requests
from itertools import cycle
proxies = ['http://proxy1.com:8080', 'http://proxy2.com:8080']
proxy_pool = cycle(proxies)
for url in urls:
proxy = next(proxy_pool)
try:
response = requests.get(url, proxies={'http': proxy, 'https': proxy}, timeout=10)
except:
continue
time.sleep(random.uniform(2, 5))
文字コード不正(UTF-8以外のエンコーディング)
【原因】古いサイトやローカルサイトで文字化けするのはなぜ?
古いWebサイトや地域特有のサイトでは、Shift-JISやEUC-JP などの非UTF-8エンコーディングを使用しています。requests ライブラリがContent-Typeヘッダーから正しく文字コードを自動判定できない場合、文字化けが発生します。
【症状】文字コード不正による文字化け
- スクレイプしたテキストが文字化け(「???」や「□」に見える)
- 正規表現マッチングが失敗
- データベース保存時にエラー
- 見た目上は正しく見えるが、テキスト処理で問題が発生
【対策】文字コード検出と明示的な指定
- 文字コード自動検出ライブラリ使用:chardet で自動判定
- 明示的にエンコーディング指定:レスポンスオブジェクトに対して
response.encoding = 'shift_jis'を設定 - HTMLメタタグから抽出:
<meta charset="shift_jis">から文字コード情報を読み取る - デコード失敗への対応:
errors='ignore'またはerrors='replace'で部分的な破損を許容
import chardet
import requests
response = requests.get(url)
# 文字コード自動判定
detected = chardet.detect(response.content)
encoding = detected['encoding']
html = response.content.decode(encoding, errors='replace')
# または明示的に指定
response.encoding = 'shift_jis'
html = response.text
タイムアウト・接続エラー(Timeout / Connection Error)
【原因】サーバーがなかなか応答しないとなぜエラーになる?
リモートサーバーの応答が遅い、ネットワーク遅延が大きい、または無限に待機した結果、タイムアウトします。特に国外サイトや遅いサーバー、または大量スクレイプで過負荷になったサーバーの場合、タイムアウトが頻発します。
【症状】タイムアウトと接続エラー
requests.exceptions.Timeoutエラー- ConnectionError や ReadTimeout 例外
- 一部のURLは取得できるが、特定のURLが常に失敗
- スクレイプが途中で止まる
【対策】タイムアウトとリトライの適切な設定
- タイムアウト値を明示的に設定:
timeout=30で最大30秒待機 - リトライロジック実装:失敗時に2~3回再試行、各試行間に遅延
- 例外処理の適切な記述:すべてのエラータイプをキャッチして処理
- 接続プールの設定:
requests.Session()で接続を再利用 - HTTP/2への対応:遅いHTTP/1.1の代わりにHTTP/2を活用(httpx など)
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retry_strategy = Retry(total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504])
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
response = session.get(url, timeout=30)
Cookie・セッション管理の失敗
【原因】ログインしたはずなのに次のリクエストでログアウトするのはなぜ?
ログインが必要なサイトや、訪問履歴に基づいてコンテンツを変える動的なサイトでは、Cookieやセッション情報を正しく管理する必要があります。毎回新規リクエストを送ると、前回のセッションが失われて、ブロックやリダイレクトが発生します。
【症状】セッション喪失によるアクセス拒否
- ログインが成功したのに次のリクエストでログアウトしている
- リダイレクトループに陥る
- HTTP 401 Unauthorized が返される
- ページの内容が異なる(アクセス権限がない)
【対策】セッション状態を保持し続ける
- Session オブジェクト使用:
requests.Session()でCookieを自動管理 - 手動Cookie設定:必要に応じて cookies パラメータで明示的に設定
- セッション永続化:Cookieをファイルに保存し、再利用(pickle や json で)
- ログイン処理の実装:POSTリクエストで認証情報を送信してセッション確立
- セッションの定期更新:長時間スクレイプする場合、定期的にセッションを再取得
import requests
session = requests.Session()
# ログイン
login_url = 'https://example.com/login'
login_data = {'username': 'user', 'password': 'pass'}
session.post(login_url, data=login_data)
# セッション保持したまま他ページへ
response = session.get('https://example.com/dashboard')
# Cookieをファイル保存
import pickle
with open('cookies.pkl', 'wb') as f:
pickle.dump(session.cookies, f)
# 後で復元
session = requests.Session()
with open('cookies.pkl', 'rb') as f:
session.cookies.update(pickle.load(f))
日本語などの多言語テキスト処理・正規表現の失敗
【原因】日本語の正規表現がマッチしないのはなぜ?
日本語を含むテキスト処理で、正規表現パターンが想定通りに動作しないケースが多いです。英語では \w で単語文字を表しますが、日本語では動作しません。また、半角・全角の混在や、Unicodeの正規化が不足していると、データクリーニングが失敗します。
【症状】多言語テキスト処理のエラー
- 正規表現で日本語がマッチしない
re.sub()で置換が効かない- 半角スペースと全角スペースが混在して比較できない
- 数字の「123」と「123」が別物として扱われる
【対策】Unicode正規化と日本語対応の正規表現
- Unicode正規化:
unicodedata.normalize('NFKC', text)で全角→半角に統一 - 言語別の正規表現:日本語を含む場合は
[\u3040-\u309F\u30A0-\u30FF\w]+など明示的にUnicodeレンジ指定 - 日本語形態素解析:複雑な日本語処理には MeCab や Janome を使用
- DOTALL フラグ:改行を含むマッチに対応(
re.DOTALLまたはre.S) - 多言語対応ライブラリ:ftfy、textacy などで自動クリーニング
import re
import unicodedata
text = "価格: 12,345 円"
# Unicode正規化(全角→半角)
normalized = unicodedata.normalize('NFKC', text)
print(normalized) # 価格: 12,345 円
# 日本語・数字・記号にマッチ
pattern = r'[\u3040-\u309F\u30A0-\u30FF0-9\-,.,。]+'
result = re.findall(pattern, normalized)
# または日本語形態素解析
from janome.tokenizer import Tokenizer
t = Tokenizer()
for token in t.tokenize(text):
print(token.base_form)
メモリリークと大規模データ処理の失敗
【原因】数千件をスクレイプするとメモリ不足になるのはなぜ?
数千~数万件のWebページをスクレイプする場合、メモリ効率が悪いと、スクレイプの途中でメモリ不足エラーが発生します。BeautifulSoupで全ページをメモリに保持したり、リスト全体をメモリに展開したまま放置すると、深刻なメモリリークが起きます。
【症状】メモリ不足による処理停止
- MemoryError や OutOfMemory エラー
- スクレイプ途中でプロセスが強制終了
- システム全体が遅くなり、スワップメモリを大量消費
- 数千件を超えるデータで突然停止
【対策】メモリ効率を優先した処理設計
- ジェネレータ使用:リストの代わりにジェネレータで遅延評価
- イテレータパターン:データをバッチで処理して随時DB・ファイル保存
- 不要オブジェクトの明示的削除:
delで参照を削除、gc.collect()で強制ガベージコレクション - ストリーミング処理:行単位・ページ単位で処理完了後に破棄
- データベース直結:スクレイプデータを一度メモリに保持せず、直接DBに挿入
- メモリプロファイリング:memory_profiler で問題箇所を特定
import gc
from bs4 import BeautifulSoup
import requests
def scrape_urls(url_list):
"""ジェネレータで1件ずつ処理"""
for url in url_list:
try:
response = requests.get(url, timeout=10)
soup = BeautifulSoup(response.content, 'html.parser')
# データ抽出と即座に処理
data = extract_data(soup)
yield data
# メモリ解放
del soup, response
gc.collect()
except Exception as e:
print(f"Error: {e}")
# バッチ処理してDB保存
def batch_insert_to_db(url_list, batch_size=100):
batch = []
for data in scrape_urls(url_list):
batch.append(data)
if len(batch) >= batch_size:
insert_db(batch)
batch = []
if batch:
insert_db(batch)
スクレイピング成功のベストプラクティス
各トラブルの対策を踏まえ、スクレイピングコードの実装時に心がけるべきポイントをまとめました。
- 常にタイムアウト設定:何かしら待機時間の制限を必ず設ける
- エラーハンドリング徹底:try-exceptで個別例外処理、ログ記録
- ヘッダー・User-Agentの適切設定:ブラウザらしく振る舞う
- リクエスト間隔調整:1~5秒程度の遅延を入れ、サーバー負荷軽減
- データ検証:取得データが期待値か確認してから処理
- 定期的なテスト・監視:CI/CD環境でスクレイピング実行テスト
- ログとアラート:エラー発生時に通知される体制構築
- 法的確認:Robots.txt、利用規約を確認
まとめ
Webスクレイピングは強力なツールですが、実装には細心の配慮が必要です。紹介した9つのトラブルは、いずれも実務で何度も遭遇するものばかり。各々の原因を理解し、紹介した対策を実装することで、安定したスクレイピングシステムを構築できるでしょう。
スクレイピング実装の際は、単に「データが取得できた」ではなく、「長期的に安定して取得できるか」という観点で、エラーハンドリング、監視、メンテナンスを念頭に置いてコーディングすることが、実務成功の鍵となります。


コメント