以前、小さなプロジェクトでウェブスクレイピングと LINE Bot のアプリケーションを作っていたため、ウェブスクレイピングの技術やプロセスは広く知られていると思っていました。また、現在はChatGPTやBardなどのさまざまな AI ツールの台頭により、ウェブスクレイピングに関する質問をする人に出会うことはなくなりました。しかし、今日は友人が MLB ウェブサイトのデータをスクレイピングする方法について質問してきました。その時、私はブラウザの開発者ツールの Network を使って MLB ウェブサイトを確認し、彼が必要とするデータを取得できる API があると伝えましたが、彼は驚いた表情をしていました。
その時、実際には多くの人がウェブスクレイピングの作成方法を知らなかったり、Python を学べばウェブスクレイピングができるという誤解をしていることに気づきました。このような理由から、この記事が生まれました。
誤解と迷信#
- Python を学べば、簡単にウェブスクレイピングができる。
- コードが書ければ、簡単に必要なデータを取得できる。
- 一つのウェブスクレイピング技術を学べば、どんなものでも取得できる。
誤解を解く#
- ウェブスクレイピングはプログラムであり、プログラムは人が書くものであるため、プロセスを理解していないと正しい結果を得ることはできない。
- ウェブスクレイピングはデータを自動的に取得するためのプロセスであり、そのプロセスを理解する必要がある。
- ウェブスクレイピングを書く前に、手動でデータを取得でき、全体のフローを理解している必要がある。
ウェブスクレイピングプログラムの流れの例 - MLB ウェブサイトを例に#
目標データ:10 年間の試合データ
目標ソース:https://www.mlb.com/gameday/braves-vs-phillies/2023/09/11/717664/final/box
取得するデータ内容:
WP:Alvarado.
HBP:Harris II, M (by Walker, T); Riley, A (by Walker, T).
Pitches-strikes:Morton, C 104-64; Lee, D 8-4; Jiménez, J 11-8; Minter 13-9; Iglesias, R 15-10; Yates 14-9; Walker, T 103-53; Bellatti 21-13; Covey 18-14; Alvarado 16-13.
Groundouts-flyouts:Morton, C 2-4; Lee, D 1-0; Jiménez, J 0-0; Minter 1-0; Iglesias, R 0-2; Yates 1-0; Walker, T 3-2; Bellatti 0-2; Covey 5-0; Alvarado 2-0.
Batters faced:Morton, C 27; Lee, D 3; Jiménez, J 2; Minter 3; Iglesias, R 5; Yates 3; Walker, T 26; Bellatti 8; Covey 7; Alvarado 5.
Inherited runners-scored:Bellatti 1-1.
Umpires:HP: Larry Vanover. 1B: Jacob Metz. 2B: Edwin Moscoso. 3B: D.J. Reyburn.
Weather:78 degrees, Sunny.
Wind:4 mph, Out To CF.
First pitch:1:08 PM.
T:3:08.
Att:30,572.
Venue:Citizens Bank Park.
September 11, 2023
第一步:データソースを見つける#
ページ上のデータを取得するため、基本的にウェブページのデータは 2 つの場所から来ています。
- SSR - バックエンドが HTML ウェブページ全体を返す
- CSR - フロントエンドがバックエンド API を呼び出してデータを取得し、ウェブページにレンダリングする
ここでは、ブラウザの開発者ツール > ネットワークを使用し、フィルターを Fetch/XHR に切り替えてページをリフレッシュし、各リクエストのレスポンスを一つずつ確認します。各リクエストを確認した結果、このリクエストが私たちが探している API であることがわかりました。なぜなら、そのレスポンスにはウェブページに表示されているデータが含まれているからです。
ヘッダーをクリックすると、その API の URL が表示されます。
https://ws.statsapi.mlb.com/api/v1.1/game/717664/feed/live?language=en
717664 がゲームの番号であると合理的に疑われます。確認するには、ウェブページの URL を見てみましょう。
https://www.mlb.com/gameday/braves-vs-phillies/2023/09/11/717664/final/box
どうやら 717664 がゲームの番号のようです。他の試合も同様です。
https://www.mlb.com/gameday/braves-vs-phillies/2023/09/11/716590/final/box
https://ws.statsapi.mlb.com/api/v1.1/game/716590/feed/live?language=en
第二步:データソースが必要なデータを含んでいるか確認する#
ブラウジング > 右クリック > オブジェクトをコピーまたはレスポンス > 全選択 > コピーができます。
簡単な場合は、オンラインの Json パーサー (jsoneditoronline, json.parser) に貼り付けて確認できます。Ctrl + F でキーワードを検索すると、API レスポンスの json 内の info に必要なデータが含まれていることがわかります。ビンゴ!
第三步:外部ツールを使用して API の可用性を確認する#
ここでは、API が特別な認証や他の要素を必要とするかどうかを確認する必要があります。これにより、そのウェブページのみがアクセスできるようになります。Postmanを使用してテストできます。このツールの使い方がわからない場合は、チュートリアル記事を検索してください。
API を呼び出すことで情報を取得できることを確認しましたので、次のステップに進むことができます。
第四步:異なる試合情報を連続して取得する方法#
プログラムは人間が設計したものであるため、ウェブスクレイピングを書くには全体のプロセスを理解している必要があります。本を読んだり、チュートリアルを見たりするだけでは書けません。この例を考えると、ウェブスクレイピングの全体的なロジックは次のようになります。
- 10 年間のすべての試合番号を取得する。
- 上記の API を使用して 10 年間の試合番号からすべての試合情報を取得する。
- 試合情報を変数に保存し、これらの情報を CSV に書き込む。
最初のステップは、すべての試合番号をどのように取得するかです。この URL を使用して、上の階層の URL を探します。
https://www.mlb.com/gameday/braves-vs-phillies/2023/09/11/717664/final/box
使用するのは
https://www.mlb.com/gameday/
結果は次のようになります。
https://www.mlb.com/scores
ここです。次に、先ほどの方法を使用して、どのリクエストがこれらのデータを取得しているかを見つけます。
API を見つけました。
https://bdfed.stitch.mlbinfra.com/bdfed/transform-mlb-scoreboard?stitch_env=prod&sortTemplate=4&sportId=1&&sportId=51&startDate=2023-09-11&endDate=2023-09-11&gameType=E&&gameType=S&&gameType=R&&gameType=F&&gameType=D&&gameType=L&&gameType=W&&gameType=A&&gameType=C&language=en&leagueId=104&&leagueId=103&&leagueId=160&contextTeamId=
この API URL を Postman に投げると、左側にその URL を呼び出す際に付加されるパラメータが表示されます。いくつかのパラメータは機能が不明かもしれませんが、無闇に変更しない方が安全です。
その中にstartDateとendDateがあることがわかります。これを変更して、複数日のデータを取得できるかテストしてみましょう。できれば、データ取得の速度を上げることができます。
ビンゴ!2023-09-01 から 2023-09-11 までのすべての試合データを一度に取得できますが、呼び出しの時間は 8 秒かかり、レスポンスのデータ量も非常に大きいです。ここでは、最大で 1 ヶ月分のデータを取得する方法を考えるかもしれません。あまりにも多すぎるとタイムアウトする可能性があります。
第五步:これらのプロセスをプログラムのロジックに変換する#
ここでは、すぐにコードを書くことはせず、上記のプロセスをプログラムのフローとロジックに変換してから書きます。ここでは、簡単なサンプルコードがどのようにウェブスクレイピングのプロセスに変換されるかを示します。
import 所需的lib ex: requests
import calendar
# 宣言全域変数
# 宣言scores_api_url
scores_api_url = "https://bdfed.stitch.mlbinfra.com/bdfed/transform-mlb-scoreboard"
# 宣言game_api_url ゲーム番号に応じて変更される部分を特定の置換値game_idに変更
game_api_url = "https://ws.statsapi.mlb.com/api/v1.1/game/game_id/feed/live?language=en"
# 宣言開始年
start_year = "2012"
# 宣言終了年
end_year = "2022"
# すべてのGameIdデータを保存
game_data = []
# 主要プログラムブロック
def main:
day_list = get_month()
# すべての月をループ
for seDay in day_list:
# get_scores_dataを使用してその月の最初の日から最後の日までのすべての試合番号を取得
gameId_list = get_scores_data(seDay[0], seDay[1])
# get_game_dateを使用してすべてのgameIdデータを取得し、game_dataに追加
game_data = game_data + get_game_date(gameId_list)
# game_dataをCSVに保存
...
# 開始年から終了年までの各月の最初の日と最後の日を取得
def get_month() -> list:
result = []
for year in range(start_year, end_year + 1):
for month in range(1, 13):
# その月の最初の日と総日数を取得
_, days_in_month = calendar.monthrange(year, month)
# 最初の日
first_day = f"{year}-{month:02}-01"
# 最後の日
last_day = f"{year}-{month:02}-{days_in_month:02}"
result.append((first_day, last_day))
return result
# scores日付を取得する関数
def get_scores_data(sDay: str, eDay: str) -> list:
gameId_list = []
# 正しいURLを置き換える
url = scores_api_url
# ペイロードを設定
payload = {
"stitch_env": "prod",
"sortTemplate": "4",
"sportId": "1",
"sportId": "51",
"startDate": sDay,
"endDate": eDay,
"gameType": "E",
...
}
res = get_api(url, payload)
if res != {}:
# 該当日の試合リストをループ
for game_list in res.get("dates"):
# 該当日の試合をループ
for game in game_list:
gameId_list.append(game.get("gamePk"))
return gameId_list
# ゲームデータを取得する関数
def get_game_date(gameId_list: list) -> list:
result = []
# gameId_listをループしてすべてのgameIdデータを取得
for gameId in gameId_list:
# 正しいgameIdを置き換える
url = game_api_url.replace("game_id", str(gameId))
res = get_api(url, {})
# APIを呼び出して値を取得
if res != {}:
# 以下はres dictから必要な情報を取得する実装
...
result.append(gameData)
return result
# APIを呼び出す関数
def get_api(url: str, payload: dict) -> dict:
res = request.get(url, params=payload)
if res.status_code == 200:
return res.json()
else:
return {}
# プログラムのエントリーポイント
if __name__ == '__main__':
main()
全体的に 10 年間の試合データを取得するプログラムはこのようになります。いくつかの機能は詳細に実装されていませんので、皆さんで試してみてください。完成後には多くの部分を最適化したり、問題に直面したりするかもしれません。以下は考えられる問題と最適化の方向性です。
- データが大きすぎてリクエストがタイムアウトする可能性があります。
- 瞬時に大量の API アクセスが発生し、サーバーがアクセスを拒否する可能性があります。
- 一時保存データが大きすぎてプログラムがクラッシュする可能性があります。
- データを CSV に保存する機能が未実装です。
- 実行済みのデータを保存し、次回起動時に最初から繰り返し実行しないようにできます。
- multiprocessing を使用して処理を加速できます。