爬虫实践----模拟知乎的登录

 

 

 

 

 

 

 

 

 

1、requests的快速入门

Python 提供了很多模块来支持 HTTP 协议的网络编程,urllib、urllib2、urllib3、httplib、httplib2,都是和 HTTP 相关的模块,看名字觉得很反人类,更糟糕的是这些模块在 Python2 与 Python3 中有很大的差异,如果业务代码要同时兼容 2 和 3,写起来会让人崩溃。幸运地是,繁荣的 Python 社区给开发者带来了一个非常惊艳的 HTTP 库 requests,一个真正给人用的HTTP库。

requests 实现了 HTTP 协议中绝大部分功能,它提供的功能包括 Keep-Alive、连接池、Cookie持久化、内容自动解压、HTTP代理、SSL认证、连接超时、Session等很多特性,最重要的是它同时兼容 python2 和 python3。

快速入门

requests的get()函数用一个网页的URL作为参数,然后返回一个Response对象。Response 对象是 对 HTTP 协议中服务端返回给浏览器的响应数据的封装,响应的中的主要元素包括:状态码、原因短语、响应首部、响应体等等,这些属性都封装在Response 对象中。

requests 除了支持 GET 请求外,还支持 HTTP 规范中的其它所有方法,包括 POST、PUT、DELTET、HEADT、OPTIONS方法。

>>> r = requests.post('http://httpbin.org/post', data = {'key':'value'})
>>> r = requests.put('http://httpbin.org/put', data = {'key':'value'})
>>> r = requests.delete('http://httpbin.org/delete')
>>> r = requests.head('http://httpbin.org/get')
>>> r = requests.options('http://httpbin.org/get')

构建请求查询参数

很多URL都带有很长一串参数,我们称这些参数为URL的查询参数,用”?”附加在URL链接后面,多个参数之间用”&”隔开 ,比如:http://fav.foofish.net/?p=4&s=20 ,现在你可以用字典来构建查询参数:

>>> args = {"p": 4, "s": 20}
>>> response = requests.get("http://fav.foofish.net", params = args)
>>> response.url
'http://fav.foofish.net/?p=4&s=2'

构建请求首部Headers

requests 可以很简单地指定请求首部字段 Headers,比如有时要指定 User-Agent 伪装成浏览器发送请求,以此来蒙骗服务器。直接传递一个字典对象给参数 headers 即可。

>>> r = requests.get(url, headers={'user-agent': 'Mozilla/5.0'})

构建POST请求数据

requests 可以非常灵活地构建 POST 请求需要的数据,如果服务器要求发送的数据是表单数据,则可以指定关键字参数 data,如果要求传递 json 格式字符串参数,则可以使用json关键字参数,参数的值都可以字典的形式传过去。

作为表单数据传输给服务器

>>> payload = {'key1': 'value1', 'key2': 'value2'}
>>> r = requests.post("http://httpbin.org/post", data=payload)

作为json格式的字符串格式传输给服务器

>>> import json
>>> url = 'http://httpbin.org/post'
>>> payload = {'some': 'data'}
>>> r = requests.post(url, json=payload)

Response中的响应体

HTTP返回的响应消息中很重要的一部分内容是响应体,响应体在 requests 中处理非常灵活,与响应体相关的属性有:content、text、json()。

  1. content 是 byte 类型,适合直接将内容保存到文件系统或者传输到网络中
  2. text 是 str 类型,比如一个普通的 HTML 页面,需要对文本进一步分析时,使用 text
  3. 如果使用第三方开放平台或者API接口爬取数据时,返回的内容是json格式的数据时,那么可以直接使用json()方法返回一个经过json.loads()处理后的对象
# content
>>> r = requests.get("https://pic1.zhimg.com/v2-2e92ebadb4a967829dcd7d05908ccab0_b.jpg")
>>> type(r.content)
<class 'bytes'>
# 另存为 test.jpg
>>> with open("test.jpg", "wb") as f:
...     f.write(r.content)

# text
>>> r = requests.get("https://foofish.net/understand-http.html")
>>> type(r.text)
<class 'str'>
>>> re.compile('xxx').findall(r.text)

# json
>>> r = requests.get('https://www.v2ex.com/api/topics/hot.json')
>>> r.json()
[{'id': 352833, 'title': '在长沙,父母同住...'

代理设置

当爬虫频繁地对服务器进行抓取内容时,很容易被服务器屏蔽掉,因此要想继续顺利的进行爬取数据,使用代理是明智的选择。如果你想爬取墙外的数据,同样设置代理可以解决问题,requests 完美支持代理。

import requests

proxies = {
  'http': 'http://10.10.1.10:3128',
  'https': 'http://10.10.1.10:1080',
}
requests.get('http://example.org', proxies=proxies)

超时设置

requests 发送请求时,默认请求下线程一直阻塞,直到有响应返回才处理后面的逻辑。如果遇到服务器没有响应的情况时,问题就变得很严重了,它将导致整个应用程序一直处于阻塞状态而没法处理其他请求。 正确的方式的是给每个请求显示地指定一个超时时间。

>>> import requests
>>> r = requests.get("http://www.google.coma")
'...一直阻塞中'

>>> r = requests.get("http://www.google.coma", timeout=5)
'5秒后报错'
Traceback (most recent call last):
socket.timeout: timed out

Session

爬虫系列学习(1):快速理解HTTP协议中介绍过HTTP协议是一中无状态的协议,为了维持客户端与服务器之间的通信状态,使用 Cookie 技术使之保持双方的通信状态。

有些网页是需要登录才能进行爬虫操作的,而登录的原理就是浏览器首次通过用户名密码登录之后,服务器给客户端发送一个随机的Cookie,下次浏览器请求其它页面时,就把刚才的 cookie 随着请求一起发送给服务器,这样服务器就知道该用户已经是登录用户。

import requests
# 构建会话
session  = requests.Session()
# 登录url
session.post(login_url, data={username, password})
# 登录后才能访问的url
r = session.get(home_url)
session.close()

构建一个session会话之后,客户端第一次发起请求登录账户,服务器自动把cookie信息保存在session对象中,发起第二次请求时requests 自动把session中的cookie信息发送给服务器,使之保持通信状态。

 

2、快速理解HTTP协议

爬虫实质上就是模拟浏览器进行HTTP请求的过程。

HTTP协议是什么?

你浏览的每一个网页都是基于 HTTP 协议呈现的,HTTP 协议是互联网应用中,客户端(浏览器)与服务器之间进行数据通信的一种协议。协议中规定了客户端应该按照什么格式给服务器发送请求,同时也约定了服务端返回的响应结果应该是什么格式。

HTTP 协议本身是非常简单的。它规定,只能由客户端主动发起请求,服务器接收请求处理后返回响应结果,同时 HTTP 是一种无状态的协议,协议本身不记录客户端的历史请求记录。

 


HTTP请求格式

HTTP 请求由3部分组成,分别是请求行、请求首部、请求体,首部和请求体是可选的,并不是每个请求都需要的。

 

请求行

请求行是每个请求必不可少的部分,它由3部分组成,分别是请求方法(method)、请求URL(URI)、HTTP协议版本,以空格隔开。

HTTP协议中最常用的请求方法有:GET、POST、PUT、DELETE。GET 方法用于从服务器获取资源,90%的爬虫都是基于GET请求抓取数据。

请求首部

因为请求行所携带的信息量非常有限,以至于客户端还有很多想向服务器要说的事情不得不放在请求首部(Header),请求首部用于给服务器提供一些额外的信息,比如 User-Agent 用来表明客户端的身份,让服务器知道你是来自浏览器的请求还是爬虫,是来自 Chrome 浏览器还是 FireFox。HTTP/1.1 规定了47种首部字段类型。HTTP首部字段的格式很像 Python 中的字典类型,由键值对组成,中间用冒号隔开。比如:

User-Agent: Mozilla/5.0

因为客户端发送请求时,发送的数据(报文)是由字符串构成的,为了区分请求首部的结尾和请求体的开始,用一个空行来表示,遇到空行时,就表示这是首部的结尾,请求体的开始。

请求体

请求体是客户端提交给服务器的真正内容,比如用户登录时的需要用的用户名和密码,比如文件上传的数据,比如注册用户信息时提交的表单信息。


HTTP响应

服务端接收请求并处理后,返回响应内容给客户端,同样地,响应内容也必须遵循固定的格式浏览器才能正确解析。HTTP 响应也由3部分组成,分别是:响应行、响应首部、响应体,与 HTTP 的请求格式是相对应的。

 

其中状态码是一个很重要的字段,在我们进行爬虫的爬取时,经常需要靠这一标准来判断我们的请求是否成功。如果状态码是200,说明客户端的请求处理成功,如果是500,说明服务器处理请求的时候出现了异常。404 表示请求的资源在服务器找不到。

响应体就是我们请求后,服务器给我们返回的内容了。通常是一个HTML网页源码,我们需要对该源码进行进一步的提取来获得我们需要的信息。

 

3、实战篇1:模拟知乎登录

经常写爬虫的都知道,有些页面在登录之前是被禁止抓取的,比如知乎的话题页面就要求用户登录才能访问,而 “登录” 离不开 HTTP 中的 Cookie 技术。

登录原理

Cookie 的原理非常简单,因为 HTTP 是一种无状态的协议,因此为了在无状态的 HTTP 协议之上维护会话(session)状态,让服务器知道当前是和哪个客户在打交道,Cookie 技术出现了 ,Cookie 相当于是服务端分配给客户端的一个标识。

cookie

  1. 浏览器第一次发起 HTTP 请求时,没有携带任何 Cookie 信息
  2. 服务器把 HTTP 响应,同时还有一个 Cookie 信息,一起返回给浏览器
  3. 浏览器第二次请求就把服务器返回的 Cookie 信息一起发送给服务器
  4. 服务器收到HTTP请求,发现请求头中有Cookie字段, 便知道之前就和这个用户打过交道了。

分析Post数据

由于知乎进行了改版,网上很多其他的模拟登录的方式已经不行了,所以这里从原理开始一步步分析要如何进行模拟登录。

要把我们的爬虫伪装成浏览器登录,则首先要理解浏览器登录时,是怎么发送报文的。首先打开知乎登录页,打开谷歌浏览器开发者工具,选择Network页,勾选Presev log,点击登陆。 我们很容易看到登录的请求首等信息:

模拟登录最终是要构建请求首和提交参数,即构造 Request Headers和FormData。


构建Headers

Request Headers中有几个参数需要注意:

  1. Content-Type (后面的boundary指定了表单提交的分割线)
  2. cookie (登陆前cookie就不为空,说明之前肯定有set-cookie的操作 )
  3. X-Xsrftoken (则是防止Xsrf跨域的Token认证,可以在Response Set-Cookie中找到 )

接下来我们看看登录时我们向服务器请求了什么,因为这是开门的钥匙,我们必须先知道钥匙由哪些部分组成,才能成功的打开大门:

可以看到Request Payload中出现最多的是---Webxxx这一字符串,上面已经说过了,这是一个分割线,我们可以直接忽略,所以第一个参数是:client_id=c3cef7c66a1843f8b3a9e6a1e3160e20 ;第二个参数为grant_type=password....整理了所有的参数如下(知乎的改版可能导致参数改变):

参数生成方式
client_idc3cef7c66a1843f8b3a9e6a1e3160e20固定
grant_typepassword固定
timestamp1530173433263时间戳
signature283d218eac893259867422799d6009749b6aff3fHash
username/passwordxxxxx/xxxxxx固定
captchaNull这是验证码模块,有时会出现

后面还有一些参数是固定参数,这里就不一一列出来了。现在总结我们需要自己生成的一些参数:

  1. X-Xsrftoken

    利用全局搜索可以发现该参数的值存在cookie中,因此可以利用正则表达式直接从cookie中提取;

  2. timestamp

    该参数为时间戳,可以使用 timestamp = str(int(time.time()*1000))生成

  3. signature

    首先ctrl+shift+F全局搜索signature,发现其是在main.app.xxx.js的一个js文件中生成的,打开该.js文件,然后复制到编辑器格式化代码

    因此我们可以用python来重写这个hmac加密过程:

    def _get_signature(timestamp):
            """
            通过 Hmac 算法计算返回签名
            实际是几个固定字符串加时间戳
            :param timestamp: 时间戳
            :return: 签名
            """
            ha = hmac.new(b'd1b964811afb40118a12068ff74a12f4', digestmod=hashlib.sha1)
            grant_type = self.login_data['grant_type']
            client_id = self.login_data['client_id']
            source = self.login_data['source']
            ha.update(bytes((grant_type + client_id + source + timestamp), 'utf-8'))
            return ha.hexdigest()

验证码

登录提交的表单中有个captcha参数,这是登录的验证码参数,有时候登录时会出现需要验证码的情况。captcha 验证码,是通过 GET 请求单独的 API 接口返回是否需要验证码(无论是否需要,都要请求一次),如果是 True 则需要再次 PUT 请求获取图片的 base64 编码。

所以登录验证的过程总共分为三步,首先GET请求看是否需要验证码;其次根据GET请求的结果,如果为True,则需要发送PUT请求来获取验证的图片;最后将验证的结果通过POST请求发送给服务器。

这是lang=cn的API需要提交的数据形式,实际上有两个 API,一个是识别倒立汉字,一个是常见的英文验证码,任选其一即可,汉字是通过 plt 点击坐标,然后转为 JSON 格式。

最后还有一点要注意,如果有验证码,需要将验证码的参数先 POST 到验证码 API,再随其他参数一起 POST 到登录 API。该部分完整的代码如下:

def _get_captcha(lang, headers):
    if lang == 'cn':
        api = 'https://www.zhihu.com/api/v3/oauth/captcha?lang=cn'
    else:
        api = 'https://www.zhihu.com/api/v3/oauth/captcha?lang=en'
    resp = self.session.get(api, headers=headers)
    show_captcha = re.search(r'true', resp.text)

    if show_captcha:
        put_resp = self.session.put(api, headers=headers)
        json_data = json.loads(put_resp.text)
        img_base64 = json_data['img_base64'].replace(r'\n', '')
        with open('./captcha.jpg', 'wb') as f:
            f.write(base64.b64decode(img_base64))
        img = Image.open('./captcha.jpg')
        if lang == 'cn':
            plt.imshow(img)
            print('点击所有倒立的汉字,按回车提交')
            points = plt.ginput(7)
            capt = json.dumps({'img_size': [200, 44],
                                'input_points': [[i[0]/2, i[1]/2] for i in points]})
        else:
            img.show()
            capt = input('请输入图片里的验证码:')
        # 这里必须先把参数 POST 验证码接口
        self.session.post(api, data={'input_text': capt}, headers=headers)
        return capt
    return ''

保存Cookie

最后实现一个检查登录状态的方法,如果访问登录页面出现跳转,说明已经登录成功,这时将 Cookies 保存起来(这里 session.cookies 初始化为 LWPCookieJar 对象,所以有 save 方法),这样下次登录可以直接读取 Cookies 文件。

self.session.cookies = cookiejar.LWPCookieJar(filename='./cookies.txt')
def check_login(self):
    resp = self.session.get(self.login_url, allow_redirects=False)
    if resp.status_code == 302:
        self.session.cookies.save()
        return True
    return False

总结

理解了我们需要哪些信息,以及信息的提交方式,现在来整理完整的登录过程:

  1. 构建HEADERS请求头和FORM_DATA表单的基本信息,一般为固定不变的信息;
  2. 从cookies中获取x-xsrftoken,更新到headers中;
  3. 检查用户名和密码是否在data表单中,如果没有,则需要更新用户名和密码到表单中;
  4. 获取时间戳,并利用时间戳来计算signature参数,模拟js中的hmac过程;
  5. 检查验证码,如果需要验证码,则先将验证码的结果POST到验证API端口,手动输入验证码的结果;
  6. 将时间戳、验证码以及signature这三个参数更新到Request Payload中,即程序中的login_data表单;
  7. headersdata这两个表单信息POST到Login_API这个接口,可以查看我们登录时的信息,是把提交的信息发送到https://www.zhihu.com/api/v3/oauth/sign_in
  8. 检查返回的response结果,如果有error,则输出错误的结果;否则表示登录成功,保存cookie文件。

    目录

    #!/usr/bin/env python
    
    # -*- coding: utf-8 -*-
    
    import requests
    
    import time
    
    import re
    
    import base64
    
    import hmac
    
    import hashlib
    
    import json
    
    import matplotlib.pyplot as plt
    
    from http import cookiejar
    
    from PIL import Image
    
    
    
    HEADERS = {
    
        'Connection': 'keep-alive',
    
        'Host': 'www.zhihu.com',
    
        'Referer': 'https://www.zhihu.com/',
    
        'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 '
    
                      '(KHTML, like Gecko) Chrome/56.0.2924.87 Mobile Safari/537.36'
    
    }
    
    LOGIN_URL = 'https://www.zhihu.com/signup'
    
    LOGIN_API = 'https://www.zhihu.com/api/v3/oauth/sign_in'
    
    FORM_DATA = {
    
        'client_id': 'c3cef7c66a1843f8b3a9e6a1e3160e20',
    
        'grant_type': 'password',
    
        'source': 'com.zhihu.web',
    
        'username': '',
    
        'password': '',
    
        # 改为'cn'是倒立汉字验证码
    
        'lang': 'en',
    
        'ref_source': 'homepage'
    
    }
    
    
    
    
    
    class ZhihuAccount(object):
    
    
    
        def __init__(self):
    
            self.login_url = LOGIN_URL
    
            self.login_api = LOGIN_API
    
            self.login_data = FORM_DATA.copy()
    
            self.session = requests.session()
    
            self.session.headers = HEADERS.copy()
    
            self.session.cookies = cookiejar.LWPCookieJar(filename='./cookies.txt')
    
    
    
        def login(self, username=None, password=None, load_cookies=True):
    
            """
    
            模拟登录知乎
    
            :param username: 登录手机号
    
            :param password: 登录密码
    
            :param load_cookies: 是否读取上次保存的 Cookies
    
            :return: bool
    
            """
    
            if load_cookies and self.load_cookies():
    
                if self.check_login():
    
                    return True
    
    
    
            headers = self.session.headers.copy()
    
            headers.update({
    
                # 'authorization': 'oauth c3cef7c66a1843f8b3a9e6a1e3160e20',
    
                'X-Xsrftoken': self._get_token()
    
            })
    
            username, password = self._check_user_pass(username, password)
    
            self.login_data.update({
    
                'username': username,
    
                'password': password
    
            })
    
            timestamp = str(int(time.time()*1000))
    
            self.login_data.update({
    
                'captcha': self._get_captcha(self.login_data['lang'], headers),
    
                'timestamp': timestamp,
    
                'signature': self._get_signature(timestamp)
    
            })
    
    
    
            resp = self.session.post(self.login_api, data=self.login_data, headers=headers)
    
            if 'error' in resp.text:
    
                print(json.loads(resp.text)['error']['message'])
    
            elif self.check_login():
    
                return True
    
            print('登录失败')
    
            return False
    
    
    
        def load_cookies(self):
    
            """
    
            读取 Cookies 文件加载到 Session
    
            :return: bool
    
            """
    
            try:
    
                self.session.cookies.load(ignore_discard=True)
    
                return True
    
            except FileNotFoundError:
    
                return False
    
    
    
        def check_login(self):
    
            """
    
            检查登录状态,访问登录页面出现跳转则是已登录,
    
            如登录成功保存当前 Cookies
    
            :return: bool
    
            """
    
            resp = self.session.get(self.login_url, allow_redirects=False)
    
            if resp.status_code == 302:
    
                self.session.cookies.save()
    
                print('登录成功')
    
                return True
    
            return False
    
    
    
        def _get_token(self):
    
            """
    
            从登录页面获取 token
    
            :return:
    
            """
    
            resp = self.session.get(self.login_url)
    
            token = resp.cookies['_xsrf']
    
            return token
    
    
    
        def _get_captcha(self, lang, headers):
    
            """
    
            请求验证码的 API 接口,无论是否需要验证码都需要请求一次
    
            如果需要验证码会返回图片的 base64 编码
    
            根据 lang 参数匹配验证码,需要人工输入
    
            :param lang: 返回验证码的语言(en/cn)
    
            :param headers: 带授权信息的请求头部
    
            :return: 验证码的 POST 参数
    
            """
    
            if lang == 'cn':
    
                api = 'https://www.zhihu.com/api/v3/oauth/captcha?lang=cn'
    
            else:
    
                api = 'https://www.zhihu.com/api/v3/oauth/captcha?lang=en'
    
            resp = self.session.get(api, headers=headers)
    
            show_captcha = re.search(r'true', resp.text)
    
    
    
            if show_captcha:
    
                put_resp = self.session.put(api, headers=headers)
    
                json_data = json.loads(put_resp.text)
    
                img_base64 = json_data['img_base64'].replace(r'\n', '')
    
                with open('./captcha.jpg', 'wb') as f:
    
                    f.write(base64.b64decode(img_base64))
    
                img = Image.open('./captcha.jpg')
    
                if lang == 'cn':
    
                    plt.imshow(img)
    
                    print('点击所有倒立的汉字,按回车提交')
    
                    points = plt.ginput(7)
    
                    capt = json.dumps({'img_size': [200, 44],
    
                                       'input_points': [[i[0]/2, i[1]/2] for i in points]})
    
                else:
    
                    img.show()
    
                    capt = input('请输入图片里的验证码:')
    
                # 这里必须先把参数 POST 验证码接口
    
                self.session.post(api, data={'input_text': capt}, headers=headers)
    
                return capt
    
            return ''
    
    
    
        def _get_signature(self, timestamp):
    
            """
    
            通过 Hmac 算法计算返回签名
    
            实际是几个固定字符串加时间戳
    
            :param timestamp: 时间戳
    
            :return: 签名
    
            """
    
            ha = hmac.new(b'd1b964811afb40118a12068ff74a12f4', digestmod=hashlib.sha1)
    
            grant_type = self.login_data['grant_type']
    
            client_id = self.login_data['client_id']
    
            source = self.login_data['source']
    
            ha.update(bytes((grant_type + client_id + source + timestamp), 'utf-8'))
    
            return ha.hexdigest()
    
    
    
        def _check_user_pass(self, username, password):
    
            """
    
            检查用户名和密码是否已输入,若无则手动输入
    
            """
    
            if username is None:
    
                username = self.login_data.get('username')
    
                if not username:
    
                    username = input('请输入手机号:')
    
            if '+86' not in username:
    
                username = '+86' + username
    
    
    
            if password is None:
    
                password = self.login_data.get('password')
    
                if not password:
    
                    password = input('请输入密码:')
    
            return username, password
    
    
    
    
    
    if __name__ == '__main__':
    
        account = ZhihuAccount()
    
        account.login(username=None, password=None, load_cookies=False)

     


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值