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()。
- content 是 byte 类型,适合直接将内容保存到文件系统或者传输到网络中
- text 是 str 类型,比如一个普通的 HTML 页面,需要对文本进一步分析时,使用 text
- 如果使用第三方开放平台或者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 相当于是服务端分配给客户端的一个标识。
- 浏览器第一次发起 HTTP 请求时,没有携带任何 Cookie 信息
- 服务器把 HTTP 响应,同时还有一个 Cookie 信息,一起返回给浏览器
- 浏览器第二次请求就把服务器返回的 Cookie 信息一起发送给服务器
- 服务器收到HTTP请求,发现请求头中有Cookie字段, 便知道之前就和这个用户打过交道了。
分析Post数据
由于知乎进行了改版,网上很多其他的模拟登录的方式已经不行了,所以这里从原理开始一步步分析要如何进行模拟登录。
要把我们的爬虫伪装成浏览器登录,则首先要理解浏览器登录时,是怎么发送报文的。首先打开知乎登录页,打开谷歌浏览器开发者工具,选择Network页,勾选Presev log,点击登陆。 我们很容易看到登录的请求首等信息:
模拟登录最终是要构建请求首和提交参数,即构造 Request Headers和FormData。
构建Headers
Request Headers中有几个参数需要注意:
- Content-Type (后面的boundary指定了表单提交的分割线)
- cookie (登陆前cookie就不为空,说明之前肯定有set-cookie的操作 )
- X-Xsrftoken (则是防止Xsrf跨域的Token认证,可以在Response Set-Cookie中找到 )
接下来我们看看登录时我们向服务器请求了什么,因为这是开门的钥匙,我们必须先知道钥匙由哪些部分组成,才能成功的打开大门:
可以看到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 ''
保存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
总结
理解了我们需要哪些信息,以及信息的提交方式,现在来整理完整的登录过程:
- 构建HEADERS请求头和FORM_DATA表单的基本信息,一般为固定不变的信息;
- 从cookies中获取
x-xsrftoken
,更新到headers中; - 检查用户名和密码是否在data表单中,如果没有,则需要更新用户名和密码到表单中;
- 获取时间戳,并利用时间戳来计算
signature
参数,模拟js中的hmac过程; - 检查验证码,如果需要验证码,则先将验证码的结果POST到验证API端口,手动输入验证码的结果;
- 将时间戳、验证码以及
signature
这三个参数更新到Request Payload
中,即程序中的login_data表单; - 将
headers
和data
这两个表单信息POST到Login_API这个接口,可以查看我们登录时的信息,是把提交的信息发送到https://www.zhihu.com/api/v3/oauth/sign_in ; - 检查返回的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)