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