【Python 網路爬蟲筆記】Selenium Library、爬取 Hackmd 文章專題 - part 4
===
目錄(Table of Contents):
[TOC]
---
感謝你點進本篇文章!!我是 LukeTseng,一個熱愛資訊的無名創作者,由於近期大學開設大數據分析程式設計這門課程,裡面談到了爬蟲概念,讓我激起一些興趣,因而製作本系列筆記。
聲明:本篇筆記僅供個人學習用途,斟酌參考。
本篇筆記使用 Jupyter Notebook,搭載 Anaconda 虛擬環境,如需下載者可至該網址:https://www.anaconda.com/download
## 安裝 Selenium 模組
透過以下指令:
```
pip install selenium
```
在 Jupyter Notebook(安裝完後記得 Restart Kernel 才會啟用):
```
!pip install selenium
```
## 什麼是 Selenium?
Selenium 是一種開源的網頁瀏覽器自動化工具,可以透過程式碼來模擬 user 在瀏覽器上的各種操作(像人一樣),從而完成自動化測試或網頁爬蟲任務。
Selenium 就是動態爬蟲中應用到最重要的技術。
Selenium 用 WebDriver 來驅動與控制瀏覽器,每種瀏覽器都有專屬的 WebDriver,Chrome 就是用 ChromeDriver、Firefox 用 GeckoDriver、Safari 用 SafariDriver 等等。
總之 WebDriver 是在 Selenium 裡面中最常使用到的技術,因為它可模擬很多 user 的操作像點擊元素、填寫表單、捲動頁面等等。
本篇筆記使用 Chrome 瀏覽器進行操作與編寫程式,要下載 ChromeDriver 可至該網站:https://sites.google.com/chromium.org/driver/downloads
記得要下載目前正使用對應的 Chrome 版本,具體如何看自己的 Chrome 版本?如下圖所示。


不過也可以不用下載,因為有另一個模組就的指令是可以在程式執行時自動下載,會在下一節介紹。
## 自動安裝 WebDriver 的方法(懶人法)
首先先安裝 webdriver-manager:
```
pip install webdriver-manager
```
之後在程式中即可使用:
```python=
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
# 自動下載並安裝對應版本的 ChromeDriver
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
```
## 第一支 selenium 程式
以下是一個小範例,會透過 selenium 自動化程式開啟瀏覽器,並前往指定網站。
```python=
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
# 建立 WebDriver 物件(自動下載對應版本的 ChromeDriver)
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
# 開啟 Google 首頁
driver.get('https://www.google.com')
# 暫停 5 秒
import time
time.sleep(5)
# 關閉瀏覽器
driver.quit()
```
在當建立 WebDriver 物件的時候,瀏覽器就會開起來了。
當程式執行後,會出現 Chrome 目前受到自動測試軟體控制的字樣,這是正常的,表示程式執行成功。
由於有設定暫停 5 秒的緣故,所以 5 秒後瀏覽器就會關閉了。
## 元素定位操作
可以使用 `.find_element()` 方法定位網頁元素,內部的第一個參數主要語法是 `By.XXX`,而 XXX 則是要定位元素的方法,具體如下範例所示:
```python=
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
driver.get('https://www.google.com')
# 1. 透過 ID 定位(最快最準)
search_box = driver.find_element(By.ID, "APjFqb")
# 2. 透過 NAME 屬性定位
search_box = driver.find_element(By.NAME, "q")
# 3. 透過 CLASS_NAME 定位
element = driver.find_element(By.CLASS_NAME, "gLFyf")
# 4. 透過 CSS Selector 定位(靈活度高)
search_box = driver.find_element(By.CSS_SELECTOR, "textarea[name='q']")
# 5. 透過 XPath 定位(功能最強)
search_box = driver.find_element(By.XPATH, "//textarea[@name='q']")
```
XPATH 取得的方式很簡單,只要對某個元素按下右鍵 -> Copy -> Copy XPATH 即可取得。

日後要定位元素也建議使用 XPATH 的方式。
## 常用操作方法
在定位網頁元素後,接下來要做的就是類似像點擊、捲動畫面等操作了,如:
- `.send_keys()` 輸入文字
- `.submit()` 提交表單(模擬按下 Enter)
- `.click()` 按下滑鼠左鍵
詳細方法可至 Selenium 官方網站:https://www.selenium.dev/documentation/webdriver/elements/interactions/
然後可到該網站進行一些 Selenium 的測試與學習:https://example.oxxostudio.tw/python/selenium/demo.html
以下是一個小範例:
```python=
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
driver.get('https://example.oxxostudio.tw/python/selenium/demo.html')
time.sleep(5) # 等待 javascript 跑完
text_box = driver.find_element(By.XPATH, '//*[@id="show"]')
text_box.send_keys("Hello World!")
B = driver.find_element(By.XPATH, '/html/body/button[2]')
B.click()
add_number = driver.find_element(By.XPATH, '//*[@id="add"]')
add_number.click()
time.sleep(10)
driver.quit()
```
## 取得網頁元素內容
| 方法 | 說明 | 範例 |
| :-- | :-- | :-- |
| `.text` | 取得元素的純文字內容 | `element.text` |
| `.get_attribute('屬性名')` | 取得元素的指定 HTML 屬性值 | `element.get_attribute('href')` |
| `.id` | 取得元素的 id | `element.id` |
| `.tag_name` | 取得元素的標籤名稱 | `element.tag_name` |
| `.size` | 取得元素的尺寸(長寬) | `element.size` |
| `.is_displayed()` | 判斷元素是否顯示在頁面上 | `element.is_displayed()` |
| `.is_enabled()` | 判斷元素是否可用 | `element.is_enabled()` |
| `.is_selected()` | 判斷元素是否被選取(如 checkbox) | `element.is_selected()` |
| `.parent` | 取得元素的父元素 | `element.parent` |
| `.screenshot('檔名.png')` | 將元素截圖並儲存為圖片 | `element.screenshot('test.png')` |
From [Selenium 函式庫 - Python 網路爬蟲教學 | STEAM 教育學習網](https://steam.oxxostudio.tw/category/python/spider/selenium.html)
以下是對於測試網站所做的範例(改自 [Selenium 函式庫 - Python 網路爬蟲教學 | STEAM 教育學習網](https://steam.oxxostudio.tw/category/python/spider/selenium.html)):
```python=
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
# 啟動瀏覽器
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
driver.get('https://example.oxxostudio.tw/python/selenium/demo.html')
# 取得 id 為 a 的元素
a = driver.find_element(By.ID, 'a')
# 取得 class 為 btn 的元素
b = driver.find_element(By.CLASS_NAME, 'btn')
# 取得 class 為 test 的元素
c = driver.find_element(By.CSS_SELECTOR, '.test')
# 取得 name 為 dog 的元素
d = driver.find_element(By.NAME, 'dog')
# 取得 tag 為 h1 的元素
h1 = driver.find_element(By.TAG_NAME, 'h1')
# 取得指定超連結文字的元素
link1 = driver.find_element(By.LINK_TEXT, '我是超連結,點擊會開啟 Google 網站')
# 取得超連結文字包含 Google 的元素
link2 = driver.find_element(By.PARTIAL_LINK_TEXT, 'Google')
# 取得元素內容
print("a.id:", a.id) # 元素 id
print("b.text:", b.text) # 元素文字
print("c.tag_name:", c.tag_name) # 元素標籤名稱
print("d.size:", d.size) # 元素尺寸
print("link1.href:", link1.get_attribute('href')) # 元素屬性值
print("link2.target:", link2.get_attribute('target')) # 元素屬性值
print("h1.is_displayed:", h1.is_displayed()) # 是否顯示
print("d.is_enabled:", d.is_enabled()) # 是否可用
print("a.is_selected:", a.is_selected()) # 是否被選取
# 將 body 元素截圖
body = driver.find_element(By.TAG_NAME, 'body')
body.screenshot('./test.png')
driver.quit()
```
輸出結果如下:

在外面額外產生了 test.png 的截圖。

## 專題:爬取 Hackmd 作者頁面的所有文章資訊
以下透過我個人 Hackmd 頁面進行爬取,首先爬取第一頁的每篇文章標題。
```python=
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import time
# 開啟瀏覽器
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
driver.get('https://hackmd.io/@LukeTseng')
# 等待 javascript 跑完
time.sleep(3)
# find_elements() 加上 s 可回傳一個列表,一次將所有相同的元素傳入列表中
titles = driver.find_elements(By.CSS_SELECTOR, 'span.line-clamp-1.flex-1.text-lg.font-semibold')
for title in titles:
print(title.text)
driver.quit()
```
輸出結果:

如果想要爬取每一頁的內容資訊怎麼辦?可以觀察到 HackMD 沒有下一頁的功能,因此需要自己實作。

從下圖中打開【檢查】,可以分析一下每個頁面的結構長怎樣,這邊實測發現每個頁面按鈕都是一樣的元素所組成,另外我也特別觀察了一下每個元素的 XPATH,他最後面都有特定的規律,如:
- `//*[@id="hackmd-app"]/section/main/div/div[2]/div/div/div[2]/div/ul/li[1]/a`
- `//*[@id="hackmd-app"]/section/main/div/div[2]/div/div/div[2]/div/ul/li[2]/a`
- `//*[@id="hackmd-app"]/section/main/div/div[2]/div/div/div[2]/div/ul/li[3]/a`
上面這些都是頁面 1、2、3 的元素,其他部分都沒有更動,唯一更動的地方只有最後面的 `/li[1]`,他裡面的中括號的數字會動,所以可以考慮使用 string format 去變動。

以下是程式碼:
```python=
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import time
# 開啟瀏覽器
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
driver.get('https://hackmd.io/@LukeTseng')
page_num = 1
# 爬取第 1 頁到第 14 頁
for page_num in range(1, 15):
print(f"正在爬取第 {page_num} 頁...")
# 等待標題元素載入
WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, 'span.line-clamp-1.flex-1.text-lg.font-semibold'))
)
# 爬取當前頁面的所有標題
titles = driver.find_elements(By.CSS_SELECTOR, 'span.line-clamp-1.flex-1.text-lg.font-semibold')
for title in titles:
print(f" - {title.text}")
# 如果不是最後一頁,點擊下一頁
if page_num < 14:
try:
# 找到下一頁的按鈕(頁碼 = 當前頁+1)
next_page_button = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, f'//*[@id="hackmd-app"]/section/main/div/div[2]/div/div/div[2]/div/ul/li[{page_num + 1}]/a'))
)
next_page_button.click()
time.sleep(2) # 等待頁面載入
except Exception as e:
print(f"無法點擊第 {page_num + 1} 頁: {e}")
break
print("所有頁面都爬完了!")
driver.quit()
```
輸出結果:

其中以下程式碼是 Selenium 的顯式等待(Explicit Wait)機制,能自動等待頁面 loading 完成,像是以下就是等待每個標題元素加載完成才繼續執行程式碼。
而其中 `WebDriverWait` 的第二個參數 10 就是最多等待 10 秒的意思。
`until` 為持續檢查某個條件,直到條件成立或超時。
`EC.presence_of_all_elements_located(...)` 是等待的條件,意思是等待「所有符合條件的元素都出現在網頁的 DOM 結構中」。
```python=
WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, 'span.line-clamp-1.flex-1.text-lg.font-semibold'))
)
```
爬取完標題後,可以來爬取每篇文章的日期、瀏覽量,甚至可以算出所有文章的總瀏覽量:
```python=
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import time
# 開啟瀏覽器
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
driver.get('https://hackmd.io/@LukeTseng')
page_num = 1
all_views = 0
# 爬取第 1 頁到第 14 頁
for page_num in range(1, 15):
print(f"正在爬取第 {page_num} 頁...")
# 等待標題元素載入
WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, 'span.line-clamp-1.flex-1.text-lg.font-semibold'))
)
# 等待日期元素載入
WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located(
(By.CSS_SELECTOR, 'div.text-text-subtle > span:first-child')
)
)
# 等待瀏覽量元素載入
WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located(
(By.XPATH, "//a[i[contains(@class, 'ph-eye')]]")
# //a 尋找所有 <a>
# contains() 函數檢查 class 屬性是否包含指定的文字
# @class 取得 class 屬性值
# 'ph-eye' 要搜尋的子字串
# i[contains(@class, 'ph-eye')]
# 當中的 i 表示 <a> 標籤必須包含一個 <i> 子元素,且 <i> 的 class 屬性要有 'ph-eye' 這個字串
)
)
# 爬取當前頁面的所有標題
titles = driver.find_elements(By.CSS_SELECTOR, 'span.line-clamp-1.flex-1.text-lg.font-semibold')
# 爬取當前頁面的所有日期
dates = driver.find_elements(By.CSS_SELECTOR, 'div.text-text-subtle > span:first-child')
# 爬取當前頁面的所有瀏覽量
views = driver.find_elements(By.XPATH, "//a[i[contains(@class, 'ph-eye')]]")
for title, date, view in zip(titles, dates, views):
print(f" - {title.text}\n 日期:{date.text}\n 瀏覽量:{view.text}\n")
all_views += int(view.text.replace(',', '')) # 將 , 替換成空字串以來轉成整數
# 如果不是最後一頁,點擊下一頁
if page_num < 14:
try:
# 找到下一頁的按鈕(頁碼 = 當前頁+1)
next_page_button = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, f'//*[@id="hackmd-app"]/section/main/div/div[2]/div/div/div[2]/div/ul/li[{page_num + 1}]/a'))
)
next_page_button.click()
time.sleep(2) # 等待頁面載入
except Exception as e:
print(f"無法點擊第 {page_num + 1} 頁: {e}")
break
print(f"所有文章總瀏覽量:{all_views}")
print("所有頁面都爬完了!")
driver.quit()
```
輸出結果:

## 參考資料
[Selenium 函式庫 - Python 網路爬蟲教學 | STEAM 教育學習網](https://steam.oxxostudio.tw/category/python/spider/selenium.html)
[瀏覽器自動化工具Selenium介紹 – CH.Tseng](https://chtseng.wordpress.com/2023/07/14/%E7%80%8F%E8%A6%BD%E5%99%A8%E8%87%AA%E5%8B%95%E5%8C%96%E5%B7%A5%E5%85%B7selenium%E4%BB%8B%E7%B4%B9/)
[[DAY7]Selenium簡介 | iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天](https://ithelp.ithome.com.tw/m/articles/10319363)
[認識 Selenium · GitBook](https://alincode.github.io/learngeb/intro/selenium.html)
[Day 15: Selenium - 基本概念和操作 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天](https://ithelp.ithome.com.tw/articles/10320979)
[python一招完美搞定Chromedriver的自动更新 - NewJune - 博客园](https://www.cnblogs.com/new-june/p/16698204.html)
[Python Selenium 教學筆記 - HackMD](https://hackmd.io/@FortesHuang/S1V6jrvet)
[動態網頁爬蟲第一道鎖 - Selenium教學:如何使用Webdriver、send_keys(附Python 程式碼) - 臺灣行銷研究](https://tmrmds.co/uncategorized/16513/)