【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的使用,異常處理、寫入文件等。基本上把自己想要的功能都實現了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章