一、前言
前文介紹了PhatomJS 和Selenium 的用法,工具准備完畢,我們來看看如何使用它們來改造我們之前寫的小爬蟲。
我們的目的是模擬頁面下拉到底部,然后頁面會刷出新的內容,每次會加載10張新圖片。
大體思路是,用Selenium + PhatomJS 來請求網頁,頁面加載后模擬下拉操作,可以根據想要獲取的圖片多少來選擇下拉的次數,然后再獲取網頁中的全部內容。
二、運行環境
我的運行環境如下:
-
系統版本
Windows10。 -
Python版本
Python3.5,推薦使用Anaconda 這個科學計算版本,主要是因為它自帶一個包管理工具,可以解決有些包安裝錯誤的問題。去Anaconda官網,選擇Python3.5版本,然后下載安裝。 -
IDE
我使用的是PyCharm,是專門為Python開發的IDE。這是JetBrians的產品,點我下載。
三、爬蟲實戰改造
3.1 模擬下拉操作
要想實現網頁的下拉操作,需要使用Selenium的一個方法來執行js代碼。該方法如下:
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
由此可見,使用execute_script
方法可以調用JavaScript API在一個加載完成的頁面中去執行js代碼。可以做任何你想做的操作哦,只要用js寫出來就可以了。
改造的爬蟲的第一步就是封裝一個下拉方法,這個方法要能控制下拉的次數,下拉后要有等待頁面加載的時間,以及做一些信息記錄(在看下面的代碼前自己先想一想啦):
def scroll_down(self, driver, times):
for i in range(times):
print("開始執行第", str(i + 1),"次下拉操作")
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") #執行JavaScript實現網頁下拉倒底部
print("第", str(i + 1), "次下拉操作執行完畢")
print("第", str(i + 1), "次等待網頁加載......")
time.sleep(20) # 等待20秒(時間可以根據自己的網速而定),頁面加載出來再執行下拉操作
這部分做完之后就是修改頁面爬取邏輯,之前是使用request 請求網址,然后找到圖片url所在位置,再然后挨個請求圖片url。現在,我們要首先使用Selenium 請求網址,然后模擬下拉操作,等待頁面加載完畢再遵循原有邏輯挨個請求圖片的url。
邏輯部分改造如下:
def get_pic(self):
print('開始網頁get請求')
# 使用selenium通過PhantomJS來進行網絡請求
driver = webdriver.PhantomJS()
driver.get(self.web_url)
self.scroll_down(driver=driver, times=5) #執行網頁下拉到底部操作,執行5次
print('開始獲取所有a標簽')
all_a = BeautifulSoup(driver.page_source, 'lxml').find_all('a', class_='cV68d') #獲取網頁中的class為cV68d的所有a標簽
print('開始創建文件夾')
self.mkdir(self.folder_path) #創建文件夾
print('開始切換文件夾')
os.chdir(self.folder_path) #切換路徑至上面創建的文件夾
print("a標簽的數量是:", len(all_a)) #這里添加一個查詢圖片標簽的數量,來檢查我們下拉操作是否有誤
for a in all_a: #循環每個標簽,獲取標簽中圖片的url並且進行網絡請求,最后保存圖片
img_str = a['style'] #a標簽中完整的style字符串
print('a標簽的style內容是:', img_str)
first_pos = img_str.index('"') + 1 #獲取第一個雙引號的位置,然后加1就是url的起始位置
second_pos = img_str.index('"', first_pos) #獲取第二個雙引號的位置
img_url = img_str[first_pos: second_pos] #使用Python的切片功能截取雙引號之間的內容
#注:為了盡快看到下拉加載的效果,截取高度和寬度部分暫時注釋掉,因為圖片較大,請求時間較長。
##獲取高度和寬度的字符在字符串中的位置
#width_pos = img_url.index('&w=')
#height_pos = img_url.index('&q=')
#width_height_str = img_url[width_pos : height_pos] #使用切片功能截取高度和寬度參數,后面用來將該參數替換掉
#print('高度和寬度數據字符串是:', width_height_str)
#img_url_final = img_url.replace(width_height_str, '') #把高度和寬度的字符串替換成空字符
#print('截取后的圖片的url是:', img_url_final)
#截取url中參數前面、網址后面的字符串為圖片名
name_start_pos = img_url.index('photo')
name_end_pos = img_url.index('?')
img_name = img_url[name_start_pos : name_end_pos]
self.save_img(img_url, img_name) #調用save_img方法來保存圖片
邏輯修改完畢。執行一下,發現報錯了,圖片的url截取錯誤。那就看看到底是為什么,先輸出找到url看看:
看看輸出內容,發現通過PhatomJS 請求網頁獲得的url(https://blablabla) 中,竟然沒有雙引號,這跟使用Chrome 看到的不一樣。好吧,那就按照沒有雙引號的字符串重新截取圖片的url。
其實,我們只需要把url的起始位置和結束邏輯改成通過小括號來獲取就可以了:
first_pos = img_str.index('(') + 1 #起始位置是小括號的左邊
second_pos = img_str.index(')') #結束位置是小括號的右邊
好啦,這次獲取圖片的url就沒什么問題了,程序可以順利的執行。
但是,細心的小伙伴兒肯定發現了,我們這個爬蟲在爬取的過程中中斷了,或者過一段時間網站圖片更新了,我再想爬,有好多圖片都是重復的,卻又重新爬取一遍,浪費時間。那么我們有什么辦法來達到去重的效果呢?
3.2 去重
想要做去重,無非就是在爬取圖片的時候記錄圖片名字(圖片名字是唯一的)。在爬取圖片前,先檢查該圖片是否已經爬取過,如果沒有,則繼續爬取,如果已經爬取過,則不進行爬取。
去重的邏輯如上,實現方式的不同主要體現在記錄已經爬取過的信息的途徑不同。比如可以使用數據庫記錄、使用log文件記錄,或者直接檢查已經爬取過的數據信息。
根據爬取內容的不同實現方式也不同。我們這里先使用去文件夾下獲取所有文件名,然后在爬取的時候做對比。
后面的爬蟲實戰再使用其他方式來做去重。
單從爬取unsplash 網站圖片這個實例來看,需要知道文件夾中所有文件的名字就夠了。
Python os 模塊中有兩種能夠獲取文件夾下所有文件名的方法,分別來個示例:
for root, dirs, files in os.walk(path):
for file in files:
print(file)
for file in os.listdir(path):
print(file)
其中,os.walk(path) 是獲取path文件夾下所有文件名和所有其子目錄中的文件夾名和文件名。
而os.listdir(path) 只是獲取path文件夾下的所有文件的名字,並不care其子文件夾。
這里我們使用os.listdir(path) 就能滿足需求。
寫一個獲取文件夾內所有文件名的方法:
def get_files(self, path):
pic_names = os.listdir(path)
return pic_names
因為在保存圖片之前就要用到文件名的對比,所以把save_img方法修改了一下,在方法外部拼接圖片名字,然后直接作為參數傳入,而不是在圖片存儲的過程中命名:
def save_img(self, url, file_name): ##保存圖片
print('開始請求圖片地址,過程會有點長...')
img = self.request(url)
print('開始保存圖片')
f = open(file_name, 'ab')
f.write(img.content)
print(file_name,'圖片保存成功!')
f.close()
為了更清晰去重邏輯,我們每次啟動爬蟲的時候檢測一下圖片存放的文件夾是否存在,如果存在則檢測與文件夾中的文件做對比,如果不存在則不需要做對比(因為是新建的嘛,文件夾里面肯定什么文件都沒有)。
邏輯修改如下,是新建的則返回True,不是新建的則返回False:
def mkdir(self, path): ##這個函數創建文件夾
path = path.strip()
isExists = os.path.exists(path)
if not isExists:
print('創建名字叫做', path, '的文件夾')
os.makedirs(path)
print('創建成功!')
return True
else:
print(path, '文件夾已經存在了,不再創建')
return False
然后修改我們的爬取邏輯部分:
主要是添加獲取的文件夾中的文件名列表,然后判斷圖片是否存在。
is_new_folder = self.mkdir(self.folder_path) #創建文件夾,並判斷是否是新創建
file_names = self.get_files(self.folder_path) #獲取文件家中的所有文件名,類型是list
if is_new_folder:
self.save_img(img_url, img_name) # 調用save_img方法來保存圖片
else:
if img_name not in file_names:
self.save_img(img_url, img_name) # 調用save_img方法來保存圖片
else:
print("該圖片已經存在:", img_name, ",不再重新下載。")
好了,來個完整版代碼(也可以去 GitHub下載):
from selenium import webdriver #導入Selenium
import requests
from bs4 import BeautifulSoup #導入BeautifulSoup 模塊
import os #導入os模塊
import time
class BeautifulPicture():
def __init__(self): #類的初始化操作
self.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1'} #給請求指定一個請求頭來模擬chrome瀏覽器
self.web_url = 'https://unsplash.com' #要訪問的網頁地址
self.folder_path = 'C:\D\BeautifulPicture' #設置圖片要存放的文件目錄
def get_pic(self):
print('開始網頁get請求')
# 使用selenium通過PhantomJS來進行網絡請求
driver = webdriver.PhantomJS()
driver.get(self.web_url)
self.scroll_down(driver=driver, times=3) #執行網頁下拉到底部操作,執行3次
print('開始獲取所有a標簽')
all_a = BeautifulSoup(driver.page_source, 'lxml').find_all('a', class_='cV68d') #獲取網頁中的class為cV68d的所有a標簽
print('開始創建文件夾')
is_new_folder = self.mkdir(self.folder_path) #創建文件夾,並判斷是否是新創建
print('開始切換文件夾')
os.chdir(self.folder_path) #切換路徑至上面創建的文件夾
print("a標簽的數量是:", len(all_a)) #這里添加一個查詢圖片標簽的數量,來檢查我們下拉操作是否有誤
file_names = self.get_files(self.folder_path) #獲取文件家中的所有文件名,類型是list
for a in all_a: #循環每個標簽,獲取標簽中圖片的url並且進行網絡請求,最后保存圖片
img_str = a['style'] #a標簽中完整的style字符串
print('a標簽的style內容是:', img_str)
first_pos = img_str.index('(') + 1
second_pos = img_str.index(')')
img_url = img_str[first_pos: second_pos] #使用Python的切片功能截取雙引號之間的內容
# 注:為了盡快看到下拉加載的效果,截取高度和寬度部分暫時注釋掉,因為圖片較大,請求時間較長。
#獲取高度和寬度的字符在字符串中的位置
# width_pos = img_url.index('&w=')
# height_pos = img_url.index('&q=')
# width_height_str = img_url[width_pos : height_pos] #使用切片功能截取高度和寬度參數,后面用來將該參數替換掉
# print('高度和寬度數據字符串是:', width_height_str)
# img_url_final = img_url.replace(width_height_str, '') #把高度和寬度的字符串替換成空字符
# print('截取后的圖片的url是:', img_url_final)
#截取url中參數前面、網址后面的字符串為圖片名
name_start_pos = img_url.index('.com/') + 5 #通過找.com/的位置,來確定它之后的字符位置
name_end_pos = img_url.index('?')
img_name = img_url[name_start_pos : name_end_pos] + '.jpg'
img_name = img_name.replace('/','') #把圖片名字中的斜杠都去掉
if is_new_folder:
self.save_img(img_url, img_name) # 調用save_img方法來保存圖片
else:
if img_name not in file_names:
self.save_img(img_url, img_name) # 調用save_img方法來保存圖片
else:
print("該圖片已經存在:", img_name, ",不再重新下載。")
def save_img(self, url, file_name): ##保存圖片
print('開始請求圖片地址,過程會有點長...')
img = self.request(url)
print('開始保存圖片')
f = open(file_name, 'ab')
f.write(img.content)
print(file_name,'圖片保存成功!')
f.close()
def request(self, url): #返回網頁的response
r = requests.get(url) # 像目標url地址發送get請求,返回一個response對象。有沒有headers參數都可以。
return r
def mkdir(self, path): ##這個函數創建文件夾
path = path.strip()
isExists = os.path.exists(path)
if not isExists:
print('創建名字叫做', path, '的文件夾')
os.makedirs(path)
print('創建成功!')
return True
else:
print(path, '文件夾已經存在了,不再創建')
return False
def scroll_down(self, driver, times):
for i in range(times):
print("開始執行第", str(i + 1),"次下拉操作")
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") #執行JavaScript實現網頁下拉倒底部
print("第", str(i + 1), "次下拉操作執行完畢")
print("第", str(i + 1), "次等待網頁加載......")
time.sleep(30) # 等待30秒,頁面加載出來再執行下拉操作
def get_files(self, path):
pic_names = os.listdir(path)
return pic_names
beauty = BeautifulPicture() #創建類的實例
beauty.get_pic() #執行類中的方法
注釋寫的很詳細,有任何問題可以留言。
四、后語
該實戰就先到這里,需要注意的是Unsplash 這個網站經常不穩定,小伙伴有遇到請求異常的情況可以多試幾次。
后面我開始寫一個查找The Beatles 樂隊的歷年專輯封面圖片和專輯名稱的爬蟲。我的設計師小伙伴兒想要收集Beatles 樂隊歷年的專輯封面圖片,然后做一個該樂隊的設計海報。
至於爬蟲框架的使用,后面我會再專門介紹,最好找到一個好的使用框架的實戰例子,這樣才能更好的理解框架的作用與優點。
OK, See you then.