一、前言
前文介紹了PhatomJS 和Selenium 的用法,工具準(zhǔn)備完畢,我們來看看如何使用它們來改造我們之前寫的小爬蟲。
我們的目的是模擬頁面下拉到底部,然后頁面會刷出新的內(nèi)容,每次會加載10張新圖片。
大體思路是,用Selenium + PhatomJS 來請求網(wǎng)頁,頁面加載后模擬下拉操作,可以根據(jù)想要獲取的圖片多少來選擇下拉的次數(shù),然后再獲取網(wǎng)頁中的全部內(nèi)容。
二、運(yùn)行環(huán)境
我的運(yùn)行環(huán)境如下:
系統(tǒng)版本
Windows10。Python版本
Python3.5,推薦使用Anaconda 這個科學(xué)計(jì)算版本,主要是因?yàn)樗詭б粋€包管理工具,可以解決有些包安裝錯誤的問題。去Anaconda官網(wǎng),選擇Python3.5版本,然后下載安裝。IDE
我使用的是PyCharm,是專門為Python開發(fā)的IDE。這是JetBrians的產(chǎn)品,點(diǎn)我下載。
三、爬蟲實(shí)戰(zhàn)改造
3.1 模擬下拉操作
要想實(shí)現(xiàn)網(wǎng)頁的下拉操作,需要使用Selenium的一個方法來執(zhí)行js代碼。該方法如下:
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
由此可見,使用execute_script方法可以調(diào)用JavaScript API在一個加載完成的頁面中去執(zhí)行js代碼??梢宰鋈魏文阆胱龅牟僮髋叮灰胘s寫出來就可以了。
改造的爬蟲的第一步就是封裝一個下拉方法,這個方法要能控制下拉的次數(shù),下拉后要有等待頁面加載的時間,以及做一些信息記錄(在看下面的代碼前自己先想一想啦):
def scroll_down(self, driver, times):
for i in range(times):
print("開始執(zhí)行第", str(i + 1),"次下拉操作")
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") #執(zhí)行JavaScript實(shí)現(xiàn)網(wǎng)頁下拉倒底部
print("第", str(i + 1), "次下拉操作執(zhí)行完畢")
print("第", str(i + 1), "次等待網(wǎng)頁加載......")
time.sleep(20) # 等待20秒(時間可以根據(jù)自己的網(wǎng)速而定),頁面加載出來再執(zhí)行下拉操作
這部分做完之后就是修改頁面爬取邏輯,之前是使用request 請求網(wǎng)址,然后找到圖片url所在位置,再然后挨個請求圖片url。現(xiàn)在,我們要首先使用Selenium 請求網(wǎng)址,然后模擬下拉操作,等待頁面加載完畢再遵循原有邏輯挨個請求圖片的url。
邏輯部分改造如下:
def get_pic(self):
print('開始網(wǎng)頁get請求')
# 使用selenium通過PhantomJS來進(jìn)行網(wǎng)絡(luò)請求
driver = webdriver.PhantomJS()
driver.get(self.web_url)
self.scroll_down(driver=driver, times=5) #執(zhí)行網(wǎng)頁下拉到底部操作,執(zhí)行5次
print('開始獲取所有a標(biāo)簽')
all_a = BeautifulSoup(driver.page_source, 'lxml').find_all('a', class_='cV68d') #獲取網(wǎng)頁中的class為cV68d的所有a標(biāo)簽
print('開始創(chuàng)建文件夾')
self.mkdir(self.folder_path) #創(chuàng)建文件夾
print('開始切換文件夾')
os.chdir(self.folder_path) #切換路徑至上面創(chuàng)建的文件夾
print("a標(biāo)簽的數(shù)量是:", len(all_a)) #這里添加一個查詢圖片標(biāo)簽的數(shù)量,來檢查我們下拉操作是否有誤
for a in all_a: #循環(huán)每個標(biāo)簽,獲取標(biāo)簽中圖片的url并且進(jìn)行網(wǎng)絡(luò)請求,最后保存圖片
img_str = a['style'] #a標(biāo)簽中完整的style字符串
print('a標(biāo)簽的style內(nèi)容是:', 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的切片功能截取雙引號之間的內(nèi)容
#注:為了盡快看到下拉加載的效果,截取高度和寬度部分暫時注釋掉,因?yàn)閳D片較大,請求時間較長。
##獲取高度和寬度的字符在字符串中的位置
#width_pos = img_url.index('&w=')
#height_pos = img_url.index('&q=')
#width_height_str = img_url[width_pos : height_pos] #使用切片功能截取高度和寬度參數(shù),后面用來將該參數(shù)替換掉
#print('高度和寬度數(shù)據(jù)字符串是:', width_height_str)
#img_url_final = img_url.replace(width_height_str, '') #把高度和寬度的字符串替換成空字符
#print('截取后的圖片的url是:', img_url_final)
#截取url中參數(shù)前面、網(wǎng)址后面的字符串為圖片名
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) #調(diào)用save_img方法來保存圖片
邏輯修改完畢。執(zhí)行一下,發(fā)現(xiàn)報錯了,圖片的url截取錯誤。那就看看到底是為什么,先輸出找到url看看:

看看輸出內(nèi)容,發(fā)現(xiàn)通過PhatomJS 請求網(wǎng)頁獲得的url(https://blablabla) 中,竟然沒有雙引號,這跟使用Chrome 看到的不一樣。好吧,那就按照沒有雙引號的字符串重新截取圖片的url。
其實(shí),我們只需要把url的起始位置和結(jié)束邏輯改成通過小括號來獲取就可以了:
first_pos = img_str.index('(') + 1 #起始位置是小括號的左邊
second_pos = img_str.index(')') #結(jié)束位置是小括號的右邊
好啦,這次獲取圖片的url就沒什么問題了,程序可以順利的執(zhí)行。
但是,細(xì)心的小伙伴兒肯定發(fā)現(xiàn)了,我們這個爬蟲在爬取的過程中中斷了,或者過一段時間網(wǎng)站圖片更新了,我再想爬,有好多圖片都是重復(fù)的,卻又重新爬取一遍,浪費(fèi)時間。那么我們有什么辦法來達(dá)到去重的效果呢?
3.2 去重
想要做去重,無非就是在爬取圖片的時候記錄圖片名字(圖片名字是唯一的)。在爬取圖片前,先檢查該圖片是否已經(jīng)爬取過,如果沒有,則繼續(xù)爬取,如果已經(jīng)爬取過,則不進(jìn)行爬取。
去重的邏輯如上,實(shí)現(xiàn)方式的不同主要體現(xiàn)在記錄已經(jīng)爬取過的信息的途徑不同。比如可以使用數(shù)據(jù)庫記錄、使用log文件記錄,或者直接檢查已經(jīng)爬取過的數(shù)據(jù)信息。
根據(jù)爬取內(nèi)容的不同實(shí)現(xiàn)方式也不同。我們這里先使用去文件夾下獲取所有文件名,然后在爬取的時候做對比。
后面的爬蟲實(shí)戰(zhàn)再使用其他方式來做去重。
單從爬取unsplash 網(wǎng)站圖片這個實(shí)例來看,需要知道文件夾中所有文件的名字就夠了。
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) 就能滿足需求。
寫一個獲取文件夾內(nèi)所有文件名的方法:
def get_files(self, path):
pic_names = os.listdir(path)
return pic_names
因?yàn)樵诒4鎴D片之前就要用到文件名的對比,所以把save_img方法修改了一下,在方法外部拼接圖片名字,然后直接作為參數(shù)傳入,而不是在圖片存儲的過程中命名:
def save_img(self, url, file_name): ##保存圖片
print('開始請求圖片地址,過程會有點(diǎn)長...')
img = self.request(url)
print('開始保存圖片')
f = open(file_name, 'ab')
f.write(img.content)
print(file_name,'圖片保存成功!')
f.close()
為了更清晰去重邏輯,我們每次啟動爬蟲的時候檢測一下圖片存放的文件夾是否存在,如果存在則檢測與文件夾中的文件做對比,如果不存在則不需要做對比(因?yàn)槭切陆ǖ穆?,文件夾里面肯定什么文件都沒有)。
邏輯修改如下,是新建的則返回True,不是新建的則返回False:
def mkdir(self, path): ##這個函數(shù)創(chuàng)建文件夾
path = path.strip()
isExists = os.path.exists(path)
if not isExists:
print('創(chuàng)建名字叫做', path, '的文件夾')
os.makedirs(path)
print('創(chuàng)建成功!')
return True
else:
print(path, '文件夾已經(jīng)存在了,不再創(chuàng)建')
return False
然后修改我們的爬取邏輯部分:
主要是添加獲取的文件夾中的文件名列表,然后判斷圖片是否存在。
is_new_folder = self.mkdir(self.folder_path) #創(chuàng)建文件夾,并判斷是否是新創(chuàng)建
file_names = self.get_files(self.folder_path) #獲取文件家中的所有文件名,類型是list
if is_new_folder:
self.save_img(img_url, img_name) # 調(diào)用save_img方法來保存圖片
else:
if img_name not in file_names:
self.save_img(img_url, img_name) # 調(diào)用save_img方法來保存圖片
else:
print("該圖片已經(jīng)存在:", img_name, ",不再重新下載。")
好了,來個完整版代碼(也可以去 GitHub下載):
from selenium import webdriver #導(dǎo)入Selenium
import requests
from bs4 import BeautifulSoup #導(dǎo)入BeautifulSoup 模塊
import os #導(dǎo)入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' #要訪問的網(wǎng)頁地址
self.folder_path = 'C:\D\BeautifulPicture' #設(shè)置圖片要存放的文件目錄
def get_pic(self):
print('開始網(wǎng)頁get請求')
# 使用selenium通過PhantomJS來進(jìn)行網(wǎng)絡(luò)請求
driver = webdriver.PhantomJS()
driver.get(self.web_url)
self.scroll_down(driver=driver, times=3) #執(zhí)行網(wǎng)頁下拉到底部操作,執(zhí)行3次
print('開始獲取所有a標(biāo)簽')
all_a = BeautifulSoup(driver.page_source, 'lxml').find_all('a', class_='cV68d') #獲取網(wǎng)頁中的class為cV68d的所有a標(biāo)簽
print('開始創(chuàng)建文件夾')
is_new_folder = self.mkdir(self.folder_path) #創(chuàng)建文件夾,并判斷是否是新創(chuàng)建
print('開始切換文件夾')
os.chdir(self.folder_path) #切換路徑至上面創(chuàng)建的文件夾
print("a標(biāo)簽的數(shù)量是:", len(all_a)) #這里添加一個查詢圖片標(biāo)簽的數(shù)量,來檢查我們下拉操作是否有誤
file_names = self.get_files(self.folder_path) #獲取文件家中的所有文件名,類型是list
for a in all_a: #循環(huán)每個標(biāo)簽,獲取標(biāo)簽中圖片的url并且進(jìn)行網(wǎng)絡(luò)請求,最后保存圖片
img_str = a['style'] #a標(biāo)簽中完整的style字符串
print('a標(biāo)簽的style內(nèi)容是:', img_str)
first_pos = img_str.index('(') + 1
second_pos = img_str.index(')')
img_url = img_str[first_pos: second_pos] #使用Python的切片功能截取雙引號之間的內(nèi)容
# 注:為了盡快看到下拉加載的效果,截取高度和寬度部分暫時注釋掉,因?yàn)閳D片較大,請求時間較長。
#獲取高度和寬度的字符在字符串中的位置
# width_pos = img_url.index('&w=')
# height_pos = img_url.index('&q=')
# width_height_str = img_url[width_pos : height_pos] #使用切片功能截取高度和寬度參數(shù),后面用來將該參數(shù)替換掉
# print('高度和寬度數(shù)據(jù)字符串是:', width_height_str)
# img_url_final = img_url.replace(width_height_str, '') #把高度和寬度的字符串替換成空字符
# print('截取后的圖片的url是:', img_url_final)
#截取url中參數(shù)前面、網(wǎng)址后面的字符串為圖片名
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) # 調(diào)用save_img方法來保存圖片
else:
if img_name not in file_names:
self.save_img(img_url, img_name) # 調(diào)用save_img方法來保存圖片
else:
print("該圖片已經(jīng)存在:", img_name, ",不再重新下載。")
def save_img(self, url, file_name): ##保存圖片
print('開始請求圖片地址,過程會有點(diǎn)長...')
img = self.request(url)
print('開始保存圖片')
f = open(file_name, 'ab')
f.write(img.content)
print(file_name,'圖片保存成功!')
f.close()
def request(self, url): #返回網(wǎng)頁的response
r = requests.get(url) # 像目標(biāo)url地址發(fā)送get請求,返回一個response對象。有沒有headers參數(shù)都可以。
return r
def mkdir(self, path): ##這個函數(shù)創(chuàng)建文件夾
path = path.strip()
isExists = os.path.exists(path)
if not isExists:
print('創(chuàng)建名字叫做', path, '的文件夾')
os.makedirs(path)
print('創(chuàng)建成功!')
return True
else:
print(path, '文件夾已經(jīng)存在了,不再創(chuàng)建')
return False
def scroll_down(self, driver, times):
for i in range(times):
print("開始執(zhí)行第", str(i + 1),"次下拉操作")
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") #執(zhí)行JavaScript實(shí)現(xiàn)網(wǎng)頁下拉倒底部
print("第", str(i + 1), "次下拉操作執(zhí)行完畢")
print("第", str(i + 1), "次等待網(wǎng)頁加載......")
time.sleep(30) # 等待30秒,頁面加載出來再執(zhí)行下拉操作
def get_files(self, path):
pic_names = os.listdir(path)
return pic_names
beauty = BeautifulPicture() #創(chuàng)建類的實(shí)例
beauty.get_pic() #執(zhí)行類中的方法
注釋寫的很詳細(xì),有任何問題可以留言。
四、后語
該實(shí)戰(zhàn)就先到這里,需要注意的是Unsplash 這個網(wǎng)站經(jīng)常不穩(wěn)定,小伙伴有遇到請求異常的情況可以多試幾次。
后面我開始寫一個查找The Beatles 樂隊(duì)的歷年專輯封面圖片和專輯名稱的爬蟲。我的設(shè)計(jì)師小伙伴兒想要收集Beatles 樂隊(duì)歷年的專輯封面圖片,然后做一個該樂隊(duì)的設(shè)計(jì)海報。
至于爬蟲框架的使用,后面我會再專門介紹,最好找到一個好的使用框架的實(shí)戰(zhàn)例子,這樣才能更好的理解框架的作用與優(yōu)點(diǎn)。
OK, See you then.