模擬登陸——以github為例

2020-10-02 12:00:49

1.分析登入過程

我們以github為例,分析登入的過程

開啟 GitHub 的登入頁面,連結為 https://github.com/login ,輸入 GitHub 的使用者名稱和密碼,開啟開發者工具,將 Preserve Log 選項勾選上,這表示顯示持續紀錄檔,如下圖所示。

在這裡插入圖片描述

點選登入按鈕,這時便會看到開發者工具下方顯示了各個請求過程,如下圖所示。

在這裡插入圖片描述
點選第一個請求,進入其詳情頁面,如下圖所示。

在這裡插入圖片描述

可以看到請求的 URL 為 https://github.com/session ,請求方式為 POST。再往下看,我們觀察到它的 Form Data 和 Headers 這兩部分內容:

在這裡插入圖片描述

Headers 裡面包含了 Cookies、Host、Origin、Referer、User-Agent 等資訊。Form Data 包含了 5 個欄位,commit 是固定的字串 Sign in,utf8 是一個勾選字元,authenticity_token 較長,其初步判斷是一個 Base64 加密的字串,login 是登入的使用者名稱,password 是登入的密碼。

綜上所述,我們現在無法直接構造的內容有 Cookies 和 authenticity_token。下面我們再來探尋一下這兩部分內容如何獲取。

在登入之前我們會存取到一個登入頁面,此頁面是通過 GET 形式存取的。輸入使用者名稱密碼,點選登入按鈕,瀏覽器傳送這兩部分資訊,也就是說 Cookies 和 authenticity_token 一定是在存取登入頁的時候設定的。

這時再退出登入,回到登入頁,重新存取登入頁,截獲發生的請求:

在這裡插入圖片描述
存取登入頁面的請求如圖所示,Response Headers 有一個 Set-Cookie 欄位。這就是設定 Cookies 的過程。

另外,我們發現 Response Headers 沒有和 authenticity_token 相關的資訊,所以可能 authenticity_token 還隱藏在其他的地方或者是計算出來的。我們再從網頁的原始碼探尋,搜尋相關欄位,發現原始碼裡面隱藏著此資訊,它是一個隱藏式表單元素,如下圖所示:

在這裡插入圖片描述
現在我們已經獲取到所有資訊,接下來實現模擬登入。

2.程式碼實現

首先我們定義一個 Login 類,初始化一些變數:

class Login(object):
    def __init__(self):
        self.headers = {
            'Referer': 'https://github.com/',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36',
            'Host': 'github.com'
        }
        self.login_url = 'https://github.com/login'
        self.post_url = 'https://github.com/session'
        self.logined_url = 'https://github.com/settings/profile'
        self.feed_url = 'https://github.com/dashboard-feed'
        self.session = requests.Session()

這裡最重要的一個變數就是 requests 庫的 Session,它可以幫助我們維持一個對談,而且可以自動處理 Cookies,我們不用再去擔心 Cookies 的問題。

接下來,存取登入頁面要完成兩件事:一是通過此頁面獲取初始的 Cookies,二是提取出 authenticity_token

在這裡我們實現一個 token() 方法,如下所示:

def token(self):
        response = self.session.get(self.login_url, headers=self.headers)
        selector = pq(response.text)
        token = selector('input[name="authenticity_token"]').attr('value')

我們用 Session 物件的 get() 方法存取 GitHub 的登入頁面,然後用 XPath 解析出登入所需的authenticity_token` 資訊並返回。

現在已經獲取初始的 Cookiesauthenticity_token,開始模擬登入,實現一個 login() 方法,如下所示:

def login(self, email, password):
    post_data = {
        'commit': 'Sign in',
        'utf8': '✓',
        'authenticity_token': self.token(),
        'login': email,
        'password': password
    }

    response = self.session.post(self.post_url, data=post_data, headers=self.headers)
    response = self.session.get(self.feed_url, headers=self.headers)
    if response.status_code == 200:
        self.dynamics(response.text)

    response = self.session.get(self.logined_url, headers=self.headers)
    if response.status_code == 200:
        self.profile(response.text)

首先構造一個表單,複製各個欄位,其中 email 和 password 是以變數的形式傳遞。然後再用 Session 物件的 post() 方法模擬登入即可。現在的github 關注人動態是通過ajax載入的,要向’https://github.com/dashboard-feed’ 傳送請求。由於 requests 自動處理了重定向資訊,我們登入成功後就可以直接跳轉到首頁,首頁會顯示所關注人的動態資訊,得到響應之後我們用 dynamics() 方法來對其進行處理。接下來再用 Session 物件請求個人詳情頁,然後用 profile() 方法來處理個人詳情頁資訊。

其中,dynamics() 方法和 profile() 方法的實現如下所示:

def dynamics(self, html):
    selector = pq(html)
    #print(selector.text())
    dynamics = selector('div[class="d-flex flex-items-baseline"] div')
    dynamics.find('span').remove()
    #print(dynamics.text())
    for item in dynamics.items():
        dynamic = item.text().strip()
        print(dynamic)

def profile(self, html):
    selector = pq(html)
    #print(selector.text())
    name = selector('input[id="user_profile_name"]').attr('value')
    email = selector('select[id="user_profile_email"] option[selected="selected"]').text()
    print(name,email)

我們新建一個 Login 物件,記住下面要輸入自己的郵箱和密碼,然後執行程式,如下所示:

if __name__ == "__main__":
    login = Login()
    login.login(email='email', password='password')

完整程式碼如下:

import requests
from pyquery import PyQuery as pq

class Login(object):
    def __init__(self):
        self.headers = {
            'Referer': 'https://github.com/',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36',
            'Host': 'github.com'
        }
        self.login_url = 'https://github.com/login'
        self.post_url='https://github.com/session'
        self.feed_url = 'https://github.com/dashboard-feed'
        self.logined_url = 'https://github.com/settings/profile'
        ## 維持對談,自動處理cookies
        self.session = requests.Session()
    
    ## 解析出登入所需要的
    def token(self):
        response = self.session.get(self.login_url, headers=self.headers)
        selector = pq(response.text)
        token = selector('input[name="authenticity_token"]').attr('value')
        return token
    def login(self, email, password):
        #print(self.token())
        post_data = {
            'commit': 'Sign in',
            'utf8': '✓',
            'authenticity_token': self.token(),
            'login': email,
            'password': password
        }
        response = self.session.post(self.post_url, data=post_data, headers=self.headers)
        response = self.session.get(self.feed_url, headers=self.headers)
        if response.status_code == 200:
            self.dynamics(response.text)
            #print(response.text)
        print("================================")
        response = self.session.get(self.logined_url, headers=self.headers)
        if response.status_code == 200:
            self.profile(response.text)
    
    ## 關注人的動態資訊
    def dynamics(self, html):
        selector = pq(html)
        #print(selector.text())
        dynamics = selector('div[class="d-flex flex-items-baseline"] div')
        dynamics.find('span').remove()
        #print(dynamics.text())
        for item in dynamics.items():
            dynamic = item.text().strip()
            print(dynamic)
    ## 詳情頁面
    def profile(self, html):
        selector = pq(html)
        #print(selector.text())
        name = selector('input[id="user_profile_name"]').attr('value')
        email = selector('select[id="user_profile_email"] option[selected="selected"]').text()
        print(name,email)
if __name__ == "__main__":
    login = Login()
    login.login(email='2685764101@qq.com', password='password')

執行結果如下:
在這裡插入圖片描述