1. 程式人生 > >Scrapy筆記(11)- 模擬登入

Scrapy筆記(11)- 模擬登入

有時候爬取網站的時候需要登入,在Scrapy中可以通過模擬登入儲存cookie後再去爬取相應的頁面。這裡我通過登入github然後爬取自己的issue列表來演示下整個原理。

要想實現登入就需要表單提交,先通過瀏覽器訪問github的登入頁面https://github.com/login,然後使用瀏覽器除錯工具來得到登入時需要提交什麼東西。

我這裡使用chrome瀏覽器的除錯工具,F12開啟後選擇Network,並將Preserve log勾上。我故意輸入錯誤的使用者名稱和密碼,得到它提交的form表單引數還有POST提交的UR

去檢視html原始碼會發現表單裡面有個隱藏的authenticity_token

值,這個是需要先獲取然後跟使用者名稱和密碼一起提交的。

重寫start_requests方法

要使用cookie,第一步得開啟它呀,預設scrapy使用CookiesMiddleware中介軟體,並且打開了。如果你之前禁止過,請設定如下

COOKIES_ENABLES = True

我們先要開啟登入頁面,獲取authenticity_token值,這裡我重寫了start_requests方法

# 重寫了爬蟲類的方法, 實現了自定義請求, 執行成功後會呼叫callback回撥函式
def start_requests(self):
    return [Request("https://github.com/login",
                    meta={'cookiejar': 1}, callback=self.post_login)]

# FormRequeset
def post_login(self, response):
    # 先去拿隱藏的表單引數authenticity_token
    authenticity_token = response.xpath(
        '//input[@name="authenticity_token"]/@value').extract_first()
    logging.info('authenticity_token=' + authenticity_token)
    pass

start_requests方法指定了回撥函式,用來獲取隱藏表單值authenticity_token,同時我們還給Request指定了cookiejar的元資料,用來往回調函式傳遞cookie標識。

使用FormRequest

Scrapy為我們準備了FormRequest類專門用來進行Form表單提交的

# 為了模擬瀏覽器,我們定義httpheader
post_headers = {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "zh-CN,zh;q=0.8,en;q=0.6",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "Content-Type": "application/x-www-form-urlencoded",
    "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.75 Safari/537.36",
    "Referer": "https://github.com/",
}
# 使用FormRequeset模擬表單提交
def post_login(self, response):
    # 先去拿隱藏的表單引數authenticity_token
    authenticity_token = response.xpath(
        '//input[@name="authenticity_token"]/@value').extract_first()
    logging.info('authenticity_token=' + authenticity_token)
    # FormRequeset.from_response是Scrapy提供的一個函式, 用於post表單
    # 登陸成功後, 會呼叫after_login回撥函式,如果url跟Request頁面的一樣就省略掉
    return [FormRequest.from_response(response,
                                      url='https://github.com/session',
                                      meta={'cookiejar': response.meta['cookiejar']},
                                      headers=self.post_headers,  # 注意此處的headers
                                      formdata={
                                          'utf8': '✓',
                                          'login': 'yidao620c',
                                          'password': '******',
                                          'authenticity_token': authenticity_token
                                      },
                                      callback=self.after_login,
                                      dont_filter=True
                                      )]

def after_login(self, response):
    pass

FormRequest.from_response()方法讓你指定提交的url,請求頭還有form表單值,注意我們還通過meta傳遞了cookie標識。它同樣有個回撥函式,登入成功後呼叫。下面我們來實現它

def after_login(self, response):
    # 登入之後,開始進入我要爬取的私信頁面
    for url in self.start_urls:
        # 因為我們上面定義了Rule,所以只需要簡單的生成初始爬取Request即可
        yield Request(url, meta={'cookiejar': response.meta['cookiejar']})

這裡我通過start_urls定義了開始頁面,然後生成Request,具體爬取的規則和下一頁規則在前面的Rule裡定義了。注意這裡我繼續傳遞cookiejar,訪問初始頁面時帶上cookie資訊。

重寫_requests_to_follow

有個問題剛開始困擾我很久就是這裡我定義的spider繼承自CrawlSpider,它內部自動去下載匹配的連結,而每次去訪問連結的時候並沒有自動帶上cookie,後來我重寫了它的_requests_to_follow()方法解決了這個問題

def _requests_to_follow(self, response):
    """重寫加入cookiejar的更新"""
    if not isinstance(response, HtmlResponse):
        return
    seen = set()
    for n, rule in enumerate(self._rules):
        links = [l for l in rule.link_extractor.extract_links(response) if l not in seen]
        if links and rule.process_links:
            links = rule.process_links(links)
        for link in links:
            seen.add(link)
            r = Request(url=link.url, callback=self._response_downloaded)
            # 下面這句是我重寫的
            r.meta.update(rule=n, link_text=link.text, cookiejar=response.meta['cookiejar'])
            yield rule.process_request(r)

頁面處理方法

在規則Rule裡面我定義了每個連結的回撥函式parse_page,就是最終我們處理每個issue頁面提取資訊的邏輯

def parse_page(self, response):
    """這個是使用LinkExtractor自動處理連結以及`下一頁`"""
    logging.info(u'--------------訊息分割線-----------------')
    logging.info(response.url)
    issue_title = response.xpath(
        '//span[@class="js-issue-title"]/text()').extract_first()
    logging.info(u'issue_title:' + issue_title.encode('utf-8'))

完整原始碼

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
Topic: 登入爬蟲
Desc : 模擬登入https://github.com後將自己的issue全部爬出來
tips:使用chrome除錯post表單的時候勾選Preserve log和Disable cache
"""
import logging
import re
import sys
import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from scrapy.http import Request, FormRequest, HtmlResponse

logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S',
                    handlers=[logging.StreamHandler(sys.stdout)])


class GithubSpider(CrawlSpider):
    name = "github"
    allowed_domains = ["github.com"]
    start_urls = [
        'https://github.com/issues',
    ]
    rules = (
        # 訊息列表
        Rule(LinkExtractor(allow=('/issues/\d+',),
                           restrict_xpaths='//ul[starts-with(@class, "table-list")]/li/div[2]/a[2]'),
             callback='parse_page'),
        # 下一頁, If callback is None follow defaults to True, otherwise it defaults to False
        Rule(LinkExtractor(restrict_xpaths='//a[@class="next_page"]')),
    )
    post_headers = {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Encoding": "gzip, deflate",
        "Accept-Language": "zh-CN,zh;q=0.8,en;q=0.6",
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
        "Content-Type": "application/x-www-form-urlencoded",
        "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.75 Safari/537.36",
        "Referer": "https://github.com/",
    }

    # 重寫了爬蟲類的方法, 實現了自定義請求, 執行成功後會呼叫callback回撥函式
    def start_requests(self):
        return [Request("https://github.com/login",
                        meta={'cookiejar': 1}, callback=self.post_login)]

    # FormRequeset
    def post_login(self, response):
        # 先去拿隱藏的表單引數authenticity_token
        authenticity_token = response.xpath(
            '//input[@name="authenticity_token"]/@value').extract_first()
        logging.info('authenticity_token=' + authenticity_token)
        # FormRequeset.from_response是Scrapy提供的一個函式, 用於post表單
        # 登陸成功後, 會呼叫after_login回撥函式,如果url跟Request頁面的一樣就省略掉
        return [FormRequest.from_response(response,
                                          url='https://github.com/session',
                                          meta={'cookiejar': response.meta['cookiejar']},
                                          headers=self.post_headers,  # 注意此處的headers
                                          formdata={
                                              'utf8': '✓',
                                              'login': 'yidao620c',
                                              'password': '******',
                                              'authenticity_token': authenticity_token
                                          },
                                          callback=self.after_login,
                                          dont_filter=True
                                          )]

    def after_login(self, response):
        for url in self.start_urls:
            # 因為我們上面定義了Rule,所以只需要簡單的生成初始爬取Request即可
            yield Request(url, meta={'cookiejar': response.meta['cookiejar']})

    def parse_page(self, response):
        """這個是使用LinkExtractor自動處理連結以及`下一頁`"""
        logging.info(u'--------------訊息分割線-----------------')
        logging.info(response.url)
        issue_title = response.xpath(
            '//span[@class="js-issue-title"]/text()').extract_first()
        logging.info(u'issue_title:' + issue_title.encode('utf-8'))

    def _requests_to_follow(self, response):
        """重寫加入cookiejar的更新"""
        if not isinstance(response, HtmlResponse):
            return
        seen = set()
        for n, rule in enumerate(self._rules):
            links = [l for l in rule.link_extractor.extract_links(response) if l not in seen]
            if links and rule.process_links:
                links = rule.process_links(links)
            for link in links:
                seen.add(link)
                r = Request(url=link.url, callback=self._response_downloaded)
                # 下面這句是我重寫的
                r.meta.update(rule=n, link_text=link.text, cookiejar=response.meta['cookiejar'])
                yield rule.process_request(r)


你可以在GitHub上看到本文的完整專案原始碼,還有另外一個自動登陸iteye網站的例子。https://github.com/yidao620c/core-scrapy