2024年6月13日 星期四

Python 學習筆記 : Selenium 4 用法 (下)

今日繼續整理 Selenium 用法, 本系列之前的文章參考 :



六. 動作鏈 (Action Chains) :  

從前面的測試中可知, Selenium 可用的網頁頁面操作是呼叫 WebElement 物件的下列三個方法 :
  • send_keys() : 模擬鍵盤的輸入動作
  • click() : 模擬滑鼠的點擊動作
  • submit() : 提交表單 
可見能執行的操作很有限. 其實最完整的滑鼠與鍵盤操作如滑鼠左鍵雙擊 (double click) 或右鍵快顯等動作都封裝在 selenium.webdriver.common.action_chains 模組的 ActionChains 類別中, 可以模擬所有的手動操作, 所以動作鏈才是 Selenium 操作瀏覽器的百寶箱.

動作鏈可用鏈式操作來進行一連串的滑鼠移動, 按鍵與鍵盤輸入, 特別是模擬下拉式功能表選項的點選, 如果直接呼叫選像物件的 click() 方法有時會因為位置定位不正確或被其他元素覆蓋而出現 'not clickable' 錯誤時, 使用動作鏈通常可解決, 提高爬蟲程式的強固性 (robotness). 因為動作鏈就是模仿手動操作那樣, 一步步走到目標元素後才呼叫 perform() 去執行, 因為除了一個動作亦行指令寫法外, 還可以用鏈式呼叫寫法, 故稱為動作鏈 : 

ActionChains 物件.action1.action2.actions3. .... actionx.perform()

使用前要先匯入 ActionChains 類別 : 

>>> from selenium.webdriver.common.action_chains import ActionChains  

用 dir() 檢視類別成員 : 

>>> dir(ActionChains)   
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'click', 'click_and_hold', 'context_click', 'double_click', 'drag_and_drop', 'drag_and_drop_by_offset', 'key_down', 'key_up', 'move_by_offset', 'move_to_element', 'move_to_element_with_offset', 'pause', 'perform', 'release', 'reset_actions', 'scroll_by_amount', 'scroll_from_origin', 'scroll_to_element', 'send_keys', 'send_keys_to_element']

可見 ActionChains 類別定義了許多滑鼠與鍵盤的操作函式, 當傳入 WebDriver 物件建立 ActionChains 實體時, 這些函式就是其方法, 說明如下 :


 ActionChains 物件常用方法 說明
 click(element) 在元素 element 上按一下滑鼠左鍵
 double_click(element) 在元素 element 上點擊滑鼠左鍵兩下
 click_and_hold(element) 在元素 element 上長按滑鼠左鍵
 context_click(element) 在元素 element 上長按滑鼠右鍵
 drag_and_drop(ele1, ele2) 在元素 ele1 上長按滑鼠左鍵並拖曳到元素 ele2 上放開滑鼠
 drag_and_drop_by_offset(ele, x, y) 在元素 ele1 上長按滑鼠左鍵並拖曳偏移座標量 (x, y)
 move_to_element(element) 將滑鼠移動到 element 元素上
 move_to_element_with_offset(
 element, x, y)
 將滑鼠移動到 element 元素上並偏移 (x, y) 座標量
 move_by_offset(x, y) 將滑鼠從目前位置移動 (x, y) 偏移量
 send_keys(text) 在目前的元素上輸入文字 text
 send_keys_to_element(ele, text) 在元素 ele 上輸入文字 text
 key_up(Keys.xxx, element) 在元素 element 上放開鍵盤的 Key.xxx 鍵
 key_down(Keys.xxx, element) 在元素 element 上按下鍵盤的 Key.xxx 鍵
 release(element) 在元素 element 上放開滑鼠之長按
 pause(s) 暫停執行 s 秒
 perform() 執行前面動作鏈的事件


以 Python 官網首頁 https://www.python.org/ 為例, 該網頁上方有一排功能選單, 當滑鼠移到選單標題上時會顯示選單 (注意, 只要 hover 就會顯現, 點擊反而會使選單消失). 以下用兩種方式來操作瀏覽器, 從 Downloads 選單中點擊 Windows 選項來切換至 Windows 版本的 Python 下載頁 :
  1. 直接定位元素後呼叫其物件之 click() 方法
  2. 使用動作鏈來逐步操作
對於這種有選單的網頁, 通常第一種方法會出現例外或錯誤, 無法達成任務, 解決方案就是改用動作鏈, 




在 Chrome 開發者工具的 Element 頁籤中搜尋 "Downloads" 會發現此下載選單是一個 id='downloads' 的 li 元素, 裡面的選項是由 ul-li 製作的, Windows 下載網址位於 class='tier-2 element-3' 的 li 所包裹的 a 元素裡面, 只要找出此 a 元素物件, 呼叫 click() 即可.  




首先建立一個代表 Firefox 瀏覽器的 WebDriver 物件 :

>>> from selenium import webdriver   
>>> driver=webdriver.Firefox()    
>>> type(driver)   
<class 'selenium.webdriver.firefox.webdriver.WebDriver'>   

然後呼叫 driver 的 get() 方法載入 Python 官網 :

>>> driver.get('https://www.python.org/')   

我們先用 id 搜尋外層的 li 元素, 然後用得到的 WebElement 物件的 find_elements() 搜尋裡面的所有 li 元素, 可見共有 8 個 li, Windows 的下載網址放在第 3 個 li (索引 2) 所包覆的 a 元素裡 :

>>> downloads_li=driver.find_element('id', 'downloads')   
>>> inner_lis=downloads_li.find_elements('tag name', 'li')     
>>> len(inner_lis)   
8
>>> windows_link=inner_lis[2].find_element('tag name', 'a') 
>>> windows_link.get_attribute('href')  
'https://www.python.org/downloads/windows/'   
 
當然也可以用外層 li 物件直接搜尋它裡面的 a 元素 (跳過內層 li), 會找到 15 個, 這時 Windows 的下載超連結在第 4 個 a 元素 (索引 3) :

>>> inner_links=downloads_li.find_elements('tag name', 'a')    
>>> len(inner_links)   
15
>>> inner_links[3].get_attribute('href')   
'https://www.python.org/downloads/windows/'   

不管是用上面哪一種方式取得 Windows 的下載超連結 a 元素的 WebElement 物件, 呼叫其 click() 方法都會出現如下 ElementNotInteractableException 錯誤 :

>>> inner_links[3].click()   # 或用 windows_link.click()
.....
raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.ElementNotInteractableException: Message: Element <a href="/downloads/windows/"> could not be scrolled into view

完整程式碼如下 :

# selenium_action_chains_1.py (此程式會出現例外)
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains

driver=webdriver.Firefox()
driver.implicitly_wait(30)
driver.get('https://www.python.org/')
downloads_li=driver.find_element('id', 'downloads')
inner_lis=downloads_li.find_elements('tag name', 'li')
windows_link=inner_lis[2].find_element('tag name', 'a')
windows_link.click()
driver.close()

這種有選單的網頁如果改用動作鏈通常就能解決. 先把 WebDriver 物件傳給 ActionChains 的建構式 ActionChains(), 它會傳回一個 ActionChains 物件 : 

>>> actions=ActionChains(driver)   

然後呼叫 move_to_element() 方法將滑鼠移到上面取得的外層 li 元素 (即 "Downloads" 這個選單), 它會傳回 ActionChains 物件 (代表滑鼠) 目前的位置是在哪個物件上 :

>>> actions.move_to_element(downloads_li)     
<selenium.webdriver.common.action_chains.ActionChains object at 0x0000013E56B34B50>

當滑鼠移到 "Downloads" 時選單應該已經展開 (但還沒呼叫 perform 不會真的出現, 這是假想的), 注意, 這些選單都是 hover 就出現的, 千萬不要去按, 按的話選單就消失了, 而是直接按 Windows 連結就可以, 當然, 如果你要先呼叫 move_to_element(windows_link) 將滑鼠移到 Windows 連結上再按也可以, 但那是多此一舉, 因為在選單顯現時 Windows 連結就已出現, 直接按它即可 : 

>>> actions.click(windows_link)       
<selenium.webdriver.common.action_chains.ActionChains object at 0x0000013E56B34B50>   

最後呼叫 perform() 方法即可得到目標網頁 : 

>>> actions.perform()   




這些方法可以使用鏈式呼叫一氣呵成, 這也是為何稱為 chain 的原因 :

>>> actions.move_to_element(downloads_li).click(windows_link).perform()  

得到目標網頁後呼叫 driver.close() 關閉瀏覽器 :

>>> driver.close()  

完整程式碼如下 :

# selenium_action_chains_2
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
import time

driver=webdriver.Firefox()
driver.implicitly_wait(30)
driver.get('https://www.python.org/')
downloads_li=driver.find_element('id', 'downloads')
inner_lis=downloads_li.find_elements('tag name', 'li')
windows_link=inner_lis[2].find_element('tag name', 'a')
actions=ActionChains(driver)
actions.move_to_element(downloads_li)
#actions.move_to_element(windows_link)  # 多此一舉
actions.click(windows_link)
actions.perform()
time.sleep(10)
driver.close()

此處使用 time.sleep(10) 讓頁面停留 10 秒以便能確認已到達目標網頁. 

參考 :


沒有留言 :