以前、私は多くの小さなプロジェクトでクローラーと 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 を使用して処理を加速できる。