六月底即將結束我的爬蟲戰爭, 趕緊把想爬的網站趁空檔來爬一爬. 今天要爬的對象是 NBA 官網的球員資料, 此範例來自下面這本書 :
一. 檢視目標網頁 :
NBA 球員資料網址如下 :
首次進入 NBA 網站時會彈出一個要求同意隱私權政策聲明的視窗 :
按右邊的 "I Accept" 鈕才會顯示表格全貌 :
用 Quick Javascript Switcher 檢測發現, 當關閉 Javascript 功能時表格內的資料即消失不見, 可見此網頁是透過 Javascript 生成的, 必須動用 Selenium 來抓.
本篇測試的任務是要擷取這些分頁的資料儲存到 csv 檔案裡.
二. 分析目標網頁使用 Selenium 擷取資料 :
首先來處理 NBA 網頁初次連線時彈出的隱私權聲明視窗問題, 我們必須按下 "I Accept" 鈕才會使此視窗關閉, 用一個尚未接受隱私權聲明的瀏覽器, 例如 Firefox, 在開發人員工具的 Inspector 中搜尋 "I Accept" 可以找到此按鈕具有 id 屬性 :
<button id="onetrust-accept-btn-handler">I Accept</button>
也可以用其 XPATH //*[@id="onetrust-accept-btn-handler"] 來定位它.
先匯入 Selenium 套件模組並載入 NBA 網頁 :
>>> from selenium import webdriver
>>> driver=webdriver.Firefox()
>>> driver.implicitly_wait(20)
>>> url='https://www.nba.com/stats/players/traditional'
>>> driver.get(url)
接下來為初次拜訪會出現的隱私權聲明彈出視窗, 利用 id 屬性取得視窗中 "I Accept" 按鈕的 button 元素物件, 然後按下它以便關閉隱私權聲明視窗 :
>>> accept_btn=driver.find_element('id', 'onetrust-accept-btn-handler')
>>> accept_btn.text # 確認是 "I Accept" 按鈕
'I Accept'
>>> accept_btn.click() # 接受隱私權聲明關閉彈出視窗
這樣 NBA 球員資料表格就完整呈現了 :
此網頁與上回的 104 人力銀行網頁一樣以分頁方式呈現資料, 但 104 網頁會隨滑鼠滾輪滑動而自動持續載入, NBA 的網頁則不會這樣, 必須按表格右上方的下一頁按鈕或下拉式選單選擇要顯示的分頁.
利用開發人員視窗可以在 Element 頁籤中找到此表格具有 "Crom_table__p1iZz" 的樣式類別, 而且是唯一的 :
我們先抓目前的第一頁資料 :
>>> table=driver.find_element('class name', 'Crom_table__p1iZz') # 取得表格
>>> trs=table.find_elements('tag name', 'tr') # 取得全部 tr 元素物件
>>> len(trs)
51
可見第一頁有 51 列, 接下來走訪每一列, 取出每一欄的資料. 首先來看標示欄位名稱的表格標頭 , 它位於 trs 的索引 0, 欄位名稱釋放在 th 元素裡 :
>>> ths=trs[0].find_elements('tag name', 'th')
>>> header=[th.text for th in ths]
>>> headers
[' ', 'PLAYER', 'TEAM', 'AGE', 'GP', 'W', 'L', 'MIN', 'PTS', 'FGM', 'FGA', 'FG%', '3PM', '3PA', '3P%', 'FTM', 'FTA', 'FT%', 'OREB', 'DREB', 'REB', 'AST', 'TOV', 'STL', 'BLK', 'PF', 'FP', 'DD2', 'TD3', '+/-', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
>>> len(header)
56
可見 th 元素在 "+/-" 欄位之後有一堆空的欄位, 這些都是 hidden 不顯示的欄位 :
可以用串列生成式刪除這些隱藏的空欄位 :
>>> header=[th.text for th in ths if th.text != '']
>>> header
[' ', 'PLAYER', 'TEAM', 'AGE', 'GP', 'W', 'L', 'MIN', 'PTS', 'FGM', 'FGA', 'FG%', '3PM', '3PA', '3P%', 'FTM', 'FTA', 'FT%', 'OREB', 'DREB', 'REB', 'AST', 'TOV', 'STL', 'BLK', 'PF', 'FP', 'DD2', 'TD3', '+/-']
>>> len(header)
30
關於串列生成式用法參考 :
接下來檢視表格內容, 它們都是放在 td 元素裡面, 檢視其中第一列, 即 trs[1] 的內容 :
>>> tds1=trs[1].find_elements('tag name', 'td')
>>> len(tds1)
30
>>> [td.text for td in tds1]
['1', 'Joel Embiid', 'PHI', '30', '6', '2', '4', '41.4', '33.0', '9.8', '22.2', '44.4', '2.2', '6.5', '33.3', '11.2', '13.0', '85.9', '3.2', '7.7', '10.8', '5.7', '4.2', '1.2', '1.5', '3.3', '58.3', '4.0', '1.0', '7.7']
可見每列的欄位數與上面表格標題一致, 都是 30 欄. 接下來可以建立一個串列來儲存第一頁的表個內容, 先建一個 rows 串列來儲存各列內容, 先把前面擷取的表格標題放在第一列 :
>>> rows=[]
>>> rows.append(header)
>>> rows
[[' ', 'PLAYER', 'TEAM', 'AGE', 'GP', 'W', 'L', 'MIN', 'PTS', 'FGM', 'FGA', 'FG%', '3PM', '3PA', '3P%', 'FTM', 'FTA', 'FT%', 'OREB', 'DREB', 'REB', 'AST', 'TOV', 'STL', 'BLK', 'PF', 'FP', 'DD2', 'TD3', '+/-']]
然後用迴圈走訪表格第二列到第一頁的最後一列, 分別將每列的各儲存格內容 td.text 放入 cols 串列中, 到列尾再將整列放入 rows 串列中 :
>>> for tr in trs[1:]: # 從第二列開始至表格尾端
cols=[] # 儲存各欄位內容之串列
tds=tr.find_elements('tag name', 'td')
for td in tds:
cols.append(td.text)
rows.append(cols)
>>> len(rows)
51
可見含表格標頭共 51 列 (每頁有 50 個球員資料). 檢視列內容 :
>>> rows[1]
['1', 'Joel Embiid', 'PHI', '30', '6', '2', '4', '41.4', '33.0', '9.8', '22.2', '44.4', '2.2', '6.5', '33.3', '11.2', '13.0', '85.9', '3.2', '7.7', '10.8', '5.7', '4.2', '1.2', '1.5', '3.3', '58.3', '4.0', '1.0', '7.7']
>>> rows[2]
['2', 'Jalen Brunson', 'NYK', '27', '13', '7', '6', '39.8', '32.4', '11.6', '26.2', '44.4', '2.0', '6.5', '31.0', '7.2', '9.2', '77.5', '0.6', '2.7', '3.3', '7.5', '2.7', '0.8', '0.2', '2.0', '47.6', '3.0', '0.0', '0.9']
>>> rows[50]
['50', 'Brandon Ingram', 'NOP', '26', '4', '0', '4', '36.4', '14.3', '4.8', '13.8', '34.5', '0.5', '2.0', '25.0', '4.3', '4.8', '89.5', '0.3', '4.3', '4.5', '3.3', '2.3', '1.0', '1.3', '2.0', '29.0', '0.0', '0.0', '-11.0']
這樣就可以來將第一個分頁的資料儲存到 csv 檔了 :
>>> import csv
>>> csv_file='nba_players_stata_page_1.csv'
>>> with open(csv_file, 'w', newline='') as f:
writer=csv.writer(f)
writer.writerows(rows)
注意, 此處 open() 務必傳入 newline='', 否則用 Excel 開啟 csv 檔時會多出一個空列. 開啟 nba_players_stata_page_1.csv 檔結果如下 :
可見已順利將第一頁存成 csv 檔了.
上面操作的完整程式碼如下 :
from selenium import webdriver
import csv
driver=webdriver.Firefox()
driver.implicitly_wait(20)
url='https://www.nba.com/stats/players/traditional'
driver.get(url)
try:
accept_btn=driver.find_element('id', 'onetrust-accept-btn-handler')
accept_btn.click()
except Exception as e:
pass
table=driver.find_element('class name', 'Crom_table__p1iZz')
trs=table.find_elements('tag name', 'tr')
ths=trs[0].find_elements('tag name', 'th')
header=[th.text for th in ths if th.text != '']
rows=[]
rows.append(header)
for tr in trs[1:]:
cols=[]
tds=tr.find_elements('tag name', 'td')
for td in tds:
cols.append(td.text)
rows.append(cols)
csv_file='nba_players_stata_page_1.csv'
with open(csv_file, 'w', newline='') as f:
writer=csv.writer(f)
writer.writerows(rows)
如果要將全部分頁都各自存檔可按下一頁鈕重複上面操作. 此按鈕再表格右上方, 在開發人員工具視窗的 Element 頁籤搜尋 "Next Page Button" 即可找到, 它具有 "Pagination_button__sqGoH" 的樣式類別, 搜尋此 class 只找到兩個, 第一個是前一頁按鈕, 第二個便是此下一頁按鈕了 :
此下一頁按鈕 HTML 碼如下 :
<button type="button" title="Next Page Button" class="Pagination_button__sqGoH" data-track="click" data-type="controls" data-pos="next">
<svg xmlns="http://www.w3.org/2000/svg" height="14" width="8" viewBox="0 0 8 14" data-no-icon="right" role="presentation">
<polyline fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="6.5,1.6 1.5,7 6.5,12.4"></polyline>
</svg>
</button>
所以只要找出具有此樣式類別的按鈕, 按第二個即可 :
>>> page_buttons=driver.find_elements('class name', 'Pagination_button__sqGoH')
>>> len(page_buttons)
2
>>> page_buttons[0].get_attribute('title')
'Previous Page Button'
>>> page_buttons[1].get_attribute('title') # 確認第二個即是下一頁按鈕
'Next Page Button'
>>> page_buttons[1].click()
這時 Seleneium 開啟的網頁果然已切換至第二頁了 :
重複執行上面的程式碼即可將第二頁球員資料存入 csv 檔中. 但問題是按到最後一頁時下一頁的按鈕雖然還在, 但是按下去沒作用, 如果要用迴圈來做重複按下一頁按鈕的動作, 必須知道總共有幾頁資料, 這可以從前面的下拉式選單的選項數目取得. 在 Element 頁籤中搜尋可知此下拉式選單有一個 class 名稱 DropDown_select__4pIg9 :
<select name="" class="DropDown_select__4pIg9">
<option value="-1">All</option>
<option value="0">1</option>
<option value="1">2</option>
<option value="2">3</option>
<option value="3">4</option>
<option value="4">5</option>
</select>
但具有此樣式類別名稱的元素有 26 個 :
而分頁選單是其中最後一個 (索引 25), 因此只要取得最後一個下拉式選單的 option 數目, 即可得到頁數 (要減 1, 因第一個選項是 All) :
>>> selects=driver.find_elements('class name', 'DropDown_select__4pIg9')
>>> len(selects)
26
>>> options=selects[25].find_elements('tag name', 'option')
>>> len(options)
6
>>> [option.text for option in options]
['All', '1', '2', '3', '4', '5']
>>> pages=len(options) - 1
>>> pages
5
完整程式碼如下 :
from selenium import webdriver
import csv
import time
driver=webdriver.Firefox()
driver.implicitly_wait(10)
url='https://www.nba.com/stats/players/traditional'
driver.get(url)
try:
accept_btn=driver.find_element('id', 'onetrust-accept-btn-handler')
accept_btn.click()
except Exception as e:
pass
selects=driver.find_elements('class name', 'DropDown_select__4pIg9')
options=selects[25].find_elements('tag name', 'option')
pages=len(options) - 1
print(f'總頁數: {pages}')
for page in range(pages):
print(f'擷取第 {page + 1} 頁 ...')
table=driver.find_element('class name', 'Crom_table__p1iZz')
trs=table.find_elements('tag name', 'tr')
ths=trs[0].find_elements('tag name', 'th')
header=[th.text for th in ths if th.text != '']
rows=[]
rows.append(header)
for tr in trs[1:]:
cols=[]
tds=tr.find_elements('tag name', 'td')
for td in tds:
cols.append(td.text)
rows.append(cols)
csv_file=f'nba_players_stata_page_{page + 1}.csv'
with open(csv_file, 'w', newline='') as f:
writer=csv.writer(f)
writer.writerows(rows)
print(f'第 {page + 1} 頁已存檔 {csv_file}')
class_name='Pagination_button__sqGoH'
page_buttons=driver.find_elements('class name', class_name)
page_buttons[1].click()
time.sleep(1)
print('NBA 球員資料已擷取存檔完成!')
driver.close()
執行結果 :
>>> %Run nba_players_stats_2.py
總頁數: 5
擷取第 1 頁 ...
第 1 頁已存檔 nba_players_stata_page_1.csv
擷取第 2 頁 ...
第 2 頁已存檔 nba_players_stata_page_2.csv
擷取第 3 頁 ...
第 3 頁已存檔 nba_players_stata_page_3.csv
擷取第 4 頁 ...
第 4 頁已存檔 nba_players_stata_page_4.csv
擷取第 5 頁 ...
第 5 頁已存檔 nba_players_stata_page_5.csv
NBA 球員資料已擷取存檔完成!
也可以操控下拉式選單來依序選擇分頁, 這就要用到 Select 類別了, 參考 :
from selenium import webdriver
from selenium.webdriver.support.select import Select
import csv
import time
driver=webdriver.Firefox()
driver.implicitly_wait(10)
url='https://www.nba.com/stats/players/traditional'
driver.get(url)
try:
accept_btn=driver.find_element('id', 'onetrust-accept-btn-handler')
accept_btn.click()
except Exception as e:
pass
selects=driver.find_elements('class name', 'DropDown_select__4pIg9')
page_select=Select(selects[25]) # 分頁下拉式選單
options=page_select.options # 選項 option 物件串列
pages=len(options) - 1 # 要扣掉第一個 All 選項
print(f'總頁數: {pages}')
for page in range(pages):
page_select.select_by_visible_text(str(page + 1)) # 選擇分頁
print(f'擷取第 {page + 1} 頁 ...')
table=driver.find_element('class name', 'Crom_table__p1iZz')
trs=table.find_elements('tag name', 'tr')
ths=trs[0].find_elements('tag name', 'th')
header=[th.text for th in ths if th.text != '']
rows=[]
rows.append(header)
for tr in trs[1:]:
cols=[]
tds=tr.find_elements('tag name', 'td')
for td in tds:
cols.append(td.text)
rows.append(cols)
csv_file=f'nba_players_stata_page_{page + 1}.csv'
with open(csv_file, 'w', newline='') as f:
writer=csv.writer(f)
writer.writerows(rows)
print(f'第 {page + 1} 頁已存檔 {csv_file}')
print('NBA 球員資料已擷取存檔完成!')
driver.close()
此處使用 selenium.webdriver.support.select.Select 類別來建立分頁下拉式選單的 Select 物件, 透過其 options 屬性值串列長度即可得到總頁數, 然後在迴圈中呼叫 Select 物件的 select_by_visible_text() 方法切換分頁.
這個範例非常具有挑戰性, 使用了許多 Selenium 爬蟲技巧. 要注意的是, 此網站除了隱私權聲明地跳出視窗外, 還會不定時跳出邀請註冊或其他公告之跳出視窗, 但因種類不確定且不是每次都會出現, 所以就不個別處理了.
沒有留言:
張貼留言