Python高級特性與網絡爬蟲(一):使用Ajax請求爬取用戶微博內容和python多進程爬取用戶圖片

最近閱讀了崔慶才寫的《Python3網絡爬蟲開發實戰》,系統地學習一下利用Python寫網絡爬蟲。由於這本書出版時間是2018年,很多書中案例涉及的網站已經改版,基本上每個案例都需要自己再研究一下網站改版後新的結構來爬取數據。這篇博文就來介紹一下如何爬取一下新浪微博用戶的微博信息和下載該用戶的微博圖片,其中涉及到的技術包括Ajax數據爬取,Python和MongoDB的交互以及windows下python的多進程編程。

使用Ajax請求爬取用戶微博信息並存儲到後端MongoDB

關於基礎的網頁前端各個節點的結構,http的get和post請求,requests庫中的get函數用法,普通網頁header請求頭的構建,Beautiful Soup和pyquery解析庫的使用這裏不做過多介紹,對這方面不是很瞭解的同學建議先去看一下《Python3網絡爬蟲開發實戰》中的前四章來詳細瞭解一下。這裏簡要介紹一下什麼是Ajax加載,它是一種異步的數據加載方式,原始的頁面最初不會包含某些數據,原始頁面加載完之後,會再向服務器請求某個接口獲取數據,然後數據才被處理從而呈現到網頁上,這其實就是發送了一個Ajax請求,本質上來講Ajax是利用了JavaScript在保證頁面不被刷新,頁面鏈接不改變的情況下與服務器交換數據並更新部分網頁的技術。
以我的移動端新浪微博頁面爲例:https://m.weibo.cn/u/6163257669,選擇Network下面的XHR選項卡,點擊刷新頁面之後,在下拉的過程中會發現有getIndex?開頭的請求冒出來,這就是Ajax請求,構造Ajax請求主要是提供Requests header和Query String Parameters中的參數。
在這裏插入圖片描述
再看一下該請求返回的數據Previews,如下圖所示,其中cards中的絕大部分card對應的是每條微博,點開一個card,若有mblog字段代表這條card對應的是一條微博,mblog字段下面不同字段包含這條微博的所有信息。我們這次主要是爬取每條微博的id,text,attitudes,comments,reposts這幾個屬性。然後將信息寫入MongoDB中,windows下MongoDB的安裝可以參考https://www.runoob.com/mongodb/mongodb-window-install.html,爲了能夠之直接在命令行通過mongo命令啓動數據庫命令行,可以將mongo安裝目錄(例如C:\Program Files\MongoDB\Server\4.2\bin)添加到相應的系統環境變量。
在這裏插入圖片描述在這裏插入圖片描述
關於構造請求的query string parameters參數,其中有一個since_id,這個代表每次Ajax加載時對應的第一條微博id,我們遍歷所有微博的方法就是從該用戶第一條微博id(有些用戶頁面上的第一條微博是置頂的,注意不要選該微博作爲第一條微博,需要找時間上最晚發的微博)開始作爲起始的since_id,返回的數據包含之後的十條card,然後再以最後一個card作爲起始的since_id再爬取後續的十條card(下一次返回的結果中要去掉第一條,防止重複爬取該條微博),以此類推最終遍歷完所有微博。最終爬取的腳本如下所示:

#author:xfxy
#time:2020/04/18

from urllib.parse import urlencode
from pyquery import PyQuery as pq
import requests
import time
import pymongo
base_url='https://m.weibo.cn/api/container/getIndex?'
def get_all_weibo(since_id):    #該函數通過提供since_id參數來爬取cards
    if since_id==None:
        return None
    #請求頭的構建
    headers={
        'Referer': 'https://m.weibo.cn/u/6163257669?from=myfollow_all&is_all=1&sudaref=login.sina.com.cn',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3706.400 SLBrowser/10.0.4040.400',
        'X-Requested-With': 'XMLHttpRequest',
    }
    #Query string parameters的構建
    params={
        'from': 'myfollow_all',
        'is_all': '1',
        'sudaref': 'login.sina.com.cn',
        'type': 'uid',
        'value': '6163257669', #這是我微博的id,如果想爬取某個用戶的微博,需要更改value,containerid,headers中的Referer
        'containerid': '1076036163257669',
        'since_id': since_id,
    }
    url=base_url+urlencode(params) #通過Query string parameters構造請求url
    r=requests.get(url,headers=headers)
    return r.json().get('data').get('cards')

items=get_all_weibo('4494845651530130') #since_id爲我第一條微博的id
client = pymongo.MongoClient(host='localhost',port=27017) #建立和MongoDB的鏈接
db=client.weibo #建立數據庫weibo
collection=db.xfxy #建立鍵值對集合collections
while(1): #循環退出條件寫在循環體中
    for item in items:
        if item.get('mblog')!=None:
            item=item.get('mblog')
        else:
            continue
        weibo=dict()
        weibo['id']=item.get('id')
        weibo['text']=pq(item.get('text')).text()
        weibo['attitudes']=item.get('attitudes_count')
        weibo['comments']=item.get('comments_count')
        weibo['reposts']=item.get('reposts_count')
        print(weibo)
        collection.insert(weibo) #向MongoDB中插入數據
    for i in range(len(items)):  #尋找最後一條含有mblog字段的card,取其mblog字段中的id作爲下一次循環的id
        if items[len(items)-1-i].get('mblog')!=None:
            since_id=items[-1].get('mblog').get('id')
            break
    items=get_all_weibo(since_id)
    time.sleep(0.5) #設置sleep防止請求過於頻繁
    if items==None or items[0]==items[-1]: #循環退出條件,只剩最後一條card或者不存在card
        break
    items=items[1:] #去掉作爲請求的since_id對應的微博,防止重複爬取

最後print出的結果和存儲在MongoDB中的數據如下所示:
在這裏插入圖片描述
在這裏插入圖片描述

Windows下多進程爬取微博用戶高清大圖

有些時候我們關注了一些喜歡發高清美圖的博主,想要爬取該博主發的原創微博的原圖。原創微博帶有圖片的,mblog字段中會有pics字段,如下圖所示中最後那行的pics,點開後我們可以看到該條微博內每張圖片都單獨列了一個條目,如圖中的0,0下面large字段字段內的url就是對應的大圖的網址,通過get該網址的內容便可下載該圖片。
在這裏插入圖片描述
在這裏插入圖片描述
我們首先用單進程的方法爬取微博用戶發的大圖,腳本如下所示,遍歷微博和之前的腳本使用的方法相同,不同的地方在於圖片下載url的獲取。最後圖片都會下載到xfxy文件內,可以通過cnt_pic控制下載圖片的數量,如下圖所示:

#author:xfxy
#time:2020/04/18

from urllib.parse import urlencode
import requests
import time
import os
base_url='https://m.weibo.cn/api/container/getIndex?'
def get_all_weibo(since_id):
    if since_id==None:
        return None
    headers={
        'Referer': 'https://m.weibo.cn/u/6163257669?from=myfollow_all&is_all=1&sudaref=login.sina.com.cn',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3706.400 SLBrowser/10.0.4040.400',
        'X-Requested-With': 'XMLHttpRequest',
    }
    params={
        'from': 'myfollow_all',
        'is_all': '1',
        'sudaref': 'login.sina.com.cn',
        'type': 'uid',
        'value': '6163257669',
        'containerid': '1076036163257669',
        'since_id': since_id,
    }
    url=base_url+urlencode(params)
    r=requests.get(url,headers=headers)
    return r.json().get('data').get('cards')

items=get_all_weibo('4494845651530130')
start=time.time()
cnt_pic=0
while(1):
    for item in items:
        if item.get('mblog')!=None:
            item=item.get('mblog')
        else:
            continue
        item = item.get('pics') #獲取有圖片的微博下各個圖片信息的list
        if item != None:
            for pic in item:
                if pic.get('large') != None:
                    url=pic.get('large').get('url') #獲取大圖下載url
                    title = url.split('/')[-1] #從url中獲取圖片名
                    with open('xfxy\{0}'.format(title),'wb') as f:
                        f.write(requests.get(url).content)
                        cnt_pic=cnt_pic+1
    if cnt_pic>200: #若下載圖片數量大於200,則循環退出
        break
    for i in range(len(items)):  #尋找最後一條含有mblog字段的card,取其mblog字段中的id作爲下一次循環的id
        if items[len(items)-1-i].get('mblog')!=None:
            since_id=items[-1].get('mblog').get('id')
            break
    items=get_all_weibo(since_id)
    time.sleep(0.5)
    if items==None or len(items)<=1:
        break
    items=items[1:]
print("運行時間:",time.time()-start)

在這裏插入圖片描述
由於下載的圖片均爲大圖,如果爬取的圖片數量比較多,有大量的時間都會耗費在下載圖片上,所以嘗試將要爬取的圖片下載url整合到一個list之後,通過多進程的方式下載圖片,利用該思路修改後的爬取腳本如下所示,通過multiprocessing實現多進程圖片下載,大家可以爬取發圖片比較多的博主對比一下多進程與單進程的效果,我自己找了一個博主爬取其發過的兩百多張圖片,單進程與多進程的效果如下面兩張圖所示,可以看到多進程爬取快了很多:

#author:xfxy
#time:2020/04/18

from urllib.parse import urlencode
import requests
import time
from multiprocessing import Pool,freeze_support
base_url='https://m.weibo.cn/api/container/getIndex?'
def get_all_weibo(since_id):
    if since_id==None:
        return None
    headers={
        'Referer': 'https://m.weibo.cn/u/6163257669?from=myfollow_all&is_all=1&sudaref=login.sina.com.cn',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3706.400 SLBrowser/10.0.4040.400',
        'X-Requested-With': 'XMLHttpRequest',
    }
    params={
        'from': 'myfollow_all',
        'is_all': '1',
        'sudaref': 'login.sina.com.cn',
        'type': 'uid',
        'value': '6163257669',
        'containerid': '1076036163257669',
        'since_id': since_id,
    }
    url=base_url+urlencode(params)
    r=requests.get(url,headers=headers)
    return r.json().get('data').get('cards')

def load_pics(url):
    title = url.split('/')[-1]
    with open('yaoyao\{0}'.format(title), 'wb') as f:
        f.write(requests.get(url).content)


if __name__ =='__main__':
    freeze_support() #在windows下使用multiprocess,由於windows下不是通過fork()產生子進程,所以需要加上freeze_support()
    items=get_all_weibo('4494883576803454')
    start=time.time()
    pic_urls=[]
    while(1):
        for item in items:
            if item.get('mblog')!=None:
                item=item.get('mblog')
            else:
                continue
            item = item.get('pics')
            if item != None:
                for pic in item:
                    if pic.get('large') != None:
                        url=pic.get('large').get('url')
                        pic_urls.append(url)
        print(len(pic_urls))
        if len(pic_urls)>200:
            break
        for i in range(len(items)):  #尋找最後一條含有mblog字段的card,取其mblog字段中的id作爲下一次循環的id
            if items[len(items)-1-i].get('mblog')!=None:
                since_id=items[-1].get('mblog').get('id')
                break
        items=get_all_weibo(since_id)
        time.sleep(0.5)
        if items==None or len(items)<=1:
            break
        items=items[1:]
    #
    pool=Pool() # 創建進程池
    pool.map(load_pics,pic_urls) #第一個參數爲函數名,第二個參數爲傳入函數的參數的迭代器
    pool.close() #關閉進程池,不再接受新的進程
    pool.join() #阻塞父進程直到所有子進程結束後再執行父進程
    print("運行時間:",time.time()-start)

在這裏插入圖片描述
在這裏插入圖片描述

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