2024年6月28日 星期五

Python 學習筆記 : 網頁爬蟲實戰 (十五) NBA 球員分頁資料

六月底即將結束我的爬蟲戰爭, 趕緊把想爬的網站趁空檔來爬一爬. 今天要爬的對象是 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 元素裡 :




在 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 爬蟲技巧. 要注意的是, 此網站除了隱私權聲明地跳出視窗外, 還會不定時跳出邀請註冊或其他公告之跳出視窗, 但因種類不確定且不是每次都會出現, 所以就不個別處理了. 

沒有留言:

張貼留言