Python網路爬蟲2 - 爬取新浪微博使用者圖片

litreily發表於2018-04-10

該部落格首發於 www.litreily.top

其實,新浪微博使用者圖片爬蟲是我學習python以來寫的第一個爬蟲,只不過當時懶,後來爬完Lofter後覺得有必要總結一下,所以就有了第一篇爬蟲部落格。現在暫時閒下來了,準備把新浪的這個也補上。

言歸正傳,既然選擇爬新浪微博,那當然是有需求的,這也是學習的主要動力之一,沒錯,就是美圖。sina使用者多數微博都是包含圖片的,而且是組圖居多,單個圖片的較少。

為了避免侵權,本文以本人微博litreily為例說明整個爬取過程,雖然圖片較少,質量較低,但爬取方案是絕對ok的,使用時只要換個使用者ID就可以了。

分析sina站點

獲取使用者ID

在爬取前,我們需要知道的是每個使用者都有一個使用者名稱,而一個使用者名稱又對應一個唯一的整型數字ID,類似於學生的學號,本人的是2657006573。至於怎麼根據使用者名稱去獲取ID,有以下兩種方法:

  1. 進入待爬取使用者主頁,在瀏覽器網址欄中即可看到一串資料,那就是使用者ID
  2. Ctrl-U檢視待爬取使用者的原始碼,搜尋"uid,注意是雙引號

其實是可以在已知使用者名稱的情況下通過爬蟲自動獲取到uid的,但是我當時初學python,並沒有考慮充分,所以後面的原始碼是以使用者ID作為輸入引數的。

圖片儲存引數解析

使用者所有的圖片都被存放至這樣的路徑下,真的是所有圖片哦!!!

https://weibo.cn/{uid}/profile?filter={filter_type}&page={page_num}

# example
https://weibo.cn/2657006573/profile?filter=0&page=1
uid: 2657006573
filter_type: 0
page_num: 1
複製程式碼

注意,是weibo.cn而不是weibo.com,至於我是怎麼找到這個頁面的,說實話,我也忘了。。。

連結中包含3個引數,uid, filter_mode 以及 page_num。其中,uid就是前面提及的使用者ID,page_num也很好理解,就是分頁的當前頁數,從1開始增加,那麼,這個filter_mode是什麼呢?

不著急,我們先來看看頁面↓

filter mode of pictures

可以看到,濾波型別filter_mode指的就是篩選條件,一共三個:

  1. filter=0 全部微博(包含純文字微博,轉載微博)
  2. filter=1 原創微博(包含純文字微博)
  3. filter=2 圖片微博(必須含有圖片,包含轉載)

我通常會選擇原創,因為我並不希望爬取結果中包含轉載微博中的圖片。當然,大家依照自己的需要選擇即可。

圖鏈解析

好了,引數來源都知道了,我們回過頭看看這個網頁。頁面是不是感覺就是個空架子?毫無css痕跡,沒關係,新浪本來就沒打算把這個頁面主動呈現給使用者。但對於爬蟲而言,這卻是極好的,為什麼這麼說?原因如下:

  1. 圖片齊全,沒有遺漏,就是個視覺化的資料庫
  2. 樣式少,頁面簡單,省流量,爬取快
  3. 靜態網頁,分頁儲存,所見即所得
  4. 原始碼包含了所有微博的首圖組圖連結

這樣的網頁用來練手再合適不過。但要注意的是上面第4點,什麼是首圖組圖連結呢,很好理解。每篇部落格可能包含多張圖片,那就是組圖,但該頁面只顯示部落格的第一張圖片,即所謂的首圖組圖連結指向的是儲存著該組圖所有圖片的網址。

由於本人微博沒組圖,所以此處以劉亦菲微博為例,說明單圖及組圖的圖鏈格式

pictures

圖中的上面一篇微博只有一張圖片,可以輕易獲取到原圖連結,注意是原圖,因為我們在頁面能看到的是縮圖,但要爬取的當然是原圖啦。

圖中下面的微博包含組圖,在圖片右側的Chrome開發工具可以看到組圖連結。

https://weibo.cn/mblog/picAll/FCQefgeAr?rl=2

開啟組圖連結,可以看到圖片如下圖所示:

picture's url

可以看到縮圖連結以及原圖連結,然後我們點選原圖看一下。

picture's origin url

可以發現,彈出頁面的連結與上圖顯示的不同,但與上圖中的縮圖連結極為相似。它們分別是:

  1. 縮圖:http://ww1.sinaimg.cn/thumb180/c260f7ably1fn4vd7ix0qj20rs1aj1kx.jpg
  2. 原圖: http://wx1.sinaimg.cn/large/c260f7ably1fn4vd7ix0qj20rs1aj1kx.jpg

可以看出,只是一個thumb180large的區別。既然發現了規律,那就好辦多了,我們只要知道縮圖的網址,就可以將域名後的第一級子域名替換成large就可以了,而不用獲取原圖連結再跳轉一次。

而且,多次嘗試可以發現組圖連結及縮圖連結滿足正規表示式:

# 1. 組圖連結:
imglist_reg = r'href="(https://weibo.cn/mblog/picAll/.{9}\?rl=2)"'

# 2. 縮圖
img_reg = r'src="(http://w.{2}\.sinaimg.cn/(.{6,8})/.{32,33}.(jpg|gif))"'
複製程式碼

到此,新浪微博的解析過程就結束了,圖鏈的格式以及獲取方式也都清楚了。下面就可以設計方案進行爬取了。

確定爬取方案

根據解析結果,很容易制定出以下爬取方案:

  1. 給定微博使用者名稱litreily
  2. 進入待爬取使用者主頁,即可從網址中獲取uid: 2657006573
  3. 獲取本人登入微博後的cookies(請求報文需要用到cookies
  4. 逐一爬取 https://weibo.cn/2657006573/profile?filter=0&page={1,2,3,...}
  5. 解析每一頁的原始碼,獲取單圖連結及組圖連結,
  • 單圖:直接獲取該圖縮圖連結;
  • 組圖:爬取組圖連結,迴圈獲取組圖頁面所有圖片的縮圖連結
  1. 迴圈將第5步獲取到的圖鏈替換為原圖連結,並下載至本地
  2. 重複第4-6步,直至沒有圖片

獲取cookies

針對以上方案,其中有幾個重點內容,其一就是cookies的獲取,我暫時還沒學怎麼自動獲取cookies,所以目前是登入微博後手動獲取的。

get cookies

下載網頁

下載網頁用的是python3自帶的urllib庫,當時沒學requests,以後可能也很少用urllib了。

def _get_html(url, headers):
    try:
        req = urllib.request.Request(url, headers = headers)
        page = urllib.request.urlopen(req)
        html = page.read().decode('UTF-8')
    except Exception as e:
        print("get %s failed" % url)
        return None
    return html
複製程式碼

獲取儲存路徑

由於我是在win10下編寫的程式碼,但是個人比較喜歡用bash,所以圖片的儲存路徑有以下兩種格式,_get_path函式會自動判斷當前作業系統的型別,然後選擇相應的路徑。

def _get_path(uid):
    path = {
        'Windows': 'D:/litreily/Pictures/python/sina/' + uid,
        'Linux': '/mnt/d/litreily/Pictures/python/sina/' + uid
    }.get(platform.system())

    if not os.path.isdir(path):
        os.makedirs(path)
    return path
複製程式碼

幸好windows是相容linux系統的斜槓符號的,不然程式中的相對路徑替換還挺麻煩。

下載圖片

由於選用的urllib庫,所以下載圖片就使用urllib.request.urlretrieve

# image url of one page is saved in imgurls
for img in imgurls:
    imgurl = img[0].replace(img[1], 'large')
    num_imgs += 1
    try:
        urllib.request.urlretrieve(imgurl, '{}/{}.{}'.format(path, num_imgs, img[2]))
        # display the raw url of images
        print('\t%d\t%s' % (num_imgs, imgurl))
    except Exception as e:
        print(str(e))
        print('\t%d\t%s failed' % (num_imgs, imgurl))
複製程式碼

原始碼

其它細節詳見原始碼

#!/usr/bin/python3
# -*- coding:utf-8 -*-
# author: litreily
# date: 2018.02.05
"""Capture pictures from sina-weibo with user_id."""

import re
import os
import platform

import urllib
import urllib.request

from bs4 import BeautifulSoup


def _get_path(uid):
    path = {
        'Windows': 'D:/litreily/Pictures/python/sina/' + uid,
        'Linux': '/mnt/d/litreily/Pictures/python/sina/' + uid
    }.get(platform.system())

    if not os.path.isdir(path):
        os.makedirs(path)
    return path


def _get_html(url, headers):
    try:
        req = urllib.request.Request(url, headers = headers)
        page = urllib.request.urlopen(req)
        html = page.read().decode('UTF-8')
    except Exception as e:
        print("get %s failed" % url)
        return None
    return html


def _capture_images(uid, headers, path):
    filter_mode = 1      # 0-all 1-original 2-pictures
    num_pages = 1
    num_blogs = 0
    num_imgs = 0

    # regular expression of imgList and img
    imglist_reg = r'href="(https://weibo.cn/mblog/picAll/.{9}\?rl=2)"'
    imglist_pattern = re.compile(imglist_reg)
    img_reg = r'src="(http://w.{2}\.sinaimg.cn/(.{6,8})/.{32,33}.(jpg|gif))"'
    img_pattern = re.compile(img_reg)
    
    print('start capture picture of uid:' + uid)
    while True:
        url = 'https://weibo.cn/%s/profile?filter=%s&page=%d' % (uid, filter_mode, num_pages)

        # 1. get html of each page url
        html = _get_html(url, headers)
        
        # 2. parse the html and find all the imgList Url of each page
        soup = BeautifulSoup(html, "html.parser")
        # <div class="c" id="M_G4gb5pY8t"><div>
        blogs = soup.body.find_all(attrs={'id':re.compile(r'^M_')}, recursive=False)
        num_blogs += len(blogs)

        imgurls = []        
        for blog in blogs:
            blog = str(blog)
            imglist_url = imglist_pattern.findall(blog)
            if not imglist_url:
                # 2.1 get img-url from blog that have only one pic
                imgurls += img_pattern.findall(blog)
            else:
                # 2.2 get img-urls from blog that have group pics
                html = _get_html(imglist_url[0], headers)
                imgurls += img_pattern.findall(html)

        if not imgurls:
            print('capture complete!')
            print('captured pages:%d, blogs:%d, imgs:%d' % (num_pages, num_blogs, num_imgs))
            print('directory:' + path)
            break

        # 3. download all the imgs from each imgList
        print('PAGE %d with %d images' % (num_pages, len(imgurls)))
        for img in imgurls:
            imgurl = img[0].replace(img[1], 'large')
            num_imgs += 1
            try:
                urllib.request.urlretrieve(imgurl, '{}/{}.{}'.format(path, num_imgs, img[2]))
                # display the raw url of images
                print('\t%d\t%s' % (num_imgs, imgurl))
            except Exception as e:
                print(str(e))
                print('\t%d\t%s failed' % (num_imgs, imgurl))
        num_pages += 1
        print('')


def main():
    # uids = ['2657006573','2173752092','3261134763','2174219060']
    uid = '2657006573'
    path = _get_path(uid)

    # cookie is form the above url->network->request headers
    cookies = ''
    headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
            'Cookie': cookies}

    # capture imgs from sina
    _capture_images(uid, headers, path)


if __name__ == '__main__':
    main()

複製程式碼

使用時記得修改main函式中的cookiesuid

爬取測試

capture litreily

capture litreily end

captured pictures

寫在最後

  • 該爬蟲已存放至開源專案capturer,歡迎交流
  • 由於是首個爬蟲,所以許多地方有待改進,相對的LOFTER爬蟲就更嫻熟寫了
  • 目前沒有發現新浪微博有明顯的反爬措施,但還是按需索取為好

相關文章