【Python 網路爬蟲筆記】Selenium Library、爬取 Hackmd 文章專題 - part 4

感謝你點進本篇文章!!我是 LukeTseng,一個熱愛資訊的無名創作者,由於近期大學開設大數據分析程式設計這門課程,裡面談到了爬蟲概念,讓我激起一些興趣,因而製作本系列筆記。

聲明:本篇筆記僅供個人學習用途,斟酌參考。

本篇筆記使用 Jupyter Notebook,搭載 Anaconda 虛擬環境,如需下載者可至該網址:https://www.anaconda.com/download

安裝 Selenium 模組

透過以下指令:

1
pip install selenium

在 Jupyter Notebook(安裝完後記得 Restart Kernel 才會啟用):

1
!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 版本?如下圖所示。

image

image

不過也可以不用下載,因為有另一個模組就的指令是可以在程式執行時自動下載,會在下一節介紹。

自動安裝 WebDriver 的方法(懶人法)

首先先安裝 webdriver-manager:

1
pip install webdriver-manager

之後在程式中即可使用:

1
2
3
4
5
6
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 自動化程式開啟瀏覽器,並前往指定網站。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 則是要定位元素的方法,具體如下範例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 即可取得。

image

日後要定位元素也建議使用 XPATH 的方式。

常用操作方法

在定位網頁元素後,接下來要做的就是類似像點擊、捲動畫面等操作了,如:

  • .send_keys() 輸入文字
  • .submit() 提交表單(模擬按下 Enter)
  • .click() 按下滑鼠左鍵

詳細方法可至 Selenium 官方網站:https://www.selenium.dev/documentation/webdriver/elements/interactions/

然後可到該網站進行一些 Selenium 的測試與學習:https://example.oxxostudio.tw/python/selenium/demo.html

以下是一個小範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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取得元素的 idelement.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 教育學習網

以下是對於測試網站所做的範例(改自 Selenium 函式庫 - Python 網路爬蟲教學 | STEAM 教育學習網):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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()

輸出結果如下:

image

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

image

專題:爬取 Hackmd 作者頁面的所有文章資訊

以下透過我個人 Hackmd 頁面進行爬取,首先爬取第一頁的每篇文章標題。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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()

輸出結果:

image

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

image

從下圖中打開【檢查】,可以分析一下每個頁面的結構長怎樣,這邊實測發現每個頁面按鈕都是一樣的元素所組成,另外我也特別觀察了一下每個元素的 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 去變動。

image

以下是程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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()

輸出結果:

image

其中以下程式碼是 Selenium 的顯式等待(Explicit Wait)機制,能自動等待頁面 loading 完成,像是以下就是等待每個標題元素加載完成才繼續執行程式碼。

而其中 WebDriverWait 的第二個參數 10 就是最多等待 10 秒的意思。

until 為持續檢查某個條件,直到條件成立或超時。

EC.presence_of_all_elements_located(...) 是等待的條件,意思是等待「所有符合條件的元素都出現在網頁的 DOM 結構中」。

1
2
3
WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, 'span.line-clamp-1.flex-1.text-lg.font-semibold'))
)

爬取完標題後,可以來爬取每篇文章的日期、瀏覽量,甚至可以算出所有文章的總瀏覽量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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()

輸出結果:

image

參考資料

Selenium 函式庫 - Python 網路爬蟲教學 | STEAM 教育學習網

瀏覽器自動化工具Selenium介紹 – CH.Tseng

[DAY7]Selenium簡介 | iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天

認識 Selenium · GitBook

Day 15: Selenium - 基本概念和操作 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天

python一招完美搞定Chromedriver的自动更新 - NewJune - 博客园

Python Selenium 教學筆記 - HackMD

動態網頁爬蟲第一道鎖 - Selenium教學:如何使用Webdriver、send_keys(附Python 程式碼) - 臺灣行銷研究