我爱编程

一个爬取人人网相册的小脚本

2018-05-22  本文已影响145人  谢宝树

背景与目标

从人人网爬取所有的图片。

主要思路与逻辑

总体思路

首先我们注意到,要爬取所有用户相册的图片,我们首先要注意到人人所有的用户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))

需要注意的地方

  1. users设置为2,主要是为了测试方便,你也可以设置为10,或者直接将循环条件改写成更可控的模式。
  2. 我没有写去重,在每次结束程序的时候,也只把待爬取里的user_id中最后那一个写入本地文件。事实上更妥善的方法是,连接数据库,建一个seen表存已经爬过的user_id,每次执行程序前,将seen中所有的user_id取出来构造成一个seen集合,每次拿到新的user_id时,先判断这个user_id是否在seen集合里,如果不在,爬取,并将其加入seen集合,再建一个表toParse存取每次结束时待爬的user_id,每次执行前去读取这个表,每次执行完覆盖掉这个表。
  3. 我们构造的header对于大部分图片来说都已经够用,但是对于人人网早期的一些图片仍然下载不了。某些图片无法爬取的原因是,它的源已不可知,显示成一张破裂图片的式样;但是有些图片无法爬取的原因是因为我们构造的header中的host是有问题的,这需要对图片的url分情况处理(在此不再细述)

缺陷

  1. 尽管代码里考虑到了一些意外情况,但是仍然要说容错率(也许应该说健壮性?)不够,比如说,最后一个user_id没有关注的人怎么办,访问的相册需要密码怎么办。
  2. phantomJS的效率并不高,因为PhantomJS的本质是一个无头浏览器,渲染本身就需要许多时间。
  3. 多线程多进程和异步的问题

总而言之,是时候上一个框架了。

github地址

https://github.com/baoshuxie/renrenscript

上一篇下一篇

猜你喜欢

热点阅读