【python】爬虫入门:代理IP池的使用、文件的写入与网易云爬取时的注意事项

一、概述

在两天前实现利用爬虫爬取网易云音乐用户的各类公开信息之后,我对现有爬虫进行了功能上的增加。主要有:

①、使用代理IP池防止IP被封;

②、将爬取用户的听歌记录、歌单、关注、粉丝这四类数据的代码分别封装成函数;

③、将爬取到的数据写入csv文件;

④、实现从指定某一用户开始,对其粉丝,粉丝的粉丝......等进行BFS式爬取。

二、具体实现

1、使用IP代理池

我们知道,使用爬虫实际上就是我们发送一条请求到服务器,服务器再返回——那如果我们发的太多太快,服务器察觉出来不对:你这小子不是人啊,是我同类,然后就不搭理你了。这不就完蛋了。

服务器怎么察觉出来不对的:你同一个IP,一直给我发消息,还都是一样的消息,他只要不傻就看得出来。

那我们换不同的IP不就得了。但是我们自己计算机或者服务器的IP是固定的(公网IP或DHCP分配的动态IP),自己改不了,那怎么办?

用代理IP咯。代理IP就是一个跳板,我们先访问代理IP,然后再去访问服务器,那样服务器就会误认为是代理IP访问它而不是我们的IP访问它,它就不会察觉出不对,就会一直搭理我们。追女孩子也是这样,你一直拿一个大号去撩同一个妹子,你要是很无趣那妹子很容易就给你发好人卡;但你用一堆小号去撩一个妹子,那即使发好人卡,还得罚一阵子呢,当然如果你太笨,每个号起手在吗反手多喝热水,被人家识破了小号,那好人卡就一发发一堆了。这里的小号就是代理IP,起手在吗反手多喝热水就是request报文中的header,高端的反爬策略会识别headers,你要是错了无法返回正确信息。所以,不论是爬虫还是撩妹,都要变通。当然前提是这妹子没有设置不允许任何人加我为好友,否则直接自闭你也没法子。

代理IP有免费的有收费的,收费的质量好,相当于空间五彩斑斓的纯净的太阳大号;免费的质量差,相当于空间不开的星星小号。你要是妹子,前者和后者同时加你,你通过谁?肯定是大号啊,这就是收费的优势。但是说不定有一些妹子心地善良,星星小号也会通过——这对我们已经够用了。所以我们在自己玩的时候就用免费的IP代理就可以了。

免费的IP代理推荐这个,很多人用的ProxyPool,强烈建议在虚拟环境中运行。注意目前所使用的redis大版本已经到3了,而该项目支持的是2系列的redis。所以如果使用3,需要在db.py中将代码修改为如下形式

return self.db.zadd(REDIS_KEY, {proxy:MAX_SCORE})#line76
return self.db.zincrby(REDIS_KEY, -1,proxy)#line56
return self.db.zadd(REDIS_KEY, {proxy:score})#line30

简而言之,第一行和第三行是redis自己的zadd函数的问题,在3版本中,zadd函数不再支持输入三个参数,因此后两个参数要以字典的形式传入,第二行是作者自己把参数顺序搞反了。

在修改完之后,先开启redis服务:

然后在虚拟环境中运行ProxyPool的run.py:

说明代理池已经正常运行。我们可以从池子中随机挑选代理了。

如何在爬虫代码中使用代理呢?

首先要从池子中获取IP,默认从'http://localhost:5555/random'这个url获取IP,一次获取一条。可以写一个函数封装一下:

PROXY_POOL_URL = 'http://localhost:5555/random'
def get_proxy():
    try:
        response = requests.get(PROXY_POOL_URL)
        if response.status_code == 200:
            return response.text
    except ConnectionError:
        return None

简而言之就是向这个url发个请求,它就自动返回一条IP,我们把这个IP返回就行。

有IP了,该怎么用呢?

很简单,request的post函数有一个proxies方法,将我们的IP传入这个方法就行:

tempIP=get_proxy()
proxies = {
    'http': tempIP,
    'https':tempIP,
}   
print("当前使用IP为:"+tempIP)
response = requests.post(url, headers=headers, data=data,proxies=proxies)

proxies是一个字典,键名最好一个http一个https,要是没有https,默认用我们自己的IP访问,那就糟了,容易被封。

如何判断我们访问用的是代理IP而不是本机IP呢?在下面加上如下代码:

res =requests.get('http://icanhazip.com/', proxies=proxies)
print(res.content)
res =requests.get('https://ip.cn', proxies=proxies)
print(res.content)

分别用该代理访问两个IP查询网站,看返回内容:

如图,两个IP查询网站返回的都是代理IP,所以我们无论访问http还是https,都用的是代理IP,说明设置成功。

2、将爬取数据的代码封装成函数并将数据写入csv

这就是苦力活。注意到我们最后要用BFS来进行遍历,因此在设计参数的时候,一定要有:

ID队列:BFS的基础,队列为空循环结束(队列几乎不可能有空的时候);

ID字典:ID与用户名的对应关系,便于寻找映射;

ID:用于构建url等;

用户名:用于新建文件等;

IDset:用于查重,如果该用户之前已经爬过,就不再爬。

另外,函数应具备以下功能:

异常处理,当目标服务器拒绝,即当前IP代理不好用的时候,应抛出异常,换一个代理重新发请求;

将信息写入csv文件,注意格式。

以爬取某用户的粉丝为例:

由于我设计的函数逻辑是根据粉丝关系进行遍历的,因此该函数也应具备更新队列的功能。代码如下:

def Spider_Followed(uid:int,IDset:set,UserName:str,IDdict:dict,IDpool:Queue):
    url="https://music.163.com/weapi/user/getfolloweds?csrf_token="#粉丝
    pre_param=Create_uid(uid,3)
    params = get_params(pre_param)
    encSecKey = get_encSecKey()
    tries = 3
    while tries>0:
        try:
            json_text = get_json(url, params, encSecKey)
            break
        except:
            tries-=1
            time.sleep(1)
    if tries == 0:
        print("用户"+UserName+"抓取失败!")
        return None
    json_dict = json.loads(json_text)
    if len(json_dict['followeds'])>30:
        return None
    current_path = os.getcwd()
    path = current_path+"\\spider\\file\\粉丝\\"
    with open(path+"总粉丝数据.csv","a",newline='',encoding='utf-8') as csvfile: 
        writer = csv.writer(csvfile)
        writer.writerow(["-------------------------------"+UserName+"的粉丝-------------------------------"])
        writer.writerows([["昵称"+'|'+"userID"]])
        for item in json_dict['followeds']:
            writer.writerows([[item['nickname']+'|'+str(item['userId'])]])
    with open(path+UserName+"的粉丝.csv","w",newline='',encoding='utf-8') as csvfile: 
        writer = csv.writer(csvfile)
        writer.writerows([["昵称"+'|'+"userID"]])
        for item in json_dict['followeds']:
            writer.writerows([[item['nickname']+'|'+str(item['userId'])]])
            if item['userId'] not in IDset and item['accountStatus'] == 0: 
                IDset.add(item['userId'])
                IDdict[item['userId']]=item['nickname']
                IDpool.put(item['userId'])

首先是构造url和参数,由于不同的url需要的参数不同,所以在构造参数的函数中要传入我们需要的参数类型,当然这个构造参数的函数要我们自己写= =,我的四个参数的构造函数如下,这应该是整个爬虫代码里面最有价值的了

def Create_uid(uid,rtype):#1:听歌记录 2:歌单 3:粉丝 4:关注
    if rtype==1:
        uidParam="{uid: \""+str(uid)+"\",type: \"-1\",limit: \"1000\",offset: \"0\",total: \"true\",csrf_token: \"\"}"
    elif rtype==2:
        uidParam="{uid: \""+str(uid)+"\",limit: \"20\",offset: \"0\",csrf_token: \"\"}"
    elif rtype==3:
        uidParam="{userId: \""+str(uid)+"\",limit: \"20\",offset: \"0\",total: \"true\",csrf_token: \"\"}"
    elif rtype==4:
        uidParam="{uid: \""+str(uid)+"\",limit: \"20\",offset: \"0\",total: \"true\",csrf_token: \"\"}"
    return uidParam

然后是进行最多三次的请求发送:while循环,循环体为try,如果发送请求成功则退出循环,否则tries-1再次循环。若tries为0则说明三次请求均失败,则直接结束函数。

之后是取得返回信息。

当我们抓到的用户,其粉丝数大于30时,我就认为该用户是一个知名用户,无抓取价值,结束函数。

该函数的数据要写入两个文件:第一个是总粉丝数据文件,第二个是该用户的粉丝数据文件。为写入文件,首先要构建文件路径,使用os.getcwd可以得到当前路径,通过字符串拼接可以得到我们想要的路径和文件名、文件格式。利用open函数可以进行写入,open函数的w参数为新建一个文件并写入,若原来文件存在,则进行覆盖写入;a参数为新建一个文件并写入,若原来文件存在,则继续写入。由此,总数据要用a,用户数据要用w。

注意,csv写入一行数据要用writerow,写入多行用writerows,对于要写在一个格子中的字符串,要用[]括起来,如果不括起来,那么该字符串的每一个字符会占一个格子,对csv来说,apple会变成a,p,p,l,e,很难看。

其他函数的具体实现与该函数类似。

有以下几个针对网易云的特定问题要注意:

第一,听歌记录无法读取的问题,部分用户会将听歌记录隐藏,这时是抓不到的,不注意的话会报错。此类用户的返回值中,code的一项值为-2,正常的应该为200;

第二,部分用户为已注销用户,若对其进行爬取会报错,因为什么都爬不到,这类用户的accountStatus值为30,正常的应为0;

第三,歌曲名字可能不是gbk字符,这时会报编码错误,一劳永逸的方法为在新建文件时候指定编码为utf-8,也就是open函数添加参数encoding='utf-8';

3、实现BFS爬取

这就是主函数的活了。BFS很简单,建队,入队,BFS,入队,出队,BFS,入队,出队......

python自己没有队列,可以import一个Queue包,注意这个包里面的队列会指定最大长度,到最大长度会阻塞。

我最开始只指定长度为1000,自己还是太年轻,怕了一百多人就阻塞了。为什么?因为这个队列的增长速度和你已经爬完的人的增长速度的比例大概为1:15左右,如下:

然后我直接将长度设置为100000,这下空间暂时够了:

代码如下:

if __name__ == "__main__":
    IDpool = Queue(maxsize=100000)
    IDset = set()
    IDdict = dict()
    IDdict[初始用户ID]="初始用户姓名"
    IDset.add(初始用户ID)
    IDpool.put(初始用户ID)
    while not IDpool.empty():
        tmpID=IDpool.get()
        Spider_Followed(tmpID,IDset,IDdict[tmpID],IDdict,IDpool)
        print(".......... 25% ",end="")
        time.sleep(2)
        Spider_Follows(tmpID,IDdict[tmpID],IDdict)
        print(".......... 50% ",end="")
        time.sleep(2)
        Spider_Playlist(tmpID,IDdict[tmpID],IDdict)
        print(".......... 75% ",end="")
        time.sleep(2)
        Spider_Record(tmpID,IDdict[tmpID],IDdict)
        time.sleep(2)
        print(".......... 100%")
        print("已完成"+IDdict[tmpID]+"的信息爬取")
        print("剩余"+str(IDpool.qsize())+"人未爬取")

注意sleep的使用,防止服务器封IP我可以说是很小心了。

4、实际效果

由于代理IP良莠不齐,因此要想把一个人的四个数据都爬下来还是有点难。比如说我的效果:

一大堆抓取失败。

要是慢慢用自己的IP爬,还是可以爬到很多数据的:

三、总结

练习了爬虫必备的几个技能,如代理IP的使用,异常处理、写入文件等。基本上把自己想要的功能都实现了。

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值