Python3網路爬蟲開發實戰,Scrapy 爬取新浪微博

2020-08-11 16:09:43

前面講解了 Scrapy 中各個模組基本使用方法以及代理池、Cookies 池。接下來我們以一個反爬比較強的網站新浪微博爲例,來實現一下 Scrapy 的大規模爬取。

很多人學習python,不知道從何學起。
很多人學習python,掌握了基本語法過後,不知道在哪裏尋找案例上手。
很多已經做案例的人,卻不知道如何去學習更加高深的知識。
那麼針對這三類人,我給大家提供一個好的學習平臺,免費領取視訊教學,電子書籍,以及課程的原始碼!
QQ羣:101677771

1. 本節目標

本次爬取的目標是新浪微博使用者的公開基本資訊,如使用者暱稱、頭像、使用者的關注、粉絲列表以及發佈的微博等,這些資訊抓取之後儲存至 MongoDB。

2. 準備工作

請確保前文所講的代理池、Cookies 池已經實現並可以正常執行,安裝 Scrapy、PyMongo 庫,如沒有安裝可以參考前文內容。

3. 爬取思路

首先我們要實現使用者的大規模爬取。這裏採用的爬取方式是,以微博的幾個大 V 爲起始點,爬取他們各自的粉絲和關注列表,然後獲取粉絲和關注列表的粉絲和關注列表,以此類推,這樣下去就可以實現遞回爬取。如果一個使用者與其他使用者有社羣網路上的關聯,那他們的資訊就會被爬蟲抓取到,這樣我們就可以做到對所有使用者的爬取。通過這種方式,我們可以得到使用者的唯一 ID,再根據 ID 獲取每個使用者發佈的微博即可。

4. 爬取分析

這裏我們選取的爬取站點是:https://m.weibo.cn,此站點是微博行動端的站點。開啓該站點會跳轉到登錄頁面,這是因爲主頁做了登錄限制。不過我們可以直接開啓某個使用者詳情頁面,如圖 13-32 所示。

圖 13-32 個人詳情頁面

我們在頁面最上方可以看到她的關注和粉絲數量。我們點選關注,進入到她的關注列表,如圖 13-33 所示。

圖 13-33 關注列表

我們開啓開發者工具,切換到 XHR 過濾器,一直下拉關注列表,即可看到下方會出現很多 Ajax 請求,這些請求就是獲取關注列表的 Ajax 請求,如圖 13-34 所示。

圖 13-34 請求列表

我們開啓第一個 Ajax 請求看一下,發現它的鏈接爲:https://m.weibo.cn/api/container/getIndex?containerid=231051_-_followers_-_1916655407&luicode=10000011&lfid=1005051916655407&featurecode=20000320&type=uid&value=1916655407&page=2,詳情如圖 13-35 和 13-36 所示。

圖 13-35 請求詳情

圖 13-36 響應結果

請求型別是 GET 型別,返回結果是 JSON 格式,我們將其展開之後即可看到其關注的使用者的基本資訊。接下來我們只需要構造這個請求的參數。此鏈接一共有 7 個參數,如圖 13-37 所示。

圖 13-37 參數資訊

其中最主要的參數就是 containerid 和 page。有了這兩個參數,我們同樣可以獲取請求結果。我們可以將介面精簡爲:https://m.weibo.cn/api/container/getIndex?containerid=231051_-_followers_-_1916655407&page=2,這裏的 containerid 的前半部分是固定的,後半部分是使用者的 id。所以這裏參數就可以構造出來了,只需要修改 containerid 最後的 id 和 page 參數即可獲取分頁形式的關注列表資訊。

利用同樣的方法,我們也可以分析使用者詳情的 Ajax 鏈接、使用者微博列表的 Ajax 鏈接,如下所示:

 

1

2

3

4

5

6

7

8

# 使用者詳情 API

user_url = 'https://m.weibo.cn/api/container/getIndex?uid={uid}&type=uid&value={uid}&containerid=100505{uid}'

# 關注列表 API

follow_url = 'https://m.weibo.cn/api/container/getIndex?containerid=231051_-_followers_-_{uid}&page={page}'

# 粉絲列表 API

fan_url = 'https://m.weibo.cn/api/container/getIndex?containerid=231051_-_fans_-_{uid}&page={page}'

# 微博列表 API

weibo_url = 'https://m.weibo.cn/api/container/getIndex?uid={uid}&type=uid&page={page}&containerid=107603{uid}'

此處的 uid 和 page 分別代表使用者 ID 和分頁頁碼。

注意,這個 API 可能隨着時間的變化或者微博的改版而變化,以實測爲準。

我們從幾個大 V 開始抓取,抓取他們的粉絲、關注列表、微博資訊,然後遞回抓取他們的粉絲和關注列表的粉絲、關注列表、微博資訊,遞回抓取,最後儲存微博使用者的基本資訊、關注和粉絲列表、發佈的微博。

我們選擇 MongoDB 作爲儲存的數據庫,可以更方便地儲存使用者的粉絲和關注列表。

5. 新建專案

接下來,我們用 Scrapy 來實現這個抓取過程。首先建立一個專案,命令如下所示:

 

1

scrapy startproject weibo

進入專案中,新建一個 Spider,名爲 weibocn,命令如下所示:

 

1

scrapy genspider weibocn m.weibo.cn

我們首先修改 Spider,設定各個 Ajax 的 URL,選取幾個大 V,將他們的 ID 賦值成一個列表,實現 start_requests() 方法,也就是依次抓取各個大 V 的個人詳情,然後用 parse_user() 進行解析,如下所示:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

from scrapy import Request, Spider

 

class WeiboSpider(Spider):

    name = 'weibocn'

    allowed_domains = ['m.weibo.cn']

    user_url = 'https://m.weibo.cn/api/container/getIndex?uid={uid}&type=uid&value={uid}&containerid=100505{uid}'

    follow_url = 'https://m.weibo.cn/api/container/getIndex?containerid=231051_-_followers_-_{uid}&page={page}'

    fan_url = 'https://m.weibo.cn/api/container/getIndex?containerid=231051_-_fans_-_{uid}&page={page}'

    weibo_url = 'https://m.weibo.cn/api/container/getIndex?uid={uid}&type=uid&page={page}&containerid=107603{uid}'

    start_users = ['3217179555', '1742566624', '2282991915', '1288739185', '3952070245', '5878659096']

 

    def start_requests(self):

        for uid in self.start_users:

            yield Request(self.user_url.format(uid=uid), callback=self.parse_user)

 

    def parse_user(self, response):

        self.logger.debug(response)

 

6. 建立 Item

接下來,我們解析使用者的基本資訊並生成 Item。這裏我們先定義幾個 Item,如使用者、使用者關係、微博的 Item,如下所示:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

from scrapy import Item, Field

 

class UserItem(Item):

    collection = 'users'

    id = Field()

    name = Field()

    avatar = Field()

    cover = Field()

    gender = Field()

    description = Field()

    fans_count = Field()

    follows_count = Field()

    weibos_count = Field()

    verified = Field()

    verified_reason = Field()

    verified_type = Field()

    follows = Field()

    fans = Field()

    crawled_at = Field()

 

class UserRelationItem(Item):

    collection = 'users'

    id = Field()

    follows = Field()

    fans = Field()

 

class WeiboItem(Item):

    collection = 'weibos'

    id = Field()

    attitudes_count = Field()

    comments_count = Field()

    reposts_count = Field()

    picture = Field()

    pictures = Field()

    source = Field()

    text = Field()

    raw_text = Field()

    thumbnail = Field()

    user = Field()

    created_at = Field()

    crawled_at = Field()

這裏定義了 collection 欄位,指明儲存的 Collection 的名稱。使用者的關注和粉絲列表直接定義爲一個單獨的 UserRelationItem,其中 id 就是使用者的 ID,follows 就是使用者關注列表,fans 是粉絲列表,但這並不意味着我們會將關注和粉絲列表存到一個單獨的 Collection 裡。後面我們會用 Pipeline 對各個 Item 進行處理、合併儲存到使用者的 Collection 裡,因此 Item 和 Collection 並不一定是完全對應的。

7. 提取數據

我們開始解析使用者的基本資訊,實現 parse_user() 方法,如下所示:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

def parse_user(self, response):

    """

    解析使用者資訊

    :param response: Response 物件

    """

    result = json.loads(response.text)

    if result.get('userInfo'):

        user_info = result.get('userInfo')

        user_item = UserItem()

        field_map = {

            'id': 'id', 'name': 'screen_name', 'avatar': 'profile_image_url', 'cover': 'cover_image_phone',

            'gender': 'gender', 'description': 'description', 'fans_count': 'followers_count',

            'follows_count': 'follow_count', 'weibos_count': 'statuses_count', 'verified': 'verified',

            'verified_reason': 'verified_reason', 'verified_type': 'verified_type'

        }

        for field, attr in field_map.items():

            user_item[field] = user_info.get(attr)

        yield user_item

        # 關注

        uid = user_info.get('id')

        yield Request(self.follow_url.format(uid=uid, page=1), callback=self.parse_follows,

                      meta={'page': 1, 'uid': uid})

        # 粉絲

        yield Request(self.fan_url.format(uid=uid, page=1), callback=self.parse_fans,

                      meta={'page': 1, 'uid': uid})

        # 微博

        yield Request(self.weibo_url.format(uid=uid, page=1), callback=self.parse_weibos,

                      meta={'page': 1, 'uid': uid})

在這裏我們一共完成了兩個操作。

  • 解析 JSON 提取使用者資訊並生成 UserItem 返回。我們並沒有採用常規的逐個賦值的方法,而是定義了一個欄位對映關係。我們定義的欄位名稱可能和 JSON 中使用者的欄位名稱不同,所以在這裏定義成一個字典,然後遍歷字典的每個欄位實現逐個欄位的賦值。
  • 構造使用者的關注、粉絲、微博的第一頁的鏈接,並生成 Request,這裏需要的參數只有使用者的 ID。另外,初始分頁頁碼直接設定爲 1 即可。

接下來,我們還需要儲存使用者的關注和粉絲列表。以關注列表爲例,其解析方法爲 parse_follows(),實現如下所示:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

def parse_follows(self, response):

    """

    解析使用者關注

    :param response: Response 物件

    """

    result = json.loads(response.text)

    if result.get('ok') and result.get('cards') and len(result.get('cards')) and result.get('cards')[-1].get('card_group'):

        # 解析使用者

        follows = result.get('cards')[-1].get('card_group')

        for follow in follows:

            if follow.get('user'):

                uid = follow.get('user').get('id')

                yield Request(self.user_url.format(uid=uid), callback=self.parse_user)

        # 關注列表

        uid = response.meta.get('uid')

        user_relation_item = UserRelationItem()

        follows = [{'id': follow.get('user').get('id'), 'name': follow.get('user').get('screen_name')} for follow in

                   follows]

        user_relation_item['id'] = uid

        user_relation_item['follows'] = follows

        user_relation_item['fans'] = []

        yield user_relation_item

        # 下一頁關注

        page = response.meta.get('page') + 1

        yield Request(self.follow_url.format(uid=uid, page=page),

                      callback=self.parse_follows, meta={'page': page, 'uid': uid})

那麼在這個方法裏面我們做瞭如下三件事。

  • 解析關注列表中的每個使用者資訊併發起新的解析請求。我們首先解析關注列表的資訊,得到使用者的 ID,然後再利用 user_url 構造存取使用者詳情的 Request,回撥就是剛纔所定義的 parse_user() 方法。
  • 提取使用者關注列表內的關鍵資訊並生成 UserRelationItem。id 欄位直接設定成使用者的 ID,JSON 返回數據中的使用者資訊有很多冗餘欄位。在這裏我們只提取了關注使用者的 ID 和使用者名稱,然後把它們賦值給 follows 欄位,fans 欄位設定成空列表。這樣我們就建立了一個存有使用者 ID 和使用者部分關注列表的 UserRelationItem,之後合併且儲存具有同一個 ID 的 UserRelationItem 的關注和粉絲列表。
  • 提取下一頁關注。只需要將此請求的分頁頁碼加 1 即可。分頁頁碼通過 Request 的 meta 屬性進行傳遞,Response 的 meta 來接收。這樣我們構造並返回下一頁的關注列表的 Request。

抓取粉絲列表的原理和抓取關注列表原理相同,在此不再贅述。

接下來我們還差一個方法的實現,即 parse_weibos(),它用來抓取使用者的微博資訊,實現如下所示:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

def parse_weibos(self, response):

    """

    解析微博列表

    :param response: Response 物件

    """

    result = json.loads(response.text)

    if result.get('ok') and result.get('cards'):

        weibos = result.get('cards')

        for weibo in weibos:

            mblog = weibo.get('mblog')

            if mblog:

                weibo_item = WeiboItem()

                field_map = {

                    'id': 'id', 'attitudes_count': 'attitudes_count', 'comments_count': 'comments_count', 'created_at': 'created_at',

                    'reposts_count': 'reposts_count', 'picture': 'original_pic', 'pictures': 'pics',

                    'source': 'source', 'text': 'text', 'raw_text': 'raw_text', 'thumbnail': 'thumbnail_pic'

                }

                for field, attr in field_map.items():

                    weibo_item[field] = mblog.get(attr)

                weibo_item['user'] = response.meta.get('uid')

                yield weibo_item

        # 下一頁微博

        uid = response.meta.get('uid')

        page = response.meta.get('page') + 1

        yield Request(self.weibo_url.format(uid=uid, page=page), callback=self.parse_weibos,

                      meta={'uid': uid, 'page': page})

這裏 parse_weibos() 方法完成了兩件事。

  • 提取使用者的微博資訊,並生成 WeiboItem。這裏同樣建立了一個欄位對映表,實現批次欄位賦值。
  • 提取下一頁的微博列表。這裏同樣需要傳入使用者 ID 和分頁頁碼。

到目前爲止,微博的 Spider 已經完成。後面還需要對數據進行數據清洗儲存,以及對接代理池、Cookies 池來防止反爬蟲。

8. 數據清洗

有些微博的時間可能不是標準的時間,比如它可能顯示爲剛剛、幾分鐘前、幾小時前、昨天等。這裏我們需要統一轉化這些時間,實現一個 parse_time() 方法,如下所示:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

def parse_time(self, date):

    if re.match(' 剛剛 ', date):

        date = time.strftime('% Y-% m-% d % H:% M', time.localtime(time.time()))

    if re.match('d + 分鐘前 ', date):

        minute = re.match('(d+)', date).group(1)

        date = time.strftime('% Y-% m-% d % H:% M', time.localtime(time.time() - float(minute) * 60))

    if re.match('d + 小時前 ', date):

        hour = re.match('(d+)', date).group(1)

        date = time.strftime('% Y-% m-% d % H:% M', time.localtime(time.time() - float(hour) * 60 * 60))

    if re.match(' 昨天.*', date):

        date = re.match(' 昨天 (.*)', date).group(1).strip()

        date = time.strftime('% Y-% m-% d', time.localtime() - 24 * 60 * 60) + ' ' + date

    if re.match('d{2}-d{2}', date):

        date = time.strftime('% Y-', time.localtime()) + date + ' 00:00'

    return date

我們用正則來提取一些關鍵數位,用 time 庫來實現標準時間的轉換。

以 X 分鐘前的處理爲例,爬取的時間會賦值爲 created_at 欄位。我們首先用正則匹配這個時間,表達式寫作 d + 分鐘前,如果提取到的時間符合這個表達式,那麼就提取出其中的數位,這樣就可以獲取分鐘數了。接下來使用 time 模組的 strftime() 方法,第一個參數傳入要轉換的時間格式,第二個參數就是時間戳。這裏我們用當前的時間戳減去此分鐘數乘以 60 就是當時的時間戳,這樣我們就可以得到格式化後的正確時間了。

然後 Pipeline 可以實現如下處理:

 

1

2

3

4

5

6

class WeiboPipeline():

    def process_item(self, item, spider):

        if isinstance(item, WeiboItem):

            if item.get('created_at'):

                item['created_at'] = item['created_at'].strip()

                item['created_at'] = self.parse_time(item.get('created_at'))

我們在 Spider 裡沒有對 crawled_at 欄位賦值,它代表爬取時間,我們可以統一將其賦值爲當前時間,實現如下所示:

 

1

2

3

4

5

6

class TimePipeline():

    def process_item(self, item, spider):

        if isinstance(item, UserItem) or isinstance(item, WeiboItem):

            now = time.strftime('% Y-% m-% d % H:% M', time.localtime())

            item['crawled_at'] = now

        return item

這裏我們判斷了 item 如果是 UserItem 或 WeiboItem 型別,那麼就給它的 crawled_at 欄位賦值爲當前時間。

通過上面的兩個 Pipeline,我們便完成了數據清洗工作,這裏主要是時間的轉換。

9. 數據儲存

數據清洗完畢之後,我們就要將數據儲存到 MongoDB 數據庫。我們在這裏實現 MongoPipeline 類,如下所示:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

import pymongo

 

class MongoPipeline(object):

    def __init__(self, mongo_uri, mongo_db):

        self.mongo_uri = mongo_uri

        self.mongo_db = mongo_db

 

    @classmethod

    def from_crawler(cls, crawler):

        return cls(mongo_uri=crawler.settings.get('MONGO_URI'), mongo_db=crawler.settings.get('MONGO_DATABASE')

        )

 

    def open_spider(self, spider):

        self.client = pymongo.MongoClient(self.mongo_uri)

        self.db = self.client[self.mongo_db]

        self.db[UserItem.collection].create_index([('id', pymongo.ASCENDING)])

        self.db[WeiboItem.collection].create_index([('id', pymongo.ASCENDING)])

 

    def close_spider(self, spider):

        self.client.close()

 

    def process_item(self, item, spider):

        if isinstance(item, UserItem) or isinstance(item, WeiboItem):

            self.db[item.collection].update({'id': item.get('id')}, {'$set': item}, True)

        if isinstance(item, UserRelationItem):

            self.db[item.collection].update({'id': item.get('id')},

                {'$addToSet':

                    {'follows': {'$each': item['follows']},

                        'fans': {'$each': item['fans']}

                    }

                }, True)

        return item

當前的 MongoPipeline 和前面我們所寫的有所不同,主要有以下幾點。

  • 在 open_spider() 方法裏面新增了 Collection 的索引,在這裏爲兩個 Item 都做了索引,索引的欄位是 id,由於我們這次是大規模爬取,同時在爬取過程中涉及到數據的更新問題,所以我們爲每個 Collection 建立了索引,建立了索引之後可以大大提高檢索效率。
  • 在 process_item() 方法裡儲存使用的是 update() 方法,第一個參數是查詢條件,第二個參數是爬取的 Item,這裏我們使用了 操作符,這樣我們如果爬取到了重複的數據即可對數據進行更新,同時不會刪除已存在的欄位,如果這裏不加set操作符,這樣我們如果爬取到了重複的數據即可對數據進行更新,同時不會刪除已存在的欄位,如果這裏不加set 操作符,那麼會直接進行 item 替換,這樣可能會導致已存在的欄位如關注和粉絲列表清空,所以這裏必須要加上 $set 操作符。第三個參數我們設定爲了 True,這個參數起到的作用是如果數據不存在,則插入數據。這樣我們就可以做到數據存在即更新、數據不存在即插入,這樣就達到了去重的效果。
  • 對於使用者的關注和粉絲列表,我們在這裏使用了一個新的操作符,叫做 ,這個操作符可以向列表型別的欄位插入數據同時去重,接下來它的值就是需要操作的欄位名稱,我們在這裏又利用了addToSet,這個操作符可以向列表型別的欄位插入數據同時去重,接下來它的值就是需要操作的欄位名稱,我們在這裏又利用了each 操作符對需要插入的列表數據進行了遍歷,這樣就可以逐條插入使用者的關注或粉絲數據到指定的欄位了,關於該操作更多的解釋可以參考 MongoDB 的官方文件,鏈接爲:https://docs.mongodb.com/manual/reference/operator/update/addToSet/

10. Cookies 池對接

新浪微博的反爬能力非常強,我們需要做一些防範反爬蟲的措施纔可以順利完成數據爬取。

如果沒有登錄而直接請求微博的 API 介面,這非常容易導致 403 狀態碼。這個情況我們在 10.2 節也提過。所以在這裏我們實現一個 Middleware,爲每個 Request 新增隨機的 Cookies。

我們先開啓 Cookies 池,使 API 模組正常執行。例如在本地執行 5000 埠,存取:http://localhost:5000/weibo/random 即可獲取隨機的 Cookies,當然也可以將 Cookies 池部署到遠端的伺服器,這樣只需要更改一下存取的鏈接就好了。

那麼在這裏我們將 Cookies 池在本地啓動起來,再實現一個 Middleware 如下:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

 

class CookiesMiddleware():

    def __init__(self, cookies_url):

        self.logger = logging.getLogger(__name__)

        self.cookies_url = cookies_url

 

    def get_random_cookies(self):

        try:

            response = requests.get(self.cookies_url)

            if response.status_code == 200:

                cookies = json.loads(response.text)

                return cookies

        except requests.ConnectionError:

            return False

 

    def process_request(self, request, spider):

        self.logger.debug(' 正在獲取 Cookies')

        cookies = self.get_random_cookies()

        if cookies:

            request.cookies = cookies

            self.logger.debug(' 使用 Cookies ' + json.dumps(cookies))

 

    @classmethod

    def from_crawler(cls, crawler):

        settings = crawler.settings

        return cls(cookies_url=settings.get('COOKIES_URL')

        )

我們首先利用 from_crawler() 方法獲取了 COOKIES_URL 變數,它定義在 settings.py 裡,這就是剛纔我們所說的介面。接下來實現 get_random_cookies() 方法,這個方法主要就是請求此 Cookies 池介面並獲取介面返回的隨機 Cookies。如果成功獲取,則返回 Cookies;否則返回 False。

接下來,在 process_request() 方法裡,我們給 request 物件的 cookies 屬性賦值,其值就是獲取的隨機 Cookies,這樣我們就成功地爲每一次請求賦值 Cookies 了。

如果啓用了該 Middleware,每個請求都會被賦值隨機的 Cookies。這樣我們就可以模擬登錄之後的請求,403 狀態碼基本就不會出現。

11. 代理池對接

微博還有一個反爬措施就是,檢測到同一 IP 請求量過大時就會出現 414 狀態碼。如果遇到這樣的情況可以切換代理。例如,在本地 5555 埠執行,獲取隨機可用代理的地址爲:http://localhost:5555/random,存取這個介面即可獲取一個隨機可用代理。接下來我們再實現一個 Middleware,程式碼如下所示:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

class ProxyMiddleware():

    def __init__(self, proxy_url):

        self.logger = logging.getLogger(__name__)

        self.proxy_url = proxy_url

 

    def get_random_proxy(self):

        try:

            response = requests.get(self.proxy_url)

            if response.status_code == 200:

                proxy = response.text

                return proxy

        except requests.ConnectionError:

            return False

 

    def process_request(self, request, spider):

        if request.meta.get('retry_times'):

            proxy = self.get_random_proxy()

            if proxy:

                uri = 'https://{proxy}'.format(proxy=proxy)

                self.logger.debug(' 使用代理 ' + proxy)

                request.meta['proxy'] = uri

 

    @classmethod

    def from_crawler(cls, crawler):

        settings = crawler.settings

        return cls(proxy_url=settings.get('PROXY_URL')

        )

同樣的原理,我們實現了一個 get_random_proxy() 方法用於請求代理池的介面獲取隨機代理。如果獲取成功,則返回改代理,否則返回 False。在 process_request() 方法中,我們給 request 物件的 meta 屬性賦值一個 proxy 欄位,該欄位的值就是代理。

另外,賦值代理的判斷條件是當前 retry_times 不爲空,也就是說第一次請求失敗之後才啓用代理,因爲使用代理後存取速度會慢一些。所以我們在這裏設定了只有重試的時候才啓用代理,否則直接請求。這樣就可以保證在沒有被封禁的情況下直接爬取,保證了爬取速度。

12. 啓用 Middleware

接下來,我們在組態檔中啓用這兩個 Middleware,修改 settings.py 如下所示:

 

1

2

3

4

DOWNLOADER_MIDDLEWARES = {

    'weibo.middlewares.CookiesMiddleware': 554,

    'weibo.middlewares.ProxyMiddleware': 555,

}

注意這裏的優先順序設定,前文提到了 Scrapy 的預設 Downloader Middleware 的設定如下:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

{

    'scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware': 100,

    'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware': 300,

    'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware': 350,

    'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware': 400,

    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 500,

    'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550,

    'scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware': 560,

    'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware': 580,

    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 590,

    'scrapy.downloadermiddlewares.redirect.RedirectMiddleware': 600,

    'scrapy.downloadermiddlewares.cookies.CookiesMiddleware': 700,

    'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 750,

    'scrapy.downloadermiddlewares.stats.DownloaderStats': 850,

    'scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware': 900,

}

要使得我們自定義的 CookiesMiddleware 生效,它在內建的 CookiesMiddleware 之前呼叫。內建的 CookiesMiddleware 的優先順序爲 700,所以這裏我們設定一個比 700 小的數位即可。

要使得我們自定義的 ProxyMiddleware 生效,它在內建的 HttpProxyMiddleware 之前呼叫。內建的 HttpProxyMiddleware 的優先順序爲 750,所以這裏我們設定一個比 750 小的數位即可。

13. 執行

到此爲止,整個微博爬蟲就實現完畢了,我們執行如下命令啓動一下爬蟲:

 

1

scrapy crawl weibocn

類似的輸出結果如下:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

2017-07-11 17:27:34 [urllib3.connectionpool] DEBUG: http://localhost:5000 "GET /weibo/random HTTP/1.1" 200 339

2017-07-11 17:27:34 [weibo.middlewares] DEBUG: 使用 Cookies {"SCF": "AhzwTr_DxIGjgri_dt46_DoPzUqq-PSupu545JdozdHYJ7HyEb4pD3pe05VpbIpVyY1ciKRRWwUgojiO3jYwlBE.", "_T_WM": "8fe0bc1dad068d09b888d8177f1c1218", "SSOLoginState": "1501496388", "M_WEIBOCN_PARAMS": "uicode%3D20000174", "SUHB": "0tKqV4asxqYl4J", "SUB": "_2A250e3QUDeRhGeBM6VYX8y7NwjiIHXVXhBxcrDV6PUJbkdBeLXjckW2fUT8MWloekO4FCWVlIYJGJdGLnA.."}

2017-07-11 17:27:34 [weibocn] DEBUG: <200 https://m.weibo.cn/api/container/getIndex?uid=1742566624&type=uid&value=1742566624&containerid=1005051742566624>

2017-07-11 17:27:34 [scrapy.core.scraper] DEBUG: Scraped from <200 https://m.weibo.cn/api/container/getIndex?uid=1742566624&type=uid&value=1742566624&containerid=1005051742566624>

{'avatar': 'https://tva4.sinaimg.cn/crop.0.0.180.180.180/67dd74e0jw1e8qgp5bmzyj2050050aa8.jpg',

'cover': 'https://tva3.sinaimg.cn/crop.0.0.640.640.640/6ce2240djw1e9oaqhwllzj20hs0hsdir.jpg',

'crawled_at': '2017-07-11 17:27',

'description': ' 成長,就是一個不斷覺得以前的自己是個傻逼的過程 ',

'fans_count': 19202906,

'follows_count': 1599,

'gender': 'm',

'id': 1742566624,

'name': ' 思想聚焦 ',

'verified': True,

'verified_reason': ' 微博知名博主,校導網編輯 ',

'verified_type': 0,

'weibos_count': 58393}

執行一段時間後,我們便可以到 MongoDB 數據庫檢視數據,爬取下來的數據如圖 13-38 和圖 13-39 所示。

圖 13-38 使用者資訊

圖 13-39 微博資訊

針對使用者資訊,我們不僅爬取了其基本資訊,還把關注和粉絲列表加到了 follows 和 fans 欄位並做了去重操作。針對微博資訊,我們成功進行了時間轉換處理,同時還儲存了微博的圖片列表資訊。

14. 本節程式碼

本節程式碼地址:https://github.com/Python3WebSpider/Weibo

15. 結語

本節實現了新浪微博的使用者及其粉絲關注列表和微博資訊的爬取,還對接了 Cookies 池和代理池來處理反爬蟲。不過現在是針對單機的爬取,後面我們會將此專案修改爲分佈式爬蟲,以進一步提高抓取效率。