由於之前接了滿多小案子是在做爬蟲+LINE Bot應用,我一直認為爬蟲這個技術或是流程應該是廣為人知,加上現在各種AI 工具像是ChatGPT, Bard, etc...的崛起,現在已經沒有遇過有人在問爬蟲的案子了,今天遇到了朋友問我有關於爬蟲爬MLB網站資料的問題,當時我很簡單的用瀏覽器的開發者管理工具裡的Network看了一下傳給我的MLB網站,跟他說了那個API可以取到他要的資料,但對方一臉
的表情後才發覺原來很多人不清楚怎麼開始製作爬蟲或是誤會了只要學python就會爬蟲的這種迷思,也因為這樣才會有這一篇文章的誕生。
目標資料: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
由於是要抓取頁面上的數據,基本上網頁的數據源自於兩個地方
這邊我會使用瀏覽器的開發人員工具>網路 並且將filter 切至Fetch/XHR然後重新整理網頁,在逐一檢查每一筆request的response,經過每一筆的檢查後我們發現這個request應該是我們要找的API,因為他的response 有著網頁上顯示的資料
點擊標頭我們可以看到他的API URL
https://ws.statsapi.mlb.com/api/v1.1/game/717664/feed/live?language=en
我們合理懷疑717664是Game的編號,要怎麼確認可以看一下網頁的網址
https://www.mlb.com/gameday/braves-vs-phillies/2023/09/11/717664/final/box
看起來717664是Game的編號,看其他場比賽也是這樣
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
我們可以瀏覽>右鍵>複製Object或是回應>全選>複製
簡單的話可以貼到線上的Json online parser(jsoneditoronline, json.parser)檢查,可以使用Ctrl + F 搜尋關鍵字,我們可以看到在API response json中的info有著上面我們要抓取的數據,Bingo!
這邊我們需要確認API是否需要特別的認證或是其他東西使他只有該網頁可以存取,我們可以使用Postman來測試,如果不會使用這個工具的可以搜尋一下教學文章。
我們確認了只要可以透過呼叫API就能取得資訊,這樣就可以進行下一步了。
程式是人設計的,所以要寫出爬蟲自己一定要知道整個流程才有辦法寫出來,並不是看個書看個教學就能夠寫出來,以這個例子為例,爬蟲的整體邏輯應該會是
第一步是該如何抓取所有場次比賽編號呢,我們可以透過這個網址去找上一層的網址
https://www.mlb.com/gameday/braves-vs-phillies/2023/09/11/717664/final/box
使用
https://www.mlb.com/gameday/
結果導頁至
https://www.mlb.com/scores
就是這邊了,再來使用剛剛方式去找到是那個reques取得這些資料的,
找到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,我們可以更改一下測試看看是不是可以抓取多天資料,如果可以的話可以加速我們抓取資料的速度。
Bingo! 我們可以一次取回2023-09-01 ~ 2023-09-11的所有比賽資料,但呼叫的時間長達8秒以及response 資料量很大,這邊可能會以最多一次一個月的方式來抓取資料,太多了可能會timeout。
這邊還先不要急著寫Code,我們要先將上面的流程轉化成程式的流程與邏輯在來寫,這邊簡單的示範程式碼該怎麼轉化成爬蟲流程
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編號改變的地方改成一個特定取代值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()
# loop 所有月份
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
payload = {
"stitch_env": "prod",
"sortTemplate": "4",
"sportId": "1",
"sportId": "51",
"startDate": sDay,
"endDate": eDay,
"gameType": "E",
...
}
res = get_api(url, payload)
if res != {}:
# loop 該天賽事list
for game_list in res.get("dates"):
# loop 該天賽事
for game in game_list:
gameId_list.append(game.get("gamePk"))
return gameId_list
# 抓取game資料函數
def get_game_date(gameId_list: list) -> list:
result = []
# loop 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年份比賽資料大致上程式長這樣,有些功能沒有仔細實作,留給大家試試看,在完成之後可能會有許多地方可以優化或是會遇到問題,以下是可能遇到的問題和優化方向。