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()。
# content
>>> r = requests.get("https://pic1.zhimg.com/v2-2e92ebadb4a967829dcd7d05908ccab0_b.jpg")
>>> type(r.content)
# 另存为 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)
>>> 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信息发送给服务器,使之保持通信状态。
爬虫实质上就是模拟浏览器进行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 响应也由3部分组成,分别是:响应行、响应首部、响应体,与 HTTP 的请求格式是相对应的。
其中状态码是一个很重要的字段,在我们进行爬虫的爬取时,经常需要靠这一标准来判断我们的请求是否成功。如果状态码是200,说明客户端的请求处理成功,如果是500,说明服务器处理请求的时候出现了异常。404 表示请求的资源在服务器找不到。
响应体就是我们请求后,服务器给我们返回的内容了。通常是一个HTML网页源码,我们需要对该源码进行进一步的提取来获得我们需要的信息。
经常写爬虫的都知道,有些页面在登录之前是被禁止抓取的,比如知乎的话题页面就要求用户登录才能访问,而 “登录” 离不开 HTTP 中的 Cookie 技术。
Cookie 的原理非常简单,因为 HTTP 是一种无状态的协议,因此为了在无状态的 HTTP 协议之上维护会话(session)状态,让服务器知道当前是和哪个客户在打交道,Cookie 技术出现了 ,Cookie 相当于是服务端分配给客户端的一个标识。
由于知乎进行了改版,网上很多其他的模拟登录的方式已经不行了,所以这里从原理开始一步步分析要如何进行模拟登录。
要把我们的爬虫伪装成浏览器登录,则首先要理解浏览器登录时,是怎么发送报文的。首先打开知乎登录页,打开谷歌浏览器开发者工具,选择Network页,勾选Presev log,点击登陆。 我们很容易看到登录的请求首等信息:
模拟登录最终是要构建请求首和提交参数,即构造 Request Headers和FormData。
Request Headers中有几个参数需要注意:
接下来我们看看登录时我们向服务器请求了什么,因为这是开门的钥匙,我们必须先知道钥匙由哪些部分组成,才能成功的打开大门:
可以看到Request Payload中出现最多的是---Webxxx
这一字符串,上面已经说过了,这是一个分割线,我们可以直接忽略,所以第一个参数是:client_id=c3cef7c66a1843f8b3a9e6a1e3160e20 ;第二个参数为grant_type=password....整理了所有的参数如下(知乎的改版可能导致参数改变):
参数 | 值 | 生成方式 |
---|---|---|
client_id | c3cef7c66a1843f8b3a9e6a1e3160e20 | 固定 |
grant_type | password | 固定 |
timestamp | 1530173433263 | 时间戳 |
signature | 283d218eac893259867422799d6009749b6aff3f | Hash |
username/password | xxxxx/xxxxxx | 固定 |
captcha | Null | 这是验证码模块,有时会出现 |
后面还有一些参数是固定参数,这里就不一一列出来了。现在总结我们需要自己生成的一些参数:
X-Xsrftoken
利用全局搜索可以发现该参数的值存在cookie中,因此可以利用正则表达式直接从cookie中提取;
timestamp
该参数为时间戳,可以使用 timestamp = str(int(time.time()*1000))生成
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 ''
最后实现一个检查登录状态的方法,如果访问登录页面出现跳转,说明已经登录成功,这时将 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
理解了我们需要哪些信息,以及信息的提交方式,现在来整理完整的登录过程:
x-xsrftoken
,更新到headers中;signature
参数,模拟js中的hmac过程;signature
这三个参数更新到Request Payload
中,即程序中的login_data表单;headers
和data
这两个表单信息POST到Login_API这个接口,可以查看我们登录时的信息,是把提交的信息发送到https://www.zhihu.com/api/v3/oauth/sign_in ;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)