模拟登陆github

我们熟知的是当我们访问一个网站,输入账号密码进入网页之后,再点击网页中的其他链接,跳转到另一个网页时,浏览器并不要求我们重新再输入一次账号密码,这是为什么呢?
原因是,当我们第一次输入账号密码后,服务器响应会返回给我们一个Cookie值,而之后再返回该网页的其他跳转网页时,客户端会将该Cookie值绑在请求上,发送给服务器,这样服务器就可以利用Cookie来判别我们是哪一个用户,找到会话,然后判别登陆状态,若状态为登陆,则不需要用户重新输入用户名密码来进行登陆。
这次的爬虫就需要做到模拟登陆,实现跨页面访问而不需要重新输入用户名及密码。

1. 首先分析登陆页面

  1. 打开登陆页面:https://www.github.com/login
    模拟登陆github
  2. 打开开发者工具,分析请求,找到对应的请求头,用以构建爬虫的请求头:
    模拟登陆github
    一般使用Host和User-Agent两个参数即可伪装成浏览器;构建请求头如下:
self.headers = {
            'Host': 'github.com',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0;Win64;x64;rv:62.0) Gecko/20100101 Firefox/62.0',
        }
 # User-Agent必须连在一起,中间不能出现空格,如‘User - Agent’(两天找不到错最后是因为多打了两个空格,哭)

这里存在一个问题:使用Chrome的User-Agent来获取源码中的token值时,返回的列表为空;而使用火狐浏览器的User-Agent时,返回的列表是我们想要的内容。这里可以发现,服务器对不同User-Agent的返回值是不同的,至于为什么?还需要进一步探索

2. 分析点击登陆之后页面(第一次登陆,即新创建了一个会话)

我们在登陆之后,访问其他页面,客户端发送给服务器Cookie值和表单,在服务器端找到会话,进而继续访问;所以我们模拟登陆需要的是模拟Cookie(request.Session()函数)和表单

  1. 查看cookie
    模拟登陆github
    发现第一次登陆后,响应头会返回Set-Cookie值,即设置Cookie返回给客户端,创建一个会话session

  2. 分析登陆后页面的请求头:
    模拟登陆github
    发现其与登陆页面的请求头是一致的,所以我们在请求登陆页面和github个人详情页面时,可以使用同一个请求头

  3. 分析登陆后页面的Form-Data(浏览器发送给服务器的表单):
    Form-Data体中含有用户名和密码,是客户端给服务器发送的表单信息,利用这些信息,可判断是在哪个会话中
    模拟登陆github
    发现其中:

    • commit、utf8其属性的值每次访问都是不变的
    • login、password的值分别是我们登陆所用的账户名和密码
    • 而authenticity-token每次访问登陆时,其值都是变化的,我们无法人为输入,需要从源码中获得;发现它藏在登陆页面的源码中

    这里介绍一个快速查找源码中关键字的方法:
    模拟登陆github
    在该界面中选中之后,按Ctrl+F,在箭头执行的输入框内输入我们想要查找的关键字,即可得到结果

  4. 获取authenticity_token的值
    1.获取xpath:
    模拟登陆github
    非常方便
    2.解析源码,获取authenticity_token的值:这里我们用到lxml里的etree模块,即利用xpath来获取信息
    (xpath教程如下:http://www.w3school.com.cn/xpath/xpath_syntax.asp)
    代码如下:

        def get_token(self):
            response = self.session.get(url=self.login_url, headers=self.headers)
            selector = etree.HTML(response.text)  # 解析HTML对象
            print(selector.xpath('//*[@id="login"]/form/input[2]/@value'))  # 提取所有id属性为login元素下的form元素的第二个input元素的value属性所对应的值
            token = selector.xpath('//*[@id="login"]/form/input[2]/@value')
            return token
    
  5. 构建form-data头:

form_data = {
            'commit': 'Sign in',
            'utf8': '✓',
            'authenticity_token': self.get_token(),
            'login': email,
            'password': passwd
        }

3. 全部代码如下:

import requests
from lxml import etree


USER = '***'    # 隐藏的用户名和密码,哈哈
PASSWORD = '******'


# 创建Login类,封装模拟登陆
class Login(object):
    def __init__(self):
        self.headers = {
            'Host': 'github.com',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0;Win64;x64;rv:62.0) Gecko/20100101 Firefox/62.0',
            # 'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Mobile Safari/537.36',
            # 使用谷歌浏览器的User-Agent时,返回的token为空列表,但使用火狐则成功,这是为什么?
        }
        self.login_url = 'https://www.github.com/login'  # 登陆页面的URL
        self.profile_url = 'https://www.github.com/settings/profile'  # 个人详情页URL
        self.session = requests.Session() # requests.Session()帮我们维持一个会话,自动处理Cookie

    # 获取authentici_token
    def get_token(self):
        response = self.session.get(url=self.login_url, headers=self.headers)
        selector = etree.HTML(response.text)  # 解析HTML对象
        print(selector.xpath('//*[@id="login"]/form/input[2]/@value'))  # 提取所有id属性为login元素下的form元素的第二个input元素的value属性所对应的值
        token = selector.xpath('//*[@id="login"]/form/input[2]/@value')
        return token

    # 登陆至个人详情页,获取信息
    def login(self, email, passwd):
        form_data = {
            'commit': 'Sign in',
            'utf8': '✓',
            'authenticity_token': self.get_token(),
            'login': email,
            'password': passwd
        }
        response = self.session.get(url=self.profile_url, data=form_data, headers=self.headers)
        print(response.status_code)
        if response.status_code == 200:
            self.dynamics(response.text)

        response = self.session.post(url=self.profile_url, headers=self.headers)
        if response.status_code == 200:
            self.profile(response.text)

    def dynamics(self, html):
        pass


    def profile(self, html):
        pass


if __name__ == '__main__':
    login = Login()
    login.login(USER, PASSWORD)