[Scratch & Python] Selenium 快速入門 === # Selenium 簡介 簡單來說,Selenium 可以用程式碼來模擬一個人在瀏覽器(Chrome, Safari, Firefox, ...)中的動作。一般來說,Selenium 通常被作為自動化測試(Automated Testing)的工具,但由於他可以模擬在瀏覽器中的操作,所以也可以作為讓我們可以自動化某些操作的輔助工具。 在作為輔助工具上,Selenium 和 PyAutoGui 不同的地方為:在滑鼠方面,PyAutoGui 是使用「座標定位」的方式去進行常見的操作(click, doubleclick, ...),而 Selenium 是使用選取「網頁元素(element)」的方式去操作。 這篇教學將以 Python 程式來自動化模擬網頁操作為主軸。 # 前備知識 ## HTML:用來描述網頁的內容 在使用 Selenium 之前,我們需要先了解「網頁元素(element)」到底是怎麼一回事。 ### 標籤、元素、屬性 這是一段簡單的 html 代碼: ```htmlmixed= <p class="description"> 你好世界,邀請您<strong>每週四下午</strong>一同來向世界說你好。 <a href="https://www.nctu.edu.tw">交大首頁請點我</a> </p> ``` 在 HTML 裡面,我們會使用許多**標籤(tag)** 來描述網頁的內容,如 `p`(段落)、`strong`(粗體字)、`a`(超連結),並可以在標籤內加上其**屬性(Attribute)**,如 `class="description"`(CSS class)、`href="https://www.nctu.edu.tw"`(設置超連結網址),而標籤會有開始標籤(start tag)與結束標籤(end tag),在其之間可填入內容,接著這一整串變形成了一個**元素(element)**。 * 標籤(tag):用來標記一個元素的開始與結束,如`p`、`strong`、`a` * 屬性(attribute):為一個元素定義屬性,如 `class="description"`、`href="https://www.nctu.edu.tw"` * 元素(element):由開始標籤、內容與結束標籤構成,如`<a href="https://www.nctu.edu.tw">交大首頁請點我</a>`、`<strong>每週四下午</strong>`、上方整段 code。 ### class 與 ID 不同的 HTML 標籤分別代表不同的意涵,如 `p` 代表段落、`a` 代表超連結、`img` 代表圖片、`strong` 代表粗體文字。為了讓網頁更好看,人們想要透過 **CSS(Cascading Style Sheets,描述 HTML 的樣式)** 去詳細設定網頁中每一處的顯示樣式為何。 通常我們會建立另一個 .css 檔案去撰寫樣式,而此時,我們就需要指定特定樣式要在哪個元素上生效。我們稱之為 **CSS Selector (CSS 選擇器)** ,替我們指定 HTML 中的特定元素。而 CSS Selector 可簡單分為以下三種: * id Selector:對應到 `id="xxxx"` 的元素上。每個網頁只應該出現一次(identification 的概念)。在 CSS 中寫作 **#xxxx**。 * class Selector:對應到 `class="xxxx"` 的元素上。每個網頁可以出現很多次。在 CSS 中寫作 **.xxxx**。 * element Selector:對應到 tag name。如 `p`、`strong` 等,在 CSS 直接寫 tag name。 如果以上方的 HTML code 來說,假設我想要讓整個 `<p class="description">...</p>` 段落的字為藍色,我可以撰寫以下的 CSS: ```css .description { color: blue; } // 使用 class Selector ``` 如果我們寫 ```css p { color: blue } // 使用 element Selector ``` 則所有使用 p 的元素的字都會是藍色。 此外,CSS Selector 還可以有**階層**的方式一層一層指定下去: ```css p.description a { color: red; // 把「交大首頁請點我」設為紅色 } ``` 或是一次指定很多個東西: ```css strong, a { color: green; // 把所有粗體字和連結都設為綠色 } ``` 還有很多詳細的用法,想了解的同學可以瀏覽:[W3School CSS 教程](http://www.w3school.com.cn/css/) 與 [W3School HTML 教程](http://www.w3school.com.cn/html/index.asp)。 ## 怎麼找到別人網頁中特定元素的 CSS Selector? 在接下來的教學中,我們會需要寫一些自動化的程式來模擬網頁上的操作。首先,有了 CSS Selector 的概念後,我們若可以知道網頁上某些按鈕、連結、文字、照片等元素的 CSS Selector,就可以把這些 Selector 提供給程式,讓程式來定位網頁中的元素並對其進行操作。 在 Chrome 中,假設我們要知道交大首頁的「在校生」連結的元素的 Selector 為何,首先我們先在連結上點右鍵,並點選「檢查」。 ![](https://i.imgur.com/LAB4qVT.png) 然後在反白的那行上面,右鍵點選 **Copy** > **Copy selector**: ![](https://i.imgur.com/A03BmLc.png) 我們可以拿到一串針對「在校生」這一個連結的 Selector: ``` #groupNav > div > ul:nth-child(1) > li.item-511 > a ``` 之後我們可以把這個用在 Selenium 中的**選擇網頁元素**的部分。 # 開始使用 Selenium ## Selenium 安裝說明 - 詳見:[[Scratch & Python] Selenium 安裝說明](https://hackmd.io/s/BJ8l4Uhkm) ## 從範例開始 ### 範例程式 A:打開交大首頁並點擊「在校生」 ```python= from selenium import webdriver from time import sleep driver = webdriver.Chrome() driver.get("http://www.nctu.edu.tw/") assert "NCTU 國立交通大學" in driver.title student = driver.find_element_by_css_selector("#groupNav > div > ul:nth-child(1) > li.item-511 > a") student.click() sleep(5) driver.close() ``` ### 程式碼說明 A 首先,我們需要將 selenium 給引入,而其中的 selenium.webdriver 模組會涵蓋我們會使用到的指令。另外也引入了 time.sleep 模組,稍後會用到。 ```python= from selenium import webdriver from time import sleep ``` 再來,下面第一行可以打開 Chrome,而第二行 `driver.get(網址)` 則是告訴 webdriver 要連到哪一個網址去。 ```python=4 driver = webdriver.Chrome() driver.get("http://www.nctu.edu.tw/") ``` 使用 `driver.title` 可以取得當前網頁標題,並使用 assert 來確定我們連到的網頁標題叫做 `NCTU 國立交通大學`。 ```python=6 assert "NCTU 國立交通大學" in driver.title ``` `driver.find_element_by_css_selector` 可以用來定位特定 CSS Selector,在此我們使用前面拿到的「在校生」連結的 Selector,當作參數傳進函式。此時 student 即為「在校生」那一個連結元素。 我們可以寫 `student.click()` 來點擊該元素。 ```python=7 student = driver.find_element_by_css_selector("#groupNav > div > ul:nth-child(1) > li.item-511 > a") student.click() ``` 最後,為了看到我們真的有連到「在校生」的那一頁,讓程式暫停五秒。最後再執行 `driver.close()` 來關閉 webdriver,程式結束。 ```python=9 sleep(5) driver.close() ``` ### 範例程式 B > 注:同 [Getting Started](http://selenium-python.readthedocs.io/getting-started.html)。 ```python= from selenium import webdriver from selenium.webdriver.common.keys import Keys from time import sleep driver = webdriver.Chrome() driver.get("http://www.python.org") assert "Python" in driver.title elem = driver.find_element_by_name("q") elem.clear() elem.send_keys("array") elem.send_keys(Keys.RETURN) assert "No results found." not in driver.page_source sleep(5) driver.close() ``` 這段程式碼會到 python.org 的網站上,在搜尋框中鍵入 array,並看看搜尋結果頁有沒有結果。 ### 程式碼說明 B 首先,我們多載入一個 Keys,讓我們之後可以對一個輸入框按鍵盤的特定按鍵(如 Return, Ctrl, Shift 等等,稍後會用到的是 Return 鍵) ```python=1 from selenium import webdriver from selenium.webdriver.common.keys import Keys from time import sleep ``` 改成進入 python.org。 ```python=5 driver = webdriver.Chrome() driver.get("http://www.python.org") assert "Python" in driver.title ``` ![](https://i.imgur.com/3zXHewa.png) 從 Chrome 檢查元素面板中,可以發現那一個搜尋框有著 `name="q"` 的屬性。Selenium 同時也支援用 `name` 屬性來尋找元素,所以我們可以寫 `driver.find_element_by_name("q")`。 接著,`elem.clear()` 可以保證搜尋框會是乾淨的。接著我們使用 `elem.send_keys("array")`,代表在 elem 上按下鍵盤的按鍵 `array`。最後再按下一個 Return 鍵,用 `Keys.RETURN` 表示。 ```python=8 elem = driver.find_element_by_name("q") elem.clear() elem.send_keys("array") elem.send_keys(Keys.RETURN) ``` > 詳細的 Keys 用法可參考:[module-selenium.webdriver.common.keys](http://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.common.keys) > name="xxxx" 是**網頁表單**會使用到的一個屬性,用來指定該輸入框資料的名稱(name)。 ## 定位元素 上面的範例中我們分別獲取了「在校生」連結元素與 Python.org 的搜尋輸入框元素,前者使用 CSS Selector 的方式(find_element_by_css_selector),後者使用 name 屬性(find_element_by_name)。以下方法的回傳型態是 **WebElement** 。 Selenium 還提供了其他選擇網頁元素的方法,如下: * find_element_by_id:使用 id Selector * find_element_by_name:使用 name 屬性 * find_element_by_xpath:使用 xpath(在此略過) * find_element_by_link_text:假設有 `<a href="xxx">這是連結文字</a>` 連結元素與文字,則可以使用 `find_element_by_link_text("這是連結文字")` 來定位之。 * find_element_by_partial_link_text:假設有 `<a href="xxx">這是連結文字</a>` 連結元素與文字,則可以使用 `find_element_by_partial_link_text("文字")` 來部分匹配並定位之。 * find_element_by_tag_name:使用 tag name * find_element_by_class_name:使用 class Selector * find_element_by_css_selector:使用 CSS Selector 以上的方法會回傳找到的第一個元素,如果想要支援回傳所有符合條件的元素,可以使用以下方法(element 加 s),會回傳一個 list: * find_elements_by_name * find_elements_by_xpath * find_elements_by_link_text * find_elements_by_partial_link_text * find_elements_by_tag_name * find_elements_by_class_name * find_elements_by_css_selector ## 操作元素 通常我們會把找到的元素指派(assign)給一個變數,如 ```python= elem = driver.find_element_by_name("q") ``` 此時我們就可以對這個元素(如 elem)進行點擊與按下鍵盤按鍵: ```python= elem.send_keys("array") elem.click() ``` 如果要支援更多操作,如雙擊、拖拉、按住按鍵不放等,我們需要使用 Action Chains。 ## Action Chains > API 文件:[Action Chains](http://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.common.action_chains) 在使用 Action Chains 前,可以載入 ActionChains class 讓程式更簡單一點: ```python= from selenium.webdriver.common.action_chains import ActionChains ``` 接著,Action Chains 支援絕大多數在網頁中會有操作,如下: * click(on_element = None):點擊 * double_click(on_element = None):雙擊 * drag_and_drop(source, target):拖拉 * key_down(value, on_element = None):按著某按鍵 * key_up(value, on_element = None):放開某按鍵 * move_by_offset(xoffset, yoffset):游標移動 (xoffset, yoffset) * move_to_element(on_element):游標移動到 on_element * send_keys(*keys):輸入某些按鍵 * send_keys_to_element(on_element, *keys):對 on_element 輸入某些按鍵 * perform():執行 Action Chains ### 範例 使用時,只要先建立一個新的 Action Chains,之後就可以開始執行你要的操作。下面的範例是按下 Ctrl + c。 ```python= actions = ActionChains(driver) actions.key_down(Keys.CONTROL) actions.send_keys('c') actions.key_up(Keys.CONTROL) actions.perform() ``` 也可以寫成這樣: ```python= ActionChains(driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform() ``` ## 切換視窗(window)與頁框(frame) ### 切換視窗(window) 在網頁中,我們時常會遇到點擊一個按鈕或連結後,就跳出一個新的視窗(window)。Selenium 也可以幫我們處理這類型的操作。 * `driver.window_handles`:取得所有的視窗(回傳一個 list) * `driver.current_window_handle`:取得當前視窗(回傳一個 string) * `driver.switch_to.window(window_name)`:切換至 window_name。 範例程式:先打開交大網頁的網站導覽頁,再點擊校園公告頁,並切換到校園公告頁。 ```python= from selenium import webdriver from time import sleep driver = webdriver.Chrome() driver.get("http://www.nctu.edu.tw/sitemap") elem = driver.find_element_by_link_text("2-1 校園公告") elem.click() print("driver.window_handles = ", driver.window_handles) # 回傳兩個分頁 [網站導覽, 校園公告] print("driver.current_window_handle = ", driver.current_window_handle) # current_window_handle 會回傳「網站導覽」分頁 driver.switch_to.window(driver.window_handles[-1]) # 切換到「校園公告」分頁 print("driver.current_window_handle = ", driver.current_window_handle) # current_window_handle 會回傳「校園公告」分頁 sleep(5) driver.close() ``` 會輸出: ``` driver.window_handles = ['CDwindow-ED23706D38AEAE05EDF9F6AC1AC0EBEB', 'CDwindow-A480A2DE4F7109B0A44395E249F7A678'] driver.current_window_handle = CDwindow-ED23706D38AEAE05EDF9F6AC1AC0EBEB driver.current_window_handle = CDwindow-A480A2DE4F7109B0A44395E249F7A678 ``` ### 切換頁框(frame) 在 HTML 中,有一類的元素我們必須特別處理:頁框(frame)。頁框白話地說,就是在一個網頁中,再載入另外一個網頁。常見的範例是,很多網站都會使用頁框的方式,載入 facebook 粉絲團的按讚框。 在 HTML 中,頁框包含兩種標籤:`frame` 與 `iframe`。 ```htmlembedded= <iframe src="another.html"></iframe> ``` 由於頁框會載入另一個網頁,所以程式沒有辦法直接存取頁框裡的元素。我們必須切換到頁框內,才能夠取得頁框裡頭的元素,但切換之後僅能取得頁框內的元素,原本網頁的元素則會變得無法存取。關於操作頁框,可以使用: * `driver.switch_to.frame(another_frame)`:切換到 another_frame。another_frame 可以接受以下幾種形式: * 頁框在網頁中出現的 index (從 0 開始) * id Selector * name * WebElement * `driver.switch_to.parent_frame()`:切換到父頁框,若已是主網頁,則無效果 * `driver.switch_to.default_content()`:切換到主網頁 舉例來說,若今天有兩個網頁 a.html 與 b.html,內容分別是: ```htmlmixed= <!-- a.html --> <html> <head></head> <body> Hello, This is A. <iframe src="b.html"></iframe> </body> </html> ``` ```htmlmixed= <!-- b.html --> <html> <head></head> <body> Hello, This is B. </body> </html> ``` 此時,如有以下的程式: ```python= driver.get(URL_A) print(driver.page_source) # 印出 a.html 的內容 driver.switch_to.frame(0) # 切換到第一個頁框 print(driver.page_source) # 印出 b.html 的內容 driver.switch_to.default_content() # 回到 a.html print(driver.page_source) # 印出 a.html 的內容 ``` 我們就可以進行對頁框的操作。 > 若一個頁框內還有另一個頁框,則需再切換一次。 > 更詳細的教學文章:[Python selenium —— 深刻解析及操作frame、iframe](https://huilansame.github.io/huilansame.github.io/archivers/switch-to-frame) ### 延遲的問題 由於打開新的視窗和頁框都需要花一些時間才能夠完全載入,為了確保我們每次尋找元素、切換視窗、切換頁框都有確實地被正確執行,我們會需要適當的時機做延遲(delay)的動作。 Selenium 內建有 WebDriverWait 的函式,不過為了簡單起見,也可以直接使用 time.sleep 來暫停若干秒,等待載入完成。詳細可參考接下來的範例程式。 ## 實用函式 * get_screenshot_as_file(filename):螢幕截圖,存為 png 檔。 ## 範例程式:自動抓取 E3 的所有課程教材 這個程式可以幫助你在一個課程的檔案相當多時,直接用程式將所有檔案下載回來。要特別注意的是,E3 點擊 [ view ] 之後出現的 popup window 裡面都含有一個頁框(frame),所以必須要切換進頁框後,再對頁框內的元素進行操作。此外,還需要等待視窗與頁框被完全載入後,才能操作元素,以免發生問題。 ```python3= from selenium import webdriver from selenium.webdriver.common.keys import Keys import getpass import time driver = webdriver.Chrome() driver.get("https://dcpc.nctu.edu.tw/index.aspx") # 填寫 E3 帳號 username = driver.find_element_by_name('txtAccount') print('Student ID:',end='') username.send_keys(input()) # 填寫 E3 密碼 password = driver.find_element_by_name('txtPwd') password.send_keys(getpass.getpass()) # 按下送出 submit = driver.find_element_by_name('btnLoginIn') submit.click() # 選擇「當期課程」 driver.find_element_by_link_text("當期課程").click() # 選擇課程(資料庫系統概論) driver.find_element_by_link_text("資料庫系統概論").click() # 選擇「教材列表」 driver.find_element_by_link_text("教材列表").click() # 尋找 [ view ],稍後可用來打開一項教材的詳細列表 views = driver.find_elements_by_link_text("[ view ]") # 記住此 window,每次處理完一項教材都要再切換回來這裡 originview = driver.current_window_handle for item in views: # 點擊 [ view ],打開一項教材的詳細列表(新視窗) item.click() # 等待新視窗完全載入 time.sleep(0.5) # 切換到剛才打開的視窗,window_handles[-1] 就是最後一個。 driver.switch_to.window(driver.window_handles[-1]) # 記住剛才剛開的視窗,等等點完一個檔案會再回來此視窗 subwindow = driver.current_window_handle # 切換至 frame 中 driver.switch_to.frame(driver.find_element_by_tag_name("frame")) # 等待 frame 被完全載入。 time.sleep(0.5) # 尋找單個檔案的 [ view ] 們,稍後點擊之。 subviews = driver.find_elements_by_link_text("[ view ]") for subitem in subviews: # 點擊單個檔案的 [ view ] subitem.click() time.sleep(0.5) # 切換到剛才打開的視窗(點擊單個檔案的 [ view ] 出現的) driver.switch_to_window(driver.window_handles[-1]) # 切換至 frame 中 driver.switch_to.frame(driver.find_element_by_tag_name("frame")) time.sleep(0.5) # 點擊「下載檔案:xxxxx」連結,下載檔案。 driver.find_element_by_partial_link_text("下載檔案").click() # 切換視窗回檔案列表 driver.switch_to_window(subwindow) # 再切換進檔案列表的 frame 中 driver.switch_to.frame(driver.find_element_by_tag_name("frame")) time.sleep(0.5) # 切換進課程教材列表的視窗 driver.switch_to_window(originview) time.sleep(0.5) driver.close() ``` ## 參考資料 * [Selenium 教學文件與 API](http://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.common.keys)