Try   HackMD

[Scratch & Python] Selenium 快速入門

Selenium 簡介

簡單來說,Selenium 可以用程式碼來模擬一個人在瀏覽器(Chrome, Safari, Firefox, )中的動作。一般來說,Selenium 通常被作為自動化測試(Automated Testing)的工具,但由於他可以模擬在瀏覽器中的操作,所以也可以作為讓我們可以自動化某些操作的輔助工具。

在作為輔助工具上,Selenium 和 PyAutoGui 不同的地方為:在滑鼠方面,PyAutoGui 是使用「座標定位」的方式去進行常見的操作(click, doubleclick, ),而 Selenium 是使用選取「網頁元素(element)」的方式去操作。

這篇教學將以 Python 程式來自動化模擬網頁操作為主軸。

前備知識

HTML:用來描述網頁的內容

在使用 Selenium 之前,我們需要先了解「網頁元素(element)」到底是怎麼一回事。

標籤、元素、屬性

這是一段簡單的 html 代碼:

<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):用來標記一個元素的開始與結束,如pstronga
  • 屬性(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。如 pstrong 等,在 CSS 直接寫 tag name。

如果以上方的 HTML code 來說,假設我想要讓整個 <p class="description">...</p> 段落的字為藍色,我可以撰寫以下的 CSS:

.description { color: blue; } // 使用 class Selector

如果我們寫

p { color: blue } // 使用 element Selector

則所有使用 p 的元素的字都會是藍色。

此外,CSS Selector 還可以有階層的方式一層一層指定下去:

p.description a {
    color: red; // 把「交大首頁請點我」設為紅色
}

或是一次指定很多個東西:

strong, a {
    color: green; // 把所有粗體字和連結都設為綠色
}

還有很多詳細的用法,想了解的同學可以瀏覽:W3School CSS 教程W3School HTML 教程

怎麼找到別人網頁中特定元素的 CSS Selector?

在接下來的教學中,我們會需要寫一些自動化的程式來模擬網頁上的操作。首先,有了 CSS Selector 的概念後,我們若可以知道網頁上某些按鈕、連結、文字、照片等元素的 CSS Selector,就可以把這些 Selector 提供給程式,讓程式來定位網頁中的元素並對其進行操作。

在 Chrome 中,假設我們要知道交大首頁的「在校生」連結的元素的 Selector 為何,首先我們先在連結上點右鍵,並點選「檢查」。

然後在反白的那行上面,右鍵點選 Copy > Copy selector

我們可以拿到一串針對「在校生」這一個連結的 Selector:

#groupNav > div > ul:nth-child(1) > li.item-511 > a

之後我們可以把這個用在 Selenium 中的選擇網頁元素的部分。

開始使用 Selenium

Selenium 安裝說明

從範例開始

範例程式 A:打開交大首頁並點擊「在校生」

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 模組,稍後會用到。

from selenium import webdriver from time import sleep

再來,下面第一行可以打開 Chrome,而第二行 driver.get(網址) 則是告訴 webdriver 要連到哪一個網址去。

driver = webdriver.Chrome() driver.get("http://www.nctu.edu.tw/")

使用 driver.title 可以取得當前網頁標題,並使用 assert 來確定我們連到的網頁標題叫做 NCTU 國立交通大學

assert "NCTU 國立交通大學" in driver.title

driver.find_element_by_css_selector 可以用來定位特定 CSS Selector,在此我們使用前面拿到的「在校生」連結的 Selector,當作參數傳進函式。此時 student 即為「在校生」那一個連結元素。

我們可以寫 student.click() 來點擊該元素。

student = driver.find_element_by_css_selector("#groupNav > div > ul:nth-child(1) > li.item-511 > a") student.click()

最後,為了看到我們真的有連到「在校生」的那一頁,讓程式暫停五秒。最後再執行 driver.close() 來關閉 webdriver,程式結束。

sleep(5) driver.close()

範例程式 B

注:同 Getting Started

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 鍵)

from selenium import webdriver from selenium.webdriver.common.keys import Keys from time import sleep

改成進入 python.org

driver = webdriver.Chrome() driver.get("http://www.python.org") assert "Python" in driver.title

從 Chrome 檢查元素面板中,可以發現那一個搜尋框有著 name="q" 的屬性。Selenium 同時也支援用 name 屬性來尋找元素,所以我們可以寫
driver.find_element_by_name("q")

接著,elem.clear() 可以保證搜尋框會是乾淨的。接著我們使用 elem.send_keys("array"),代表在 elem 上按下鍵盤的按鍵 array。最後再按下一個 Return 鍵,用 Keys.RETURN 表示。

elem = driver.find_element_by_name("q") elem.clear() elem.send_keys("array") elem.send_keys(Keys.RETURN)

詳細的 Keys 用法可參考: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)給一個變數,如

elem = driver.find_element_by_name("q")

此時我們就可以對這個元素(如 elem)進行點擊與按下鍵盤按鍵:

elem.send_keys("array") elem.click()

如果要支援更多操作,如雙擊、拖拉、按住按鍵不放等,我們需要使用 Action Chains。

Action Chains

API 文件:Action Chains

在使用 Action Chains 前,可以載入 ActionChains class 讓程式更簡單一點:

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。

actions = ActionChains(driver) actions.key_down(Keys.CONTROL) actions.send_keys('c') actions.key_up(Keys.CONTROL) actions.perform()

也可以寫成這樣:

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。

範例程式:先打開交大網頁的網站導覽頁,再點擊校園公告頁,並切換到校園公告頁。

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 中,頁框包含兩種標籤:frameiframe

<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,內容分別是:

<!-- a.html --> <html> <head></head> <body> Hello, This is A. <iframe src="b.html"></iframe> </body> </html>
<!-- b.html --> <html> <head></head> <body> Hello, This is B. </body> </html>

此時,如有以下的程式:

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

延遲的問題

由於打開新的視窗和頁框都需要花一些時間才能夠完全載入,為了確保我們每次尋找元素、切換視窗、切換頁框都有確實地被正確執行,我們會需要適當的時機做延遲(delay)的動作。

Selenium 內建有 WebDriverWait 的函式,不過為了簡單起見,也可以直接使用 time.sleep 來暫停若干秒,等待載入完成。詳細可參考接下來的範例程式。

實用函式

  • get_screenshot_as_file(filename):螢幕截圖,存為 png 檔。

範例程式:自動抓取 E3 的所有課程教材

這個程式可以幫助你在一個課程的檔案相當多時,直接用程式將所有檔案下載回來。要特別注意的是,E3 點擊 [ view ] 之後出現的 popup window 裡面都含有一個頁框(frame),所以必須要切換進頁框後,再對頁框內的元素進行操作。此外,還需要等待視窗與頁框被完全載入後,才能操作元素,以免發生問題。

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()

參考資料