背景與目標
從人人網(wǎng)爬取所有的圖片喊崖。
主要思路與邏輯
總體思路
首先我們注意到蒋荚,要爬取所有用戶相冊的圖片,我們首先要注意到人人所有的用戶ID都是九位數(shù)筹陵。比如這個相冊網(wǎng)址
http://photo.renren.com/photo/965740423/albumlist/v7?offset=0&limit=40#
嘗試去掉 “ 沿后?”后面的參數(shù),我們會發(fā)現(xiàn)這個相冊網(wǎng)址是結(jié)構(gòu)性的朽砰,可以被拆解為 http://photo.renren.com/photo/ + user_id +/albumlist/v7尖滚。
一個簡單而直接的想法是,我們將user_id從1遍歷到999999999,訪問每個ID的相冊瞧柔,如果為空漆弄,跳過;否則爬取這個相冊造锅。
但是撼唾,顯然,人人網(wǎng)的有效用戶數(shù)應該是不足一億的哥蔚,也就是說我們的命中率不超過百分之十倒谷,我們會爬取許多的無效頁面蛛蒙,這在效率上是一種拖累。
換一個思路渤愁,我們從某一個源用戶出發(fā)牵祟,訪問他關(guān)注的用戶,或者關(guān)注他的用戶抖格,然后再訪問他關(guān)注的那些用戶所關(guān)注的用戶诺苹,最后我們會遍歷到所有人人網(wǎng)有人關(guān)注的用戶(當然,也會漏掉所有無人關(guān)注的用戶雹拄,不過收奔,我相信這種邊緣性用戶是非常少的,可以忽略)滓玖。得到用戶的user_id之后坪哄,我們就可以訪問他的相冊,得到每個相冊的url呢撞,再訪問每個相冊的url损姜,拿到每張照片的url,最后訪問每張照片的url并保存在本地殊霞。
獲取每個用戶的所有相冊url
鑒于我們本機的硬盤容量和測試需要摧阅,我們顯然不能一開始就拿到所有的user_id,必須給它設(shè)置一個終止條件绷蹲,我們在同級目錄下創(chuàng)建一個photos文件夾棒卷,文件夾下創(chuàng)建一個id.txt,將初始的源用戶ID寫在里面祝钢,每次我們終止這個程序的時候比规,將剩余的未爬的user_id覆蓋性寫入這個txt,下一次開始的時候,再從這個txt讀取一開始的user_id列表拦英。這樣就相當于一次變相的斷點續(xù)爬蜒什。
對于每個user_id,構(gòu)造出他的相冊頁面,并爬取出所有的album_id疤估,構(gòu)造album_url添加進album_list
代碼如下:
#獲取關(guān)注的用戶的urls
def get_urls(base_user_id,users):
browser = webdriver.PhantomJS()
urls =[]
base_url = 'http://www.renren.com/SysHome.do' #登錄頁
browser.get(base_url)
browser.find_element_by_id('email').clear()
browser.find_element_by_id('password').clear()
browser.find_element_by_id('email').send_keys('13689024414')
browser.find_element_by_id('password').send_keys('19950708')
browser.find_element_by_id('login').click()
time.sleep(1) #請勿注釋掉這一句
user_ids=['%s' %base_user_id]
print('OK1')
count=0 #count是用來控制循環(huán)的灾常,否則user_ids不斷被append進urls,會直到爬取完所有的人人頁面才終止程序
for user_id in user_ids:
user_id = user_ids.pop(0)
user_url = 'http://follow.renren.com/list/' + user_id + '/pub/v7' #訪問每個用戶的關(guān)注用戶頁
browser.get(user_url)
followers = browser.find_elements_by_class_name('photo') #用選擇器找到每個關(guān)注者
for follower in followers:
follower_id = follower.get_attribute('namecard') #拿到關(guān)注者的user_id
print(follower_id)
if follower_id not in user_ids:
user_ids.append(follower_id) #簡單的去重,加入user_ids列表
url='http://photo.renren.com/photo/' + '%s' % follower_id + '/albumlist/v7?offset=0&limit=40#' #構(gòu)造關(guān)注者的相冊url
if url not in urls:
urls.append(url)
print(url)
count += 1
if count==users:
break
#print('OK2')
if user_ids:
print(user_ids[-1])
with open('photos/last_id.txt','w+',encoding='utf-8') as f: #循環(huán)結(jié)束后铃拇,將user_ids里面最后一個user_id寫入本地文件
f.write(user_ids[-1])
with open('photos/time.txt','a+',encoding='utf-8') as f: #記錄運行時間和每次保存的最后一個user_id
f.write(user_ids[-1]+'\n')
#print(urls)
browser.quit()
return urls
爬取相冊中每個照片的url
人人網(wǎng)的相冊的照片是動態(tài)加載的钞瀑,用的是AJAX。雖然用Selenium+PhantomJS模擬也能看到慷荔,但遠不如直接訪問AJAX來得快雕什。打開Chrome,訪問某個照片數(shù)量大于40的相冊頁面,打開開發(fā)者工具-network-篩選XHR贷岸,頁面下拉壹士,可以看到形如http://photo.renren.com/photo/341508340/album-622844419/bypage/ajax/v7?page=3&pageSize=20的請求,這同樣是由user_id和album_id構(gòu)造出來的json url凰盔,直接讀取解析即可墓卦。
代碼如下:
#獲取照片的url
def get_photo_urls(base_user_id,users):
browser = webdriver.PhantomJS()
urls = get_urls(base_user_id,users) #調(diào)用get_urls獲取urls
album_urls = []
base_url = 'http://www.renren.com/SysHome.do'
browser.get(base_url)
browser.find_element_by_id('email').clear()
browser.find_element_by_id('password').clear()
browser.find_element_by_id('email').send_keys('xxxxxxxxxx') #請輸入你自己的手機號
browser.find_element_by_id('password').send_keys('xxxxxxx') #請輸入你自己的密碼
browser.find_element_by_id('login').click()
time.sleep(1)
print('OK3')
#print(browser.get_cookies())
for url in urls:
print(url)
browser.get(url)
browser.implicitly_wait(3)
albums=browser.find_elements_by_class_name("album-box") #找到相冊
for album in albums:
if not album:
continue
album_url = album.find_element_by_class_name('album-item').get_attribute('href')
count = album.find_element_by_class_name('album-count').text
item={}
print(album_url,count)
item['url'] = album_url
item['count'] = int(count)
album_urls.append(item) #album_urls是dict的list,包含每個相冊的url和相冊中照片的數(shù)量count
photo_urls=[]
for item in album_urls: #http://photo.renren.com/photo/500999244/album-848184418/v7?page=3&pageSize=20
for i in range(1,math.ceil(item['count']/20)+1):
browser.get(item['url']+'?page=%d&pageSize=20'%i) #訪問照片數(shù)據(jù)來源的url
browser.implicitly_wait(3)
photos=browser.find_elements_by_class_name("photo-box")
for photo in photos:
photo_url = photo.find_element_by_class_name('p-b-item').get_attribute('src')
print(photo_url)
photo_urls.append(photo_url)
browser.quit()
return photo_urls
下載到本地
我們在這個程序的同級目錄下建立一個photos文件夾,用于保存下載到本地的圖片户敬,并按下載日期分類落剪。
代碼如下:
#創(chuàng)建路徑
def make_dir(path):
if not os.path.exists(path):
os.mkdir(path)
return None
#圖片保存到本地
def save_photos(urls):
date = datetime.now()
dir_name = date.strftime('%b %d')
make_dir('photos/'+dir_name)
n=1
for url in urls:
if not url:
continue
print(url)
name = url[8:].replace('/','_')
file_name = '%s' % name
with open('photos/'+dir_name+'/'+file_name,'wb+') as f: #請確保在本腳本的同級目錄下有一個photos文件夾,下載的圖片將會存儲在photos下按日期建立的文件夾中
f.write(requests.get(url,headers=header(url)).content)
print('正在下載第%s張圖片' % n)
n = n+1
print('OK4')
return n
構(gòu)造header
事實上尿庐,通過前幾步下載到本地的圖片忠怖,我們會發(fā)現(xiàn)還是無法打開,原因是因為我們用來請求的header是python自帶的header,網(wǎng)站不會給這種header返回正確的內(nèi)容抄瑟。所以我們要把自己偽裝成一個正常的用戶凡泣,代碼如下
def header(referer):
headers = {
'Host': 'fmn.rrimg.com',
'Pragma': 'no-cache',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/59.0.3071.115 Safari/537.36',
'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
'Referer': '{}'.format(referer),
}
return headers
主程序main
def main():
start_time=datetime.now()
start_date=start_time.strftime('%b''%d')
with open('photos/last_id.txt','r') as f:
base_user_id=f.read() #獲取初始id,每次運行本程序之后此ID在get_urls()中更新
users = 2 #獲取循環(huán)次數(shù)
photo_urls = get_photo_urls(base_user_id,users)
num = save_photos(photo_urls)
end_time=datetime.now()
do_time = (end_time - start_time).seconds
with open('photos/time.txt','a+',encoding='utf-8') as f:
f.write("{} 爬取了 {} 張圖片,耗時{}秒\n".format(start_date,num,do_time))
需要注意的地方
- users設(shè)置為2,主要是為了測試方便皮假,你也可以設(shè)置為10鞋拟,或者直接將循環(huán)條件改寫成更可控的模式。
- 我沒有寫去重惹资,在每次結(jié)束程序的時候贺纲,也只把待爬取里的user_id中最后那一個寫入本地文件。事實上更妥善的方法是褪测,連接數(shù)據(jù)庫猴誊,建一個seen表存已經(jīng)爬過的user_id,每次執(zhí)行程序前侮措,將seen中所有的user_id取出來構(gòu)造成一個seen集合懈叹,每次拿到新的user_id時,先判斷這個user_id是否在seen集合里分扎,如果不在澄成,爬取,并將其加入seen集合畏吓,再建一個表toParse存取每次結(jié)束時待爬的user_id,每次執(zhí)行前去讀取這個表环揽,每次執(zhí)行完覆蓋掉這個表。
- 我們構(gòu)造的header對于大部分圖片來說都已經(jīng)夠用庵佣,但是對于人人網(wǎng)早期的一些圖片仍然下載不了。某些圖片無法爬取的原因是汛兜,它的源已不可知巴粪,顯示成一張破裂圖片的式樣;但是有些圖片無法爬取的原因是因為我們構(gòu)造的header中的host是有問題的,這需要對圖片的url分情況處理(在此不再細述)
缺陷
- 盡管代碼里考慮到了一些意外情況肛根,但是仍然要說容錯率(也許應該說健壯性辫塌?)不夠,比如說派哲,最后一個user_id沒有關(guān)注的人怎么辦臼氨,訪問的相冊需要密碼怎么辦。
- phantomJS的效率并不高芭届,因為PhantomJS的本質(zhì)是一個無頭瀏覽器储矩,渲染本身就需要許多時間。
- 多線程多進程和異步的問題
總而言之褂乍,是時候上一個框架了持隧。