[Python網頁爬蟲]網站圖片擷取-2自動取網站圖片

Sean Yeh
Python Everywhere -from Beginner to Advanced
26 min readAug 16, 2022

--

Qingjing Farm, Taiwan, photo by Sean Yeh

上一篇我們透過兩個簡化過的範例程式碼,說明了網站擷取圖片方式。 為了讓程式在現實世界發揮功用,我們需要逐步改寫程式,透過selenium的使用讓程式可以自行打開瀏覽器,下載圖片,並且存在指定的路徑中。

準備爬蟲環境

在進行實際資料擷取的工作前,需要將爬蟲環境環境準備好。由於這次我們要透過selenium讓程式自行打開瀏覽器並下載圖片,因此需要安裝Selenium套件與下載Webdriver。Selenium套件的安裝與Chrome WebDriver的下載步驟可以參考這一篇

載入Selenium套件

當Selenium套件下載與安裝完畢後,就可以將它們載入到程式中。可以透過「 from selenium import webdriver」載入Webdriver。

from selenium import webdriver

載入driver

selenium中的 webdriver可以開啟各種瀏覽器的驅動程式,透過這個驅動程式來操作網頁瀏覽器。selenium會因為要操作的瀏覽器之不同,而需要下載對應於該瀏覽器之驅動程式。例如要操作Firefox瀏覽器,就需要下載Firefox之驅動程式。

本文中的範例會透過Google的Chrome瀏覽器來開啟網頁並進行資料爬取的程序。因此,需要下載相對應Google Chrome瀏覽器的驅動程式。下載與選用驅動程式的詳細說明同樣可以參考這一篇

驅動程式下載完畢後,會將它放在執行程式的電腦裡面( 其路徑大概是這樣:r'/Users/path_to/driver/chromedriver' )。並透過變數chromeDriver,我們將路徑指定給Google Chrome 瀏覽器驅動程式。

chromeDriver = r'/Users/path_to/driver/chromedriver'

路徑指定完畢後,就可以使用 webdriver.Chrome()建立一個 Google Chrome瀏覽器物件,並存在變數driver中。

# 啟動chrome瀏覽器
chromeDriver = r'/Users/path_to/driver/chromedriver'
# chromedriver檔案放的位置
driver = webdriver.Chrome(chromeDriver)

載入其他套件

在此除了匯入selenium套件之外,還會使用到下面幾個套件。

  • urllib套件:用來擷取圖片;
  • time套件:用來計算selenium暫停的時間;以及
  • os套件:用來對電腦的檔案系統進行操作與讀寫。
import urllib
import time
import os

目標網站

如同上一篇文章中的示範,在一篇裡面仍然會使用免費的圖片網站unsplash 來進行網路爬蟲的示範。因此我們將unsplash網站設定為本次爬蟲的目標網站。

進入目標頁面

觀察目標網站後,我們發現在這個網站中間的搜尋框(文字顯示:Search free high-resolution photos)裡面輸入文字並按下enter鍵搜尋。可以發現網頁的搜尋結果頁面會直接導向帶有關鍵字的網址。也就是以下面的格式表示:

https://官網/photos/搜尋關鍵字
目標網站的網址格式

因此,我們可以將搜尋關鍵字與前半部的網址拆開來,將搜尋關鍵字作為一個變數。日後若要截取不同類型的圖片,只要修改變數的值就可以了。

搜尋關鍵字拆開作為一個變數

若將此概念表現在程式上,可以做如下的撰寫:

# 搜尋關鍵字
keyword ='paris'
# 爬取頁面網址
url = f'<https://unsplash.com/s/photos/{keyword}>'

並除了將搜尋關鍵字以變數keyword拆出來外,並將網址頁面指定為 url 變數。

上面的方式,是直接將搜尋關鍵字寫在程式碼中。我們或者可以將關鍵字的地方設計成一個輸入框,可以在程式一開始執行的時候,詢問程式使用者,想要爬取的條件為何(如下程式碼)。如果沒有輸入任何條件時,就以預設的關鍵字作為搜尋條件(預設的關鍵字可以依照業務的需求來指定,筆者在此使用paris作為預設的關鍵字)。如此可以依照使用者每次不同的需要,爬取不同類型的圖片。

keyword = input("請輸入要搜尋的關鍵字 ")
if keyword == '':
print("由於您沒有輸入關鍵字,將以預設字paris作為搜尋條件")
keyword ='paris'

不論是使用hard code的方式寫在程式碼中或者是在執行程式時透過對話框輸入,當url被確定之後,就可以使用 webdriver.Chrome()物件的get方法進入目標網頁。由於在前面我們已經透過driver變數表示webdriver.Chrome()物件,在此使用 driver.get(url)啟動瀏覽器,開始爬取頁面。

driver.get(url)

放大頁面範圍

由於這程式在每一次爬取時,只會抓視窗内看到的圖片,因此我們會希望視窗展開的越大越好,最好佔滿整個螢幕的畫面。

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

driver.maximize_window()

分析網站

目標網站已經取得了,瀏覽器也已經開啟,接下來就可以爬取瀏覽器中出現的資料了。在取得畫面中的資料前,需要分析網站HTML各元素的架構,才能夠辨別出目標資料位於整個網頁的哪個位置。

照慣例,我們打開Google Chrome的DevTools來檢視HTML的結構,從中找出可以使用的HTML標籤(Tag)元素。

找尋目標區塊元素

以上畫面是程式以預設關鍵字 paris (紅色1的位置)搜尋而產生的結果頁面,來分析網站的HTML標籤。從DevTools上面逐步尋找框著目標內容區塊的最內層元素以及目標內容區塊。

結果我們得到緊貼著所有內容最外圈有個div標籤元素,其上含有屬性名稱為data-test而其值為search-photos-route(紅色2的位置),這個div元素在整個網頁中只存在一個。因此可以確定我們已經找到包覆著果肉的最內一層附著的皮。

使用XPATH找到目標元素

於DevTools最下方的位置,使用Cmd+F打開搜尋輸入方框,在輸入框裡面填寫XPATH規則,並找出正確的XPATH。如果填寫的XPATH規則正確,其對應於該規則的HTML標籤元素就會呈現黃色。

如同上圖紅色框3的位置,該<div>標籤元素以XPATH表示時會呈現如下:

//div[@data-test='search-photos-route']

找尋目標資料元素

以本次範例來說,目標資料即區塊中的圖片(如下圖紅色1的位置)。若從DevTools上觀察,可以看到表示圖片的標籤元為img(亦即紅色2的位置)。

紅色2位置的<img>標籤元素的路徑,若以XPATH表示時,會呈現如同紅色3的位置所表示的XPATH:

xpath = '//div[@class="mItv1"]//figure//div[@class="VQW0y Jl9NH"]/img'

此外,在畫面中紅字4的位置,您也可以看到透過這個XPATH路徑尋找到符合條件的項目總共有20項,也就是說總共有20筆資料(20張圖片),此數量剛好符合此頁面上圖片的數量。可以確定我們目前選定的XPATH沒有錯誤。

擷取圖片

到目前為止,已經定義好目標元素的XPATH位置。接著可以開始擷取資料了。

擷取資料時需要將目標資料元素取出,在這裡我們透過for迴圈以及find_elements_by_xpath將元素一個個取出。

值得一提的是,由於我們要取出的目標資料元素不只一個,因此在這裡find_elements的elements是複數,別忘了要加上s。這是在程式撰寫過程中常常會弄錯的一個地方,需要特別注意。

透過find_elements_by_xpath取出的img標籤元素中包含很多資訊,而我們實際上需要的只是img標籤中的src屬性值。

因此,除了透過for迴圈取出element之後,要再利用get_attribute()方法取出<img>標籤裡面的src屬性值( get_attribute('src') )。

xpath = '//div[@class="mItv1"]//figure//div[@class="VQW0y Jl9NH"]/img'for element in driver.find_elements_by_xpath(xpath):
img_url = element.get_attribute('src')
print('img_url: ',img_url)

此外,在for迴圈裡面的最後一行加上 print(‘img_url: ‘,img_url)讓我們可以透過 print() 將爬取的資料顯示在終端機畫面中,以便於觀察程式執行的結果。

資料存檔

此階段會將程式擷取到的圖片,一張張的存入資料夾中。我們還可以將這個階段,細分為下面幾個步驟說明:

  • 1. 存檔資料夾
  • 2. 檔案命名
  • 3. 保存圖片

1. 存檔資料夾

為了儲存圖片,我們需要在電腦中指定一個資料夾,提供爬蟲儲存從網站上擷取到的成果。

指定資料夾最簡單的方式就是直接給定一個資料夾目錄,並且該資料夾目錄位於專案同一階層的位置。

因此,我們直接在程式碼中給定一個資料夾目錄imgs,並且將它指定給變數local_path(如下程式碼)。另外,我們也需要親自在專案目錄中「手動」建立一個同樣名稱(imgs)的資料夾。以免後來執行程式時,找不到路徑而發生錯誤。

local_path = 'imgs'

當然,這個部分仍然可以再進行一些優化。例如,程式在指定的資料夾不存在時,可以自行建立該資料夾;或者是資料夾的名稱,依照目前執行爬蟲的日期時間來命名等等各種的優化方向,在此就不加以說明。

2. 檔案命名

完成了儲存資料的建立後,再來看看儲存圖片檔案的名稱問題。

由於我們希望可以對爬蟲擷取到的圖片重新修訂檔案的名稱,因此可以透過 split()方法將取得的圖片路徑進行拆解。

由於圖片路徑會以https://官網/photos/搜尋關鍵字/圖片表示,因此最簡單的方式是以斜線為拆解字串的條件( / )。

透過 .split('/')[-1]可以取出拆解後的最後一個字串。然後將此結果指定給變數ext。(這部分一樣可以再進行優化)

ext = img_url.split('/')[-1]

接著將要儲存的圖片檔名用變數filename表示。在變數filename中,將關鍵字以及前面拆解出來的兩個字串相結合,並且在結合後的字串最後面加上 .jpg 作為副檔名,其結果如下:

filename = f'{keyword}_{ext}.jpg'

程式碼中的{keyword}為搜尋的關鍵字,它來自於我們程式最初執行時透過input輸入的字串(如下面的程式碼)。使用這個關鍵字作為圖片名稱的一部分,可以讓我們輕鬆的辨識出該圖片歸屬的類型。

keyword = input("請輸入要搜尋的關鍵字 ")

最後,再加上print()將最後產生的檔案名稱呈現在螢幕上(這一行不加也可以)。

print('filename-->',filename)

3. 保存圖片

有了儲存的資料夾、儲存的檔名後,再來就要撰寫保存圖片的程式碼。我們在此使用urllib套件來下載並保存圖片。

或許您還記得,我們曾經在上一篇文章中使用urllib下載圖片。在此可以直接將該段程式碼移植到這個裏,並且加上些許的調整後使用(調整完畢後如下面的程式碼)。

# 保存圖片
res = urllib.request.urlopen(img_url)
file_path = open(os.path.join(local_path , filename),'wb')
size = 0
while True:
info = res.read(10000)
if len(info) < 1:
break
size = size + len(info)
file_path.write(info)
print(f'已下載{filename}:',size)
file_path.close()
res.close()

關閉網頁

當資料儲存的作業完畢之後,就可以使用quit方法把chrome瀏覽器關閉。

driver.quit()

到目前為止,我們的程式碼已經可以下載需要的圖片了。您可以在條件的提示下,輸入想要下載的圖片類型,而下面的程式碼就會透過selenium自動開啟一個瀏覽器,並且自動下載圖片到指定的資料夾裡面。以下是到目前為止的程式碼:

from selenium import webdriver
import urllib
import os
# 搜尋關鍵字'outdoor fashion'
keyword = input("請輸入要搜尋的關鍵字 ")
if keyword == '':
print("由於您沒有輸入關鍵字,將以預設字paris作為搜尋條件")
keyword ='paris'
# 啟動chrome瀏覽器
chromeDriver = r'/Users/seanyeh/Dev/ImgDownloader/driver/chromedriver'
# chromedriver檔案位置
driver = webdriver.Chrome(chromeDriver)

# 目標頁面網址
url = f'<https://unsplash.com/s/photos/{keyword}>'
# 打開瀏覽器,爬取頁面
driver.get(url)
# 視窗極大化
driver.maximize_window()
# 存圖位置
local_path = 'imgs'
# 目標元素的xpath
xpath = '//div[@class="mItv1"]//figure//div[@class="VQW0y Jl9NH"]/img'

for element in driver.find_elements_by_xpath(xpath):
img_url = element.get_attribute('src')
print('img_url: ',img_url)

ext = img_url.split('/')[-1]
filename = f'{keyword}_{ext}.jpg'
print('filename-->',filename)
# 保存圖片
res = urllib.request.urlopen(img_url)
file_path = open(os.path.join(local_path , filename),'wb')
size = 0
while True:
info = res.read(10000)
if len(info) < 1:
break
size = size + len(info)
file_path.write(info)
print(f'已下載{filename}:',size)
file_path.close()
res.close()

driver.quit()

優化程式

如果您對上面的程式碼滿意的話,可以就此收工。不過,實際上我們還可以對該程式碼進行一些修正與優化。

1. try … except錯誤處理

利用Python try…except的「例外處理」方式指出錯誤,當錯誤發生時,就會執行與它有關的程式,以避免因錯誤而導致整個流程當掉。

for element in driver.find_elements_by_xpath(xpath):
try:
img_url = element.get_attribute('src')
print('img_url: ',img_url)

ext = img_url.split('/')[-1]
filename = f'{keyword}_{ext}.jpg'
print('filename-->',filename)
# 保存圖片
res = urllib.request.urlopen(img_url)
file_path = open(os.path.join(local_path , filename),'wb')
size = 0
while True:
info = res.read(10000)
if len(info) < 1:
break
size = size + len(info)
file_path.write(info)
print(f'已下載{filename}:',size)
file_path.close()
res.close()
except:
print('發生錯誤')
break;

在此當except捕捉到任何型態的錯誤時,會直接透過break跳出迴圈,以避免整個程式當掉。

其他關於 try … except錯誤處理的說明可以參考下面這一篇。

2.建立類別子資料夾

若執行目前的程式碼,程式執行後爬蟲擷取的檔案都會儲存在imgs資料夾裡面,其結果會像下面的樣子。

由於輸入的關鍵字是happy,所以下圖中每個圖檔的檔名都以 happy_ 作為開頭。隨然檔案在經過排序後,同樣名稱開頭的檔案會排在一起,可以讓我們比較容易尋找與閱覽。但大家是否發現一個問題,如果imgs資料夾中已經有很多檔案的話,若單純就這樣透過排序的方式來瀏覽圖檔,還是會讓人感到有點麻煩。除非您下載的這些檔案還會經過另外的程式處理(例如做成資料庫,並透過網頁來查詢等等)。

基於這個理由,我們可以透過優化來解決這個痛點。讓程式碼依照關鍵字來建立空白的子資料夾。也就是我們在程式一開始輸入的關鍵字,當時輸入的字串是什麼,就以該字串建立一個資料夾。如此就可以讓圖片依照不同的關鍵字來分門別類的放好。

為要達到這樣的功能,我們需要一個新變數來儲存子資料夾的位置。在此就命名為store_path吧。

子資料夾乃使用os.path裡面的 join() 函式來實現。 join() 函式可以組合路徑與查詢的關鍵字,成為一個資料夾路徑。

store_path = os.path.join(local_path, keyword)

由於我們並不知道程式的使用者每次在使用時會輸入什麼樣的關鍵字,也無法像前面建立imgs資料夾一樣的透過「手動」方式預先建立子資料夾。於是我們希望程式依照情況可以自行建立子資料夾。

我們希望程式在建立子資料夾前,先檢查該資料夾是否已經存在,如果尚未建立過該名稱的資料夾,就可以自動新增一個。因此先透過os.path中的 exists() 來檢查該路徑是否已經存在,並把結果指定給isExist變數。

isExist = os.path.exists(store_path)

isExist為布林,換句話說只有兩個值( TrueFalse )。若isExist的結果為 Falseif not isExist: ),就需要依照該關鍵字建立一個子資料夾。而建立子資料夾在這裡使用 os.makedirs()函式來實現。

因此建立資料夾的這一段的程式碼可以寫成下面:

#建立資料夾
store_path = os.path.join(local_path,keyword)
isExist = os.path.exists(store_path)
if not isExist:
os.makedirs(store_path)
print(f"建立資料夾{store_path}")

在建立子資料夾後,使用 print(),將結果顯示在螢幕上。

完成後,將上面這一段程式碼插入整段程式的try區段裡面,#保存圖片 程式碼註解的上方。(如下說明)

for element in driver.find_elements_by_xpath(xpath):
try:
...略...
#建立資料夾
store_path = os.path.join(local_path,keyword)
isExist = os.path.exists(store_path)
if not isExist:
os.makedirs(store_path)
print(f"建立資料夾{store_path}")

# 保存圖片
...略...
except:
...略...

如果存檔後再次執行程式的話,會得到像下面的結果。由於這次搜尋的關鍵字是excited,因此您會看到在imgs裡面建立了一個excited的資料夾,且所有以excited_開頭的圖片也都自動歸類在此資料夾裡面(如下圖)。

3.滾動型頁面處理

接下來的優化方向是針對滾動行頁面爬取資料時的問題。

您是否發現有時候目標網站的頁面就像瀑布一樣的(例如Facebook、Twitter等社群媒體網站都是採用這種設計),越是向下捲動,呈現的資料就越多?

這時候,可以透過selenium來模擬滾動滑鼠的效果,讓頁面不斷的向下捲動,並且透過這樣的捲動方式讓原本沒有出現的資料逐步地被載入畫面中。

#起始點0

要實現滾動畫面的功能,首先需要一個滾動的起始點 — 數字0。對於這個起始點0,我們以變數position來表示。

position = 0

# 逐步增加數值

有了起始點數字0之後,需要設計一個for迴圈讓它每次可以增加一定的數字。我們在這裡先將該數值設定為200,換句話說,當迴圈loop一次,就會替變數position加上200的數值。

以上這個兩個步驟的作用,是要模擬滾動滑鼠的動作。每輪迴一次for迴圈,就會像滑鼠一般的將頁面滾動200px單位。

而滾動滑鼠的功能,還需要借助JavaScript來實現。在JavaScript語法中,可以利用window.scrollTo讓滑鼠滾動,如下程式碼滑鼠將滾動200px。

window.scrollTo(0, 200);

您需要依照不同的目標頁面,來微調整滑鼠滾動的距離。

若以目前的目標網站來說,筆者測試各種數值後發現,使用200px作為每次滾動的距離,最能確保圖片慢慢的呈現在網頁上。若一次設定的滾動幅度太大的話,例如設定程式每次滾動距離為500px,恐怕下載的效果不好。

於是,我們把這一段程式碼寫成下面的結構:

position = 0 for 迴圈
position += i*200

此外,要讓程式在每次迴圈巡迴時,執行JavaScript程式,需要透過driver中的 .execute_script()來執行行JavaScript程式。

driver.execute_script(js)

最後,我們還需要一點留白。也就是在每一次for回圈執行完畢後,暫停個數秒鐘,等待頁面上的圖片全部都載入完畢了,在進行下一輪的爬取。使用time套件的sleep函式,可以讓程式暫停幾秒鐘。我們在這裡設定sleep()為10,也就是每滾動一次滑鼠,就暫停10秒鐘。

附帶說明,程式暫停所需的時間,因會每個人電腦效能的優劣而異。大家可以依照個人使用的系統效能來調整時間長度。如果是效能很好的電腦,可以縮短這個等待的值。效能比較不好的話,就需要長一點的時間才有機會將檔案正確下載到頁面上。

time.sleep(10)

總結面的幾個概念,結合起來寫成程式碼,其表現如下:

# 模擬滾動視窗瀏覽更多圖片
scroll_times = input("請輸入頁面滾動次數")
position = 0
for i in range(scroll_times):
position += i*200 # 每次下滾200
js = "window.scrollTo(0, {});".format(position)
#執行script
driver.execute_script(js)
time.sleep(10)
print(f'下滾第{i}輪')

4.避免重複下載圖檔

這是上面「滾動型頁面處理」優化後產生的新問題。採用「透過滾動視窗捲軸,來改變畫面的內容而下載檔案」的方法有可能發生同樣圖片被重複下載的問題。為了避免發生這樣的情況,就需要再寫一個簡單程式來排除這個問題。解決方法是,建立一個容器來搜集下載過的圖檔位置,程式只要檢查到該路徑已經被收錄到容器中,就不會再被下載一次。

這個解決方法,需要一個容器以及一段判斷式。在判斷建立之前,需要先建立一個字典來做為容器,用以記錄下載過的圖片位址。我們就稱它為img_url_dic:

img_url_dic = {}

有了容器後,就可以寫一個判斷式,當網址不存在容器裡面時,就可以下載檔案並且要將下載過的圖片位址紀錄在字典裡面。這裡把網址作為key( img_url_dic[img_url]),而且只需要把網址收錄進來,不需要指定value。

# 排除重複下載檔案
if img_url != None and not img_url in img_url_dic:
img_url_dic[img_url] = ''

所有程式碼

以下是優化後的程式碼,大家若有更好的優化方向,也懇請提出建議,讓程式碼可以更加完善。

from selenium import webdriver
import urllib
import os
import time
# 搜尋關鍵字'outdoor fashion'
keyword = input("請輸入要搜尋的關鍵字 ")
if keyword == '':
print("由於您沒有輸入關鍵字,將以預設字paris作為搜尋條件")
keyword ='paris'
scroll_times = int(input("請輸入頁面滾動次數"))# 啟動chrome瀏覽器 請自行修改路徑
chromeDriver = r'/Users/path_to/driver/chromedriver'
# chromedriver檔案位置
driver = webdriver.Chrome(chromeDriver)

# 目標頁面網址
url = f'<https://unsplash.com/s/photos/{keyword}>'
# 打開瀏覽器,爬取頁面
driver.get(url)
# 視窗極大化
driver.maximize_window()
# 存圖位置
local_path = 'imgs'
# 紀錄下載過的圖片網址,避免重複下載
img_url_dic = {}
# 目標元素的xpath
xpath = '//div[@class="mItv1"]//figure//div[@class="VQW0y Jl9NH"]/img'
# 模擬滾動視窗瀏覽更多圖片position = 0
m = 0 # 圖片編號
for i in range(scroll_times):
position += i*200 # 每次下滾200px
js = "window.scrollTo(0, {});".format(position)
#執行script
driver.execute_script(js)
time.sleep(10)
print(f'下滾第{i+1}輪: js = {js} ')

for element in driver.find_elements_by_xpath(xpath):
try:
img_url = element.get_attribute('src')
print('img_url: ',img_url)

# 排除重複下載檔案
if img_url != None and not img_url in img_url_dic:
img_url_dic[img_url] = ''

ext = img_url.split('/')[-1]
filename = f'{keyword}_{ext}.jpg'
print('filename-->',filename)

#建立資料夾
store_path = os.path.join(local_path,keyword)
isExist = os.path.exists(store_path)
if not isExist:
os.makedirs(store_path)
print(f"建立資料夾{store_path}")

# 保存圖片
res = urllib.request.urlopen(img_url)
file_path = open(os.path.join(store_path , filename),'wb')
size = 0
while True:
info = res.read(10000)
if len(info) < 1:
break
size = size + len(info)
file_path.write(info)
print(f'已下載{filename}:',size)
file_path.close()
res.close()
except:
print('發生錯誤')
break;

driver.quit()

--

--

Sean Yeh
Python Everywhere -from Beginner to Advanced

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