一个爬取人人网相册的小脚本
背景与目标
从人人网爬取所有的图片。
主要思路与逻辑
总体思路
首先我们注意到,要爬取所有用户相册的图片,我们首先要注意到人人所有的用户ID都是九位数。比如这个相册网址
http://photo.renren.com/photo/965740423/albumlist/v7?offset=0&limit=40#
尝试去掉 “ ?”后面的参数,我们会发现这个相册网址是结构性的,可以被拆解为 http://photo.renren.com/photo/ + user_id +/albumlist/v7。
一个简单而直接的想法是,我们将user_id从1遍历到999999999,访问每个ID的相册,如果为空,跳过;否则爬取这个相册。
但是,显然,人人网的有效用户数应该是不足一亿的,也就是说我们的命中率不超过百分之十,我们会爬取许多的无效页面,这在效率上是一种拖累。
换一个思路,我们从某一个源用户出发,访问他关注的用户,或者关注他的用户,然后再访问他关注的那些用户所关注的用户,最后我们会遍历到所有人人网有人关注的用户(当然,也会漏掉所有无人关注的用户,不过,我相信这种边缘性用户是非常少的,可以忽略)。得到用户的user_id之后,我们就可以访问他的相册,得到每个相册的url,再访问每个相册的url,拿到每张照片的url,最后访问每张照片的url并保存在本地。
获取每个用户的所有相册url
鉴于我们本机的硬盘容量和测试需要,我们显然不能一开始就拿到所有的user_id,必须给它设置一个终止条件,我们在同级目录下创建一个photos文件夹,文件夹下创建一个id.txt,将初始的源用户ID写在里面,每次我们终止这个程序的时候,将剩余的未爬的user_id覆盖性写入这个txt,下一次开始的时候,再从这个txt读取一开始的user_id列表。这样就相当于一次变相的断点续爬。
对于每个user_id,构造出他的相册页面,并爬取出所有的album_id,构造album_url添加进album_list
代码如下:
#获取关注的用户的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是用来控制循环的,否则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' #访问每个用户的关注用户页
browser.get(user_url)
followers = browser.find_elements_by_class_name('photo') #用选择器找到每个关注者
for follower in followers:
follower_id = follower.get_attribute('namecard') #拿到关注者的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#' #构造关注者的相册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: #循环结束后,将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
人人网的相册的照片是动态加载的,用的是AJAX。虽然用Selenium+PhantomJS模拟也能看到,但远不如直接访问AJAX来得快。打开Chrome,访问某个照片数量大于40的相册页面,打开开发者工具-network-筛选XHR,页面下拉,可以看到形如http://photo.renren.com/photo/341508340/album-622844419/bypage/ajax/v7?page=3&pageSize=20的请求,这同样是由user_id和album_id构造出来的json url,直接读取解析即可。
代码如下:
#获取照片的url
def get_photo_urls(base_user_id,users):
browser = webdriver.PhantomJS()
urls = get_urls(base_user_id,users) #调用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和相册中照片的数量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) #访问照片数据来源的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文件夹,用于保存下载到本地的图片,并按下载日期分类。
代码如下:
#创建路径
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
构造header
事实上,通过前几步下载到本地的图片,我们会发现还是无法打开,原因是因为我们用来请求的header是python自带的header,网站不会给这种header返回正确的内容。所以我们要把自己伪装成一个正常的用户,代码如下
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 #获取循环次数
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设置为2,主要是为了测试方便,你也可以设置为10,或者直接将循环条件改写成更可控的模式。
- 我没有写去重,在每次结束程序的时候,也只把待爬取里的user_id中最后那一个写入本地文件。事实上更妥善的方法是,连接数据库,建一个seen表存已经爬过的user_id,每次执行程序前,将seen中所有的user_id取出来构造成一个seen集合,每次拿到新的user_id时,先判断这个user_id是否在seen集合里,如果不在,爬取,并将其加入seen集合,再建一个表toParse存取每次结束时待爬的user_id,每次执行前去读取这个表,每次执行完覆盖掉这个表。
- 我们构造的header对于大部分图片来说都已经够用,但是对于人人网早期的一些图片仍然下载不了。某些图片无法爬取的原因是,它的源已不可知,显示成一张破裂图片的式样;但是有些图片无法爬取的原因是因为我们构造的header中的host是有问题的,这需要对图片的url分情况处理(在此不再细述)
缺陷
- 尽管代码里考虑到了一些意外情况,但是仍然要说容错率(也许应该说健壮性?)不够,比如说,最后一个user_id没有关注的人怎么办,访问的相册需要密码怎么办。
- phantomJS的效率并不高,因为PhantomJS的本质是一个无头浏览器,渲染本身就需要许多时间。
- 多线程多进程和异步的问题
总而言之,是时候上一个框架了。