[Python網頁爬蟲]如何使用Selenium爬取網頁資料-2

Sean Yeh
Python Everywhere -from Beginner to Advanced
25 min readMay 10, 2022
Praha, Czech, photo by Sean Yeh

網頁自動化要做的事,就是模擬滑鼠與鍵盤操作這些元素的方式,包括滑鼠的點擊事件、螢幕上的移動行為或鍵盤中輸入文字等等。然而就像希臘哲人阿基米德所說的「 Give me a place to stand on, and I will move the Earth.(給我一個支點,我就能移動地球。」一樣,要達到「自動化」的效果有個前提,就是要給程式一個「支點」。就網頁來說這個支點就是HTML裡面的標籤元素,例如像H1、H2、div、span、article等,這些標籤元素都可以被當作「支點」,提供程式進行各種的行為操作。因此,操作網站的前提就是我們需要「找到」這些可以作為支點的HTML標籤元素。

標籤的特性

在尋找支點之前,必須先瞭解HTML標籤元素。如果您本身會製作網站的話,對於這個部分應該不陌生。大體上來說HTML標籤有下面的特型:

除了少數例外,原則上HTML標籤都成雙成對:

例如 <div></div> 這樣子是一對 ,而 <article></article> 也是一對。至於 <img><br> 則是例外。

標籤上可以帶有各種屬性:

標籤可以賦予各種不同的屬性,就像人一樣,一個人可以帶有身分證字號、姓名、職業、性別等等屬性。如下面例子:

<form class="myform"> </form><input name='firstname' class='control-form'><div id="navigator"></div>

成對的標籤間可以帶有文字資料:

在HTML標籤間可以放入字串作為文字資料。如下面例子:

<a>關於我們</a>  <p>這是一個文章的段落,沒有什麼內容</p>

標籤可以巢狀層級呈現:

原則上在一組成對的HTML標籤裡面,可以再放入另一組成對的HTML標籤,並且可以一直持續下去。如下面例子:

<table>
<tr>
<td></td> <td></td> <td></td>
</tr>
<tr>
<td></td> <td></td> <td></td>
</tr>
</table>

在外(上)層的為父標籤,內(下)層的為子標籤。

藉由以上這些特性,就可以找到用來作為支點的HTML標籤元素。而Selenium裡面的WebDriver API提供我們用來找到這些元素的方法。

關於WebDriver API

WebDriver API屬於Selenium中用來操作瀏覽器的一套API。透過這個API不僅可以操作頁面上的各種元素,也提供了一些操作瀏覽器的方法,諸如可以控制瀏覽器的大小、操作頁面的前進與後退等。這個API支援多個語言,包括Python在內。上一篇文章中已經使用WebDriver API獲取網頁資料。

WebDriver API提供了不少元素的定位方式,在Python之下有下面幾個定位方式:

由這個表可以得知,我們可以透過id、name、tag name定位或者是css的class name定位。方法如下:

ID定位

假設有下面的HTML標籤(第1行),我們可以使用 find_element_by_id(id名稱)的方式(第2行)來定位。

<div id="q">...</div>find_element_by_id('q')

name定位

假設有下面的HTML標籤(第1行),我們可以使用 find_element_by_name(name的值)的方式(第2行)來定位。

<input name="username">find_element_by_name('username')

tag name定位

假設有下面的HTML標籤(第1行),我們可以使用 find_element_by_tag_name(HTML標籤的名稱)的方式(第2行)來定位。


<input value="your name">
find_element_by_tag_name('input')

class name 定位

假設有下面的HTML標籤(第1行),我們可以使用 find_element_by_class_name(class的值)的方式(第2行)來定位。

<button class="submit_btn">Submit</button>find_element_by_class_name('submit_btn')

link text定位

這個定位方式與前面不同,它適用於以超連結文字作為定位。例如我們有下面的HTML超連結<a>標籤(第1行),可以使用 find_element_by_link_text(超連結文字)的方式(第2行)來定位:

<a href="/about">關於我們</a>find_element_by_link_text('關於我們')

partial link 定位

有時候若想要作為定位的超連結文字太長,則可以使用partial link 來定位。這種方式可以擷取文字連結的一部分來定位。例如我們有下面的HTML超連結<a>標籤(第1行),可以使用 find_element_by_partial_link_text(部分超連結文字)的方式(第2行)來定位。因為「如何養成一個健康活潑的小朋友的15種方式」太長,於是我們只取用前面四個字「如何養成」來定位。

# <a href="/about-20221023204345">如何養成一個健康活潑的小朋友的15種方式</a>find_element_by_partial_link_text('如何養成')

XPath定位

XPath (XML Path Language) 是一種用來尋找XML文件中某個節點(node)位置的查詢語言。其中共有七種節點:element, attribute, text, namespace, processing-instruction, comment, document。由於HTML可被視為XML的表現方式之一,所以Selenium也可以使用這種方式來定位。可以透過 find_element_by_xpath() 方法來實現定位。

XPath可以利用元素屬性定位。例如:

find_element_by_xpath('//article/')

其中, // 表示目前頁面在某一個目錄下面。也就是說在某個目錄下,有個article標籤。

又如下面的例子:

//article/div[contains(@class,"article-contents")]

其中, div[contains()] 表示div元素中包含某些屬性。在這個例子裡,contains括號中是 contains(@class,"article-contents"),表示我們要尋找的是具備class名稱為article-contents的div元素。

css selector定位

CSS是一種用來描述HTML表現方式的語言。使用過的人都知道,它可以透過選擇器的使用方式,來為頁面綁定屬性。而這些選擇器也可以被seleium用來進行定位。

因此,在使用這種css selector定位方式時,就像是在使用CSS選擇器一樣,可以透過class選擇器、id選擇器、萬用字元選擇器、後代選擇器E F、子元素選擇器等CSS選擇器的方式來定位。

# 後代選擇器
find_element_by_css_selector("ul li")
# 子元素選擇器
find_element_by_css_selector("span > input

更多關於CSS選擇器的說明可以參考下面的文章:

Selenium爬取多頁面

上一篇我們說明了單一頁面的爬取方式,雖然這個方式對於不曾使用自動化方式擷取網頁資料而透過複製貼上的人力處理來說,已經快很多。但是當已經學會使用爬蟲程式擷取網頁資料的你我來說,這樣子的自動化程度,還是會覺得不夠。於是我們要在這一篇說明如何站在既有的單頁爬取資料的濟楚上,再進一步的自動化爬取多個頁面。

目標頁面

在這次的示範中,我們會延續上一篇「[Python網頁爬蟲]如何使用Selenium爬取網頁資料-1」的例子,因此仍然對下面的目標頁面進行資料的爬取。目標頁面為一日文網站,裡面搜羅各種動漫訊息。

不同於上一篇僅針對「單一頁面」進行特定資料爬取,在這裡我們的目標是要透過程式自動將這個單元的各個分頁,一次爬取並請保存在一個CSV檔案裡面。

進行流程

到目前為止,我們都是針對單一頁面爬取資料。如果想要的資料分布在多個頁面的話,該怎麼辦?

這裡就是要處理資料分佈在多個頁面的狀況。其中大部分的步驟在上一篇文章中有較詳細的說明,有興趣可以參考。

  • 載入相關套件
  • 載入driver
  • 進入目標頁面
  • 放大頁面範圍
  • 找尋目標區塊元素
  • 找尋目標資料元素
  • 找尋換頁區塊元素
  • 爬取目標資料
  • 關閉網頁
  • 資料存檔

載入相關套件

一樣需要使用到selenium中的webdriver,以進行網頁自動化。

from selenium import webdriver

與載入pandas套件,已進行儲存資料相關程序。

import pandas as pd

以及控制下載時間的套件。

import time

載入driver

連結先前下載的驅動程式。在此一樣透過變數path指定Google Chrome瀏覽器驅動程式的路徑。

path = '/Users/yourMac/yourFolders/yourProject/driver/chromedriver'
driver = webdriver.Chrome(path)

接著使用 webdriver.Chrome() 建立一個Google Chrome瀏覽器物件,並存在變數driver中。

進入目標頁面

使用 webdriver.Chrome()物件的get方法進入目標網頁。由於我們在上一個步驟中已經用透過driver變數表示該物件了,因此這裡可以使用 driver.get(web)

web =  'https:// nlab.itmedia.co.jp/research/category/entertainment/anime/'
driver.get(web)

放大頁面範圍

使用driver中的 maximize_window()方法,可以將瀏覽器的視窗設定為螢幕的最大寬度,讓瀏覽器全螢幕顯示。

driver.maximize_window()

找尋目標區塊元素

打開Google Chrome的DevTools來檢視HTML的結構,對HTML原始檔案進行判讀,從中找到可以使用的HTML標籤(Tag)元素。

找到匡住目標內容區塊的最內層元素以及目標內容區塊。得到CSS class名稱為mainBoxList的div元素標籤,以及CSS class名稱為article-index的article元素標籤。

透過driver中的 find_element_by_class_name 方法來取得特定CSS class名稱的元素,並分別存入參數containerarticles中。

container = driver.find_element_by_class_name('mainBoxList')
articles = container.find_elements_by_xpath('./article')

找尋目標資料元素

目標資料包括標題(title)、時間(time)、作者(author)以及文章縮圖(thumb)與文章連結(link)。

標題(title)以XPATH表示時會呈現如下:

//article/div[contains(@class,"article-contents")]/div[contains(@class,"article-title")]/h3/a

其他如時間(time)、作者(author)、文章縮圖(thumb)與文章連結(link)等以XPATH呈現如下:

//article/div[contains(@class,"article-contents")]/div[contains(@class,"article-meta")]/time
# 時間(time)

//article/div[contains(@class,"article-contents")]/div[contains(@class,"article-meta")]/div[contains(@class,"article-author")]/a
# 作者(author)
//article/a[contains(@class,"article-thumb")]/div[contains(@class,"myBoxBG")]/img/
# 文章縮圖(thumb)
//article/div[contains(@class,"article-contents")]/div[contains(@class,"myBoxBtn")]/a
#文章連結(link)

找尋換頁相關元素

由於這次我們要爬取多個頁面,需要尋找網頁上面Pagination換頁按鈕。在此一樣要透過Google DevTools找到可以定位的xpath。

Pagination換頁按鈕區

尋找後發現class名稱為pagingElements的<ul>標籤,可以用來定位。於是透過xpath來表示該標籤的位置 //div[@class="wp-pagenavi"]。 並且使用find_element_by_xpath來取得。

pagination = driver.find_element_by_xpath('//div[@class="wp-pagenavi"]')

換頁按鈕

另外,我們需要取得每個頁碼的按鈕。經過同樣方式尋找後,發現頁碼的按鈕在class名稱為wp-pagenavi的div標籤之下,以a標籤的形式展現。因此可以使用下面方式取得:從pagination以下尋找tag名稱為li的標籤。

pages = pagination.find_elements_by_tag_name('a')

取得總頁碼

還需要取得總共有多少頁需要下載。要取得這個值我們需要「最後一頁」按鈕的數字。

這個數字可以由倒數第3個 <a>標籤( pages[-2].text )來取出最後一個按鈕的數字。以[-3]的方式取出倒數第3個 <a>標籤,再透過text取得標籤裡面的字串後,再使用int()將取得的字串轉換為整數型態。

last_page = int(pages[-3].text)

尋找下一頁按鈕

知道頁碼之後,我們還要找到「下一頁」的按鈕。屆時可以透過selenium點選的功能( click() ),一頁頁的點下去。我們將按鈕指定為變數next_page。

next_page = driver.find_element_by_xpath('//a[contains(@class,"nextpostslink")]')next_page.click()

爬取目標資料

所有需要的定位按鈕都已經準備妥當之後,就可以實際的來設計資料爬取的程式。

在截取單一頁面時候,首先要建立空白串列,然後透過for迴圈將目標資料元素中的文字逐一的取出,再透過append的方法,把資料添加到先前建立的空白串列中。但是在截取多頁面時候,我們可以將擷取多頁面的步驟拆分成多次擷取單一頁面的程序,單一頁面與單一頁面間則透過換頁的程序來串連之。為了實現多頁面的擷取,還需要一個迴圈來幫助我們進行反覆的程序。

while迴圈

設計一個while迴圈來擷取多個頁面,當條件符合的時候就繼續擷取,直到停止條件滿足為止。

為了實現這個概念,需要先建立一個變數來代表目前的條件,在此我們使用current_page作為這個變數。並且指定current_page的起始值為1( current_page =1 ),作為迴圈的起始值也是目前的值。另外,先前曾取得的「最後一頁」頁碼( last_page )的值則為迴圈的最大值。

使用wile迴圈的語法來表示,當目前的值小於最大值的狀況下,就可以繼續爬取資料,直到始值等於最大值之後結束( while current_page <= last_page: )。並且每進行一次迴圈,就將目前的值(current_page)加一。

current_page =1while current_page <= last_page:

# 爬取資料....
current_page = current_page + 1

while迴圈內

在while迴圈之內,要做的事就是先爬取單一頁面資料,資料爬取完畢後,按下「下一頁」,再繼續爬取另一個單一頁面的資料。依序執行,直到while迴圈的條件滿足為止。因此,我們可以將之前爬取單一資料的程式放入while迴圈中,並且把前面找到的「下一頁」按鈕( next_page )放入,並且加上selenium的點擊行為( next_page.click() )。程式碼如下:

current_page =1article_title = []
article_time = []
article_author = []
article_detail_link = []
article_image_thumb = []
while current_page <= last_page:

container = driver.find_element_by_class_name('mainBoxList')
articles = container.find_elements_by_xpath('./article')

for product in products:
title = article.find_element_by_xpath( './div[contains(@class,"article-contents")]/div[contains(@class,"article-title")]/h3/a').text

art_time = article.find_element_by_xpath( '//div[contains(@class,"article-contents")]/div[contains(@class,"article-meta")]/time').get_attribute("datetime")

author = article.find_element_by_xpath( './div[contains(@class,"article-contents")]/div[contains(@class,"article-meta")]/div[contains(@class,"article-author")]/a').text
thumb = article.find_element_by_xpath( './a[contains(@class,"article-thumb")]/div[contains(@class,"myBoxBG")]/img' ).get_attribute("src") link = article.find_element_by_xpath( './div[contains(@class,"article-contents")]/div[contains(@class,"myBoxBtn")]/a' ).get_attribute("href")current_page = current_page + 1next_page = driver.find_element_by_xpath( '//a[contains(@class,"nextpostslink")]' )
next_page.click()

優化

上面的程式碼,基本上已經可以擷取多頁面資料,不過仍然可以稍微優化一下。如果下一頁按鈕失效,該如何?這種狀況可能會導致整個程式當掉。這時可以加上try…except 來要避免這種情況發生。如下:

try:
next_page = driver.find_element_by_xpath( '//a[contains(@class,"nextpostslink")]' )
next_page.click()
except:
print("已無頁可爬")

為了確保資料都下載下來才開始爬取,我們增加暫停的時間,讓selenium等待頁面下載完畢之後,才開始進行資料爬取。

time.sleep(2)

關閉網頁

資料讀取之後,使用quit方法關閉hrome瀏覽器。

driver.quit()

資料存檔

最後,把擷取到的資料存入CSV檔案中。存擋的步驟如下:

data_dict = {'article_title':article_title,'article_time':article_time,'article_author':article_author,'article_image_thumb':article_image_thumb,'article_detail_link':article_detail_link}

df_anime = pd.DataFrame(data_dict)
df_anime.to_csv('itmedia_anime_paginations.csv', index=False)

完整程式碼

以下為完整之程式碼。在完整版程式碼裡面,做了一些修正:

  • 將輸出的檔案資料夾位置與檔名抽象出來,讓每次都可以依照需要變更。
  • 將Xpath的路徑位置抽出來,讓程式碼看起來比較不會太複雜。
  • 加上各階段的提示訊息。

藉由這些修正,可以讓程式碼變得比較好用一點。大家也可以試著把它拿來修改,調整成對自己有用的程式。

from selenium import webdriver
import pandas as pd
import time,os
web = 'https://nlab.itmedia.co.jp/research/category/entertainment/anime/'

path = '/Users/yourMac/yourFolders/yourProject/driver/chromedriver'
output_file = 'itmedia_animate_paginations.csv'
output_folder ='output'
output_url = f'{output_folder}/{output_file}'
driver = webdriver.Chrome(path,options=options)
driver.get(web)driver.maximize_window()#Pagination
pagination = driver.find_element_by_xpath('//div[@class="wp-pagenavi"]')
pages = pagination.find_elements_by_tag_name('a')
last_page = int(pages[-2].text)
current_page =1article_title = []
article_time = []
article_author = []
article_detail_link = []
article_image_thumb = []
print(f"開始爬取資料~總頁數{last_page}頁")
while current_page <= last_page:
print(f"開始爬取第{current_page}頁")
time.sleep(5)
container = driver.find_element_by_class_name('mainBoxList')
articles = container.find_elements_by_xpath('./article')

path_for_title = './div[contains(@class,"article-contents")]/div[contains(@class,"article-title")]/h3/a'
path_for_art_time ='//div[contains(@class,"article-contents")]/div[contains(@class,"article-meta")]/time'
path_for_author ='./div[contains(@class,"article-contents")]//div[contains(@class,"article-author")]/a'
path_for_thumb ='./a[contains(@class,"article-thumb")]/div[contains(@class,"myBoxBG")]/img'
path_for_link = './div[contains(@class,"article-contents")]/div[contains(@class,"myBoxBtn")]/a'
for article in articles:
title = article.find_element_by_xpath(path_for_title).text
art_time = article.find_element_by_xpath(path_for_art_time).get_attribute("datetime")
author = article.find_element_by_xpath(path_for_author).text
thumb = article.find_element_by_xpath(path_for_thumb).get_attribute("src")
link = article.find_element_by_xpath(path_for_link).get_attribute("href")
article_title.append(title)
article_time.append(art_time)
article_author.append(author)
article_image_thumb.append(thumb)
article_detail_link.append(link)
current_page = current_page + 1
try:
next_page = driver.find_element_by_xpath('//a[contains(@class,"nextpostslink")]')
next_page.click()
except:
print("已無頁可爬")
print("結束爬取資料~")
driver.quit()
print(f"開始資料存擋~檔名{output_file}")
data_dict = {'article_title':article_title,'article_time':article_time,'article_author':article_author,'article_image_thumb':article_image_thumb,'article_detail_link':article_detail_link}

df_anime = pd.DataFrame(data_dict)
if not os.path.exists(output_folder):
print(f"{output_folder} 資料夾不存在~建立資料夾")
os.mkdir(output_folder)
df_anime.to_csv(output_url, index=False)print(f"資料存擋完畢~{output_url}")

--

--

Sean Yeh
Python Everywhere -from Beginner to Advanced

# Taipei, Internet Digital Advertising,透過寫作讓我們回想過去、理解現在並思考未來。並樂於分享,這才是最大贏家。