この講義にたどり着いたということは、すでにWebスクレイピングがどう動くか、そしてBeautifulSoup
を使ってHTMLから必要なデータを抽出する方法をよく理解しているということだね。でも、Webスクレイピングの世界は教科書通りにいかないことが多い。時にはデータ収集の夢がエラーとの戦いに変わることもある。だからこそ、落とし穴を避けつつスクレイパーを強固にする方法について話そう。
1. Webスクレイピングでよくあるエラー
404エラーや他のHTTPエラー
定番の問題:ページを取得しようとしたら、中身の代わりに誇らしげな「404 Not Found」が返ってくる。これはページが削除または移動された場合に起こる。他にもよくあるHTTPエラーには403 (Forbidden)、500 (Internal Server Error)、503 (Service Unavailable)があるね。
HTML構造の変更
データを抽出するコードを書くのにたくさん時間を費やしたのに、翌日にはサイトが少し見栄えを良くしようとしてHTML構造を変更。ああ、また全部書き直しだ!
リクエスト数の制限
あるサイトをずっとスクレイピングしていると、怪しまれることがある。最良の場合、しばらくブロックされるだけだけど、最悪の場合は永久にアクセス禁止になる。
待機時間とタイムアウト
時にはページの読み込みが遅く、タイムアウトの制限時間を超えたらスクリプトが落ちてしまうこともある。
2. エラー処理の方法
try-exceptを使う
スクリプトはどんな予期せぬ事態でも落ちるべきではない。try-except
ブロックを追加すると、エラーをキャッチして、スクレイパーが落ちることなく動作を続けるようにできるよ。
import requests
from bs4 import BeautifulSoup
url = 'http://example.com/some-nonexistent-page'
try:
response = requests.get(url)
response.raise_for_status() # 不正な応答に対してHTTPErrorを発生させる
except requests.exceptions.HTTPError as errh:
print("HTTPエラー:", errh)
except requests.exceptions.ConnectionError as errc:
print("接続エラー:", errc)
except requests.exceptions.Timeout as errt:
print("タイムアウトエラー:", errt)
except requests.exceptions.RequestException as err:
print("何か他のエラーが発生:", err)
良いスクリプトは単に例外をキャッチするだけでなく、エラーの種類ごとに対応策を持っている。例えば、IPがバンされたら次のプロキシに切り替えたり、サイトが落ちたら他のサイトのパースを行ったりできる。ページ上に存在すべき要素が見つからない場合は、スクリプトのアップデートが必要だと通知してメールを送ることも可能。
ログ記録
「ログなんて何のため?」って思う?実は、ログは第二の目なんだよ。何が問題だったのかを特定して迅速に修正する助けになるよ。
import logging
logging.basicConfig(filename='scraper.log', level=logging.INFO)
try:
# スクレイピングのコード
pass
except Exception as e:
logging.error("例外が発生しました", exc_info=True)
タイムアウトとリトライの使用
時には少し待ってもう一度試すだけでうまくいくこともある。このためにはタイムアウトとリトライが役立つよ。
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
except requests.exceptions.Timeout:
print("タイムアウトが発生しました。リトライ中...")
# リクエストを再試行するか、別のアクションを実行
3. 強固なスクレイピングの例
エラー処理付きのシンプルなスクレイパー
いくつかの一般的なエラーを処理できる、小さいけれど信頼性のあるスクレイパーを作ろう。
import requests
from bs4 import BeautifulSoup
import time
import logging
logging.basicConfig(filename='scraper.log', level=logging.INFO)
url = 'http://example.com/some-nonexistent-page'
def fetch_html(url):
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.text
except requests.exceptions.HTTPError as errh:
logging.error("HTTPエラー: %s", errh)
except requests.exceptions.ConnectionError as errc:
logging.error("接続エラー: %s", errc)
except requests.exceptions.Timeout as errt:
logging.warning("タイムアウトが発生しました。リトライ中...")
time.sleep(1) # 少し待って再試行
return fetch_html(url)
except requests.exceptions.RequestException as err:
logging.error("何か他のエラーが発生しました %s", err)
return None
html_content = fetch_html(url)
if html_content:
soup = BeautifulSoup(html_content, 'html.parser')
# データ抽出のコード
else:
print("ページの内容を取得できませんでした。")
部分的なデータ保存
途中で障害が起きても既に抽出されたデータを失わないように、データを部分的に保存しておこう。例えば、一連のページから情報を取得して進むごとに結果を保存することで、データ損失のリスクを最小限に抑えることができる。
import csv
def save_to_csv(data, filename='data.csv'):
with open(filename, mode='a', newline='') as file:
writer = csv.writer(file)
writer.writerow(data)
この方法なら、スクリプトが途中で落ちても、全てのデータを失うことなく、最後に保存した状態から再開できるよ。
パースしているページが部分的に変わった場合でも、データの大部分はまだ利用可能。少しのデータが欠けているからといってパースをキャンセルして貴重なデータを失うのはもったいないよね。
GO TO FULL VERSION