2018年5月27日 星期日

Python 學習筆記 : Selenium 模組瀏覽器自動化測試 (二)

Selenium 對我這個 "自動控" 來說可說是天上掉下來的禮物. 我的野心不僅僅是要把一切流程自動化, 還要能智慧化, 最好能創造一個可教育的人工意識來代理我們的思考與決策 ... 不過這都太遠也太充滿想像力了, 還是回過神來繼續測試 Selenium 吧.

以下測試參考了下面幾本書 :
  1.  Python 網路爬蟲實戰 (松崗, 胡松濤) 第 8 章
  2.  Python 自動化的樂趣 (碁峰, AL Sweigart) 第 11 章
  3.  Python 程式設計實務 (博碩, 何敏煌) 第 10-3 節
  4.  Python 初學特訓班 (碁峰, 文淵閣工作室) 第 6-3, 6.4 節
  5.  不只是測試-Python 網路爬蟲王者 Slenium (佳魁, 蟲師) 第 4 章
  6.  Learning Selenium Testing Tools With Python (Packt, Unmesh Gundecha) 第 4 章
本系列之前的文章參考 :

Python 學習筆記 : 安裝執行環境與 IDLE 基本操作
Python 學習筆記 : 檔案處理
Python 學習筆記 : 日誌 (logging) 模組測試
Python 學習筆記 : 資料庫存取測試 (一) SQLite
Python 學習筆記 : 資料庫存取測試 (二) MySQL
Python 學習筆記 : Selenium 模組瀏覽器自動化測試 (一)

除了上列書籍外, 我還參考了下列 Selenium 教學網站 :

Selenium with Python
Free Selenium Tutorials
Selenium Tutorial for Beginners
https://www.tutorialspoint.com/selenium/index.htm

從前一篇測試可知 Chrome 的 Web Driver 安裝最沒問題, 所以以下測試是以 Chrome 為對象, 理論上 Selenium 的所有函數在所支援的瀏覽器都是通用的.


1. 取得瀏覽器物件之屬性 : 

呼叫 webdriver.Chrome(), webdriver.Opera() 或 webdriver.Firfox() 等函數將傳回一個 WebDriver 物件 (瀏覽器物件), Selenium Webdriver API 在此物件中提供了許多屬性與方法以操控瀏覽器, 例如呼叫 get() 方法可在模擬瀏覽器中開啟指定網頁, 透過下列屬性可取得已開啟網頁之屬性 :

 屬性 說明
 name 瀏覽器名稱
 title 目前開啟網頁之標題
 current_url 目前開啟網頁之 URL
 page_source 目前開啟網頁之原始碼
 session_id 網頁連線 id
 capabilities 瀏覽器功能設定

以開啟 "Webbots, Spiders and Screen Scrapers" 這本書的測試網頁 hello_world.html 為例 :

>>> from selenium import webdriver         #匯入 webdriver 模組
>>> browser=webdriver.Chrome()              #開啟模擬瀏覽器
>>> browser.get("http://www.webbotsspidersscreenscrapers.com/hello_world.html") 
>>> browser.name                                          #瀏覽器名稱
'chrome'
>>> browser.title                                             #網頁標題
'Hello, world!'
>>> browser.current_url                                #網頁 URL
'http://www.webbotsspidersscreenscrapers.com/hello_world.html' 
>>> browser.page_source                                #網頁原始碼
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"><html xmlns="http://www.w3.org/1999/xhtml"><head>\n\t<title>Hello, world!</title>\n</head>\n\n<body>\nCongratulations! If you can read this, <br />\nyou successfully downloaded this file.\n\n\n</body></html>'
>>> browser.session_id                                    #連線 ID
'ba0f0e4f432eb1a2f0280736f20f2825'
>>> browser.capabilities                                  #瀏覽器功能設定
{'acceptInsecureCerts': False, 'acceptSslCerts': False, 'applicationCacheEnabled': False, 'browserConnectionEnabled': False, 'browserName': 'chrome', 'chrome': {'chromedriverVersion': '2.38.552522 (437e6fbedfa8762dec75e2c5b3ddb86763dc9dcb)', 'userDataDir': 'C:\\Users\\cht\\AppData\\Local\\Temp\\scoped_dir5572_4888'}, 'cssSelectorsEnabled': True, 'databaseEnabled': False, 'handlesAlerts': True, 'hasTouchScreen': False, 'javascriptEnabled': True, 'locationContextEnabled': True, 'mobileEmulationEnabled': False, 'nativeEvents': True, 'networkConnectionEnabled': False, 'pageLoadStrategy': 'normal', 'platform': 'Windows NT', 'rotatable': False, 'setWindowRect': True, 'takesHeapSnapshot': True, 'takesScreenshot': True, 'unexpectedAlertBehaviour': '', 'version': '66.0.3359.181', 'webStorageEnabled': True}


2. 操控瀏覽器之位置與大小 : 

瀏覽器物件的下列方法可操控瀏覽器之位置與大小 :

 方法 說明
 get_window_position() 取得瀏覽器視窗左上角位置
 set_window_position(x, y) 設定瀏覽器視窗左上角位置
 get_window_size() 取得瀏覽器視窗大小
 set_window_size(x, y) 設定瀏覽器視窗大小
 maximize_window() 將瀏覽器視窗最大化
 minimize_window() 將瀏覽器視窗最小化

例如 :

>>> browser.get_window_position() 
{'x': 10, 'y': 10}
>>> browser.set_window_position(50, 100) 
>>> browser.get_window_position()   
{'x': 50, 'y': 100}
>>> browser.get_window_size()   
{'width': 1050, 'height': 664}
>>> browser.set_window_size(1024, 768) 
>>> browser.get_window_size()   
{'width': 1024, 'height': 738}
>>> browser.maximize_window()   
>>> browser.get_window_size() 
{'width': 1296, 'height': 698}
>>> browser.minimize_window()   
Traceback (most recent call last):
  File "<pyshell#22>", line 1, in <module>
    browser.minimize_window()
  File "C:\Python36\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 740, in minimize_window
    self.execute(Command.MINIMIZE_WINDOW)
  File "C:\Python36\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 314, in execute
    self.error_handler.check_response(response)
  File "C:\Python36\lib\site-packages\selenium\webdriver\remote\errorhandler.py", line 208, in check_response
    raise exception_class(value)
selenium.common.exceptions.WebDriverException: Message: unknown command: session/ba0f0e4f432eb1a2f0280736f20f2825/window/minimize 

奇怪, 為何 minimize_window() 不動作呢? 錯誤訊息是 "unknown command", 難道 Chrome 沒有實作此方法? 搜尋網路發現有人也遇到此問題, 但看似無解, 參考 :

Python Selenium浏览器最小化方法minimize_window(self)抛异常

我在下列文章看到模擬 minimize_window() 的方法 :

Selenium Python minimize browser window

>>> browser.set_window_position(-3000, 0)     #模擬視窗最小化
>>> browser.set_window_position(0, 0)             #視窗復原


 3. 操控瀏覽器按鈕 : 

瀏覽器基本上有 "上一頁", "下一頁", "重新載入", 以及 "關閉" 四個按鈕, 可以透過瀏覽器物件的下列方法用程式操控 :

 方法 說明
 back() 按瀏覽器 "上一頁" 鈕
 forward() 按瀏覽器 "下一頁" 鈕
 refresh() 按瀏覽器 "更新" 鈕
 quit() 按瀏覽器 "關閉" 鈕, 同時關閉驅動程式
 close() 按瀏覽器 "關閉" 鈕

例如 :

>>> from selenium import webdriver         #匯入 webdriver 模組
>>> browser=webdriver.Chrome()              #開啟模擬瀏覽器
>>> urls=["http://www.google.com.tw",     #URL 串列
  "http://tw.yahoo.com", 
  "https://twitter.com"] 
>>> for url in urls:                                         #依序開啟網頁 (最後停在 Twitter)
browser.get(url)   

>>> browser.current_url                               #最後停在 Twitter
'https://twitter.com/'
>>> browser.back()                                        #回前一頁 (YAHOO)
>>> browser.current_url                             
'https://tw.yahoo.com/'
>>> browser.back()                                        #回前一頁 (Google)
>>> browser.current_url 
'https://www.google.com.tw/?gws_rd=ssl'
>>> browser.forward()                                   #回後一頁 (YAHOO)
>>> browser.current_url 
'https://tw.yahoo.com/'
>>> browser.forward()                                  #回後一頁 (Twitter)
>>> browser.current_url   
'https://twitter.com/'
>>> browser.refresh()                                    #重新載入

上例依序開啟 Google, YAHOO, Twitter 三個網頁, 最後網頁是載入 Twitter. 透過呼叫 back(), forward() 來操控載入上一頁與下一頁動作, 並透過 cuurent_url 屬性來檢查是否載入正確網頁.


4. 儲存網頁快照 (screenshot) : 

瀏覽器物件有一個 save_screenshot() 方法可將目前網頁儲存為 PNG 檔案下載至本機目錄下, 傳回值為 True 表示存檔成功, False 為失敗.

 方法 說明
 save_screenshot(filename) 將目前網頁儲存為 png 檔案下載

傳入參數若為單純檔名, 則 PNG 檔將儲存在 Python 安裝目錄下; 也可以傳入包含路徑之檔名, 這樣 PNG 檔就會存在指定目錄下, 但要注意在 Windows 下路徑分隔符是兩個倒斜線 (Escape), 例如 :

>>> from selenium import webdriver         #匯入 webdriver 模組
>>> browser=webdriver.Chrome()              #開啟模擬瀏覽器
>>> browser.get("http://tw.yahoo.com")    #開啟 YAHOO
>>> browser.save_screenshot("yahoo.png")      #儲存快照 PNG
True
>>> browser.save_screenshot("d:\Python\test\yahoo.png")   #須雙倒斜線
False
>>> browser.save_screenshot("d:\\Python\\test\\yahoo.png")   #指定路徑
True

注意, 所儲存是完整網頁的快照, 不受視窗大小的影響, 用 set_window_size()將瀏覽器視窗設成多小都能下載到完整網頁之快照.


5. 操控網頁元素 : 

這部分就是 Selenium Webdriver 作為網路爬蟲的核心功能, 在上面的測試中利用 page_source 屬性可取得所開啟網頁之原始網頁 HTML 內容, Webdriver 本身自帶了許多方法可從 HTML 原始碼中擷取資訊, 不需要用到 BeautifulSoup. 此外, 還可以透過下列方法來取得網頁元素 (標籤 tag), 以便進行網頁操控.

 方法 說明
 find_element(by, value) 使用 by 指定之方法取得第一個符合 value 的元素
 find_element_by_class_name(name) 傳回符合指定 class 名稱之元素
 find_elements_by_class_name(name) 傳回符合指定 class 名稱之元素串列
 find_element_by_css_selector(selector) 傳回符合指定 CSS 選擇器名稱之元素
 find_elements_by_css_selector(selector) 傳回符合指定 CSS 選擇器名稱之元素串列
 find_element_by_id(id) 傳回符合指定 id 之元素
 find_elements_by_id(id) 傳回符合指定 id 之元素串列
 find_element_by_link_text(text) 傳回符合指定超連結文字之元素
 find_elements_by_link_text(text) 傳回符合指定超連結文字之元素串列
 find_element_by_partial_link_text(text) 傳回符合部分指定超連結文字之元素
 find_elements_by_partial_link_text(text) 傳回符合部分指定超連結文字之元素串列
 find_element_by_name(name) 傳回符合指定元素名稱之元素
 find_elements_by_name(name) 傳回符合指定元素名稱之元素串列
 find_element_by_tag_name(tag) 傳回符合指定標籤名稱之元素
 find_elements_by_tag_name(tag) 傳回符合指定標籤名稱之元素串列

呼叫 find_element_ 方法會傳回一個代表網頁元素的 WebElement 物件, 如果是呼叫 find_elements_ 方法則會傳回 WebElement 物件組成的串列. 此 WebElement 物件有如下屬性與方法可取得網頁元素的內容或對元素進行操控, 例如按下按鈕或填入資料等.

 屬性或方法 說明
 tag_name 元素 (標籤) 名稱
 location 傳回元素對應螢幕左上角之座標 (x, y 字典)
 text 開頭標籤與結尾標籤之間的文字 (即 innerText)
 get_attribute(attr) 傳回屬性名稱 attr 之內容
 click()  觸發元素之 click 事件 (按鈕, 超連結, 表單提交等元素)
 send_keys(str) 對元素傳送文字 str (文字欄位或文字區域 textarea 元素)
 clear() 清除文字欄位或文字區域之內容
 is_displayed() 傳回 True (元素可見) 或 False (元素不可見)
 is_enabled() 傳回 True (元素可用) 或 False (元素不可用)
 is_selected() 傳回 True (元素有被勾選) 或 False (元素沒被勾選) : 核取方塊/選項圓鈕


Invent With Python 網站首頁為例, 在 Chrome 按 F12 或按 Ctrl + U 開啟 "檢視網頁原始碼", 可知有一個 id 屬性值為 navbarSupportedContent 的 div 元素  :

<body style="background-color: #caeab7;">

<nav class="navbar navbar-toggleable-md">
  <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon text-white">☰</span>
  </button>
  <a class="navbar-brand" href="/">Invent with Python</a>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item dropdown">
        <a class="nav-link dropdown-toggle" id="navbarDropdownReadMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
          Read for Free
        </a>
        <div class="dropdown-menu" aria-labelledby="navbarDropdownReadMenuLink">
          <a class="dropdown-item" href="https://automatetheboringstuff.com">Automate the Boring Stuff with Python</a>
          <a class="dropdown-item" href="/cracking">Cracking Codes with Python</a>
          <a class="dropdown-item" href="/invent4thed">Invent Your Own Computer Games with Python</a>
          <a class="dropdown-item" href="/pygame">Making Games with Python & Pygame</a>
          <a class="dropdown-item" href="https://inventwithscratch.com">Scratch Programming Playground</a>
        </div>
      </li>

.... (略)

      <li class="nav-item">
        <a class="nav-link" href="https://www.youtube.com/user/Albert10110">YouTube</a>
      </li>

      <li class="nav-item">
        <a class="nav-link" href="https://www.reddit.com/r/inventwithpython">Forum</a>
      </li>

      <li class="nav-item">
        <a class="nav-link" href="/blog">Blog</a>
      </li>

      <li class="nav-item">
        <a class="nav-link" href="/#donate">Donate</a>
      </li>

先利用 find_element_by_id() 方法取得此 div 元素之 WebElement 物件, 就可以用上表中的屬性與方法取得此標籤之內容, 例如 :

>>> from selenium import webdriver 
>>> browser=webdriver.Chrome() 
>>> browser.get("http://inventwithpython.com/") 
>>> divEle=browser.find_element_by_id("navbarSupportedContent") 
>>> type(divEle)   
<class 'selenium.webdriver.remote.webelement.WebElement'>   
>>> divEle.tag_name 
'div'
>>> divEle.get_attribute("class") 
'collapse navbar-collapse'
>>> divEle.location 
{'x': 0, 'y': 0}
>>> divEle.text   
'' 

以 id 搜尋理論上應該只找到一個 WebElement 物件, 若以 class 或 tag 等來尋找則會找到多個 WebElement 物件, 這時就會將這些物件放在 list 中傳回, 例如搜尋 class 屬性為 dropdown-item 的元素會找到具有此 class 之多個超連結 a, 這些 WebElement 物件會放在串列中傳回, 可以用 for in 來迭代顯示 a 元素中的 href 屬性 (即網址), 例如 :

>>> classEles=browser.find_elements_by_class_name("nav-link")
>>> for ele in classEles: 
print(ele.get_attribute("href")) 


None
None
None
https://www.youtube.com/user/Albert10110
https://www.reddit.com/r/inventwithpython
http://inventwithpython.com/blog
http://inventwithpython.com/#donate

前三個 None 是因為找到三個 class="nav-link dropdown-toggle" 的無 href 屬性之 a 元素之故. 

沒有留言 :