使用session或scrapy模拟登录知乎
使用session或scrapy模拟登录知乎
session 模拟登陆
大家知道,登陆知乎需要验证码,那么如何通过 session 来模拟登陆知乎呢。首先通过分析知乎登陆请求, 如下图所示
上图中标注的部分就是我们需要模拟的请求,也就是说,我们使用 session 登陆的时候,首先需要构造我们的请求头部 header。
在上图中,有一个 x-xsrftoken
字段,这个字段是知乎登陆请求中必须有的字段,那么这个字段是如何获取的呢,在我们打开知乎登陆页面的时候,查看网页源代码,在这个里面,我们就能获取到这个信息,如下图所示
这是一个隐藏的标签,在页面上是不会显示的。(这种手法,有点类似于,当关闭浏览器 cookie 功能的时候,服务器传递 session_id
,也可以采用这种做法,发送一个隐藏的标签,保存 session_id
),那么,获取这个 xsrf token
就变得非常简单了,首先获取登录页面,然后通过正则表达式获取这个值,代码如下所示:
def get_xsrf_token():
response = session.get("https://www.zhihu.com", headers=header) # 这个参数必须是 headers
match_obj = re.search('.*name="_xsrf" value="(.*?)"', response.text) # match vs search
if match_obj:
return match_obj.group(1)
else:
return ""
大家可能发现,在上面的代码中,我已经使用到了 header,我构造的 header 如下
agent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"
header = {
"Host": "www.zhihu.com",
"Referer": "https://www.zhihu.com/",
"Upgrade-Insecure-Requests": "1",
"User-Agent": agent
}
注意,上面的代码的正则表达式部分,使用的是 search,不是 match,match 可能匹配不了正确的结果,不明之处可以查看 python documentation 中的 match vs search
的部分
既然 header 和 xsrf_token
都已经获取到了,那我们试着使用 session 来模拟登陆一下,在登录之前,我们需要构造 post_data
发送用户名和密码进行登录
def zhihu_login(account, password):
"""
模拟知乎登录
"""
post_data = {}
post_url = ""
if re.match("^1\d{10}", account): # 手机号码
print("phone login")
post_url = "https://www.zhihu.com/login/phone_num"
post_data = {
"_xsrf": get_xsrf_token(),
"phone_num": account,
"password": password
}
print(header)
print(post_data)
else:
if "@" in account:
print("email login")
post_url = "https://www.zhihu.com/login/email" # 邮箱登录
post_data = {
"_xsrf": get_xsrf_token(),
"email": account,
"password": password
}
response_text = session.post(post_url, data=post_data, headers = header)
这段代码运行的时候,登录失败,错误提示是验证码错误,F12
浏览器分析,查看登录的参数,如下
参数包含四个值,分别是 _xsrf
, password
, phone_num
, captcha_type
,我们在上面的代码中构建的 post_data
缺少验证码参数,所以模拟登录失败。
知乎登录的时候,如果登录成功,再次登录的时候,可能不需要输入验证码,这是因为在浏览器的 cookies 中保存了相关的数据,F12进入调试模式,将 storage 中的 cookie 删除,再登录,就需要输入验证码了
同时,还需要注意的一点就是,captcha_type
这个参数是 cn
,验证码是中文,让你点击倒立的汉字,点击正确才能通过验证码验证,如下所示
我们获取验证码的方式比较直接,就是通过将验证码图片下载下来,打开之后,输入验证码,构造 post_data
然后发送请求,这样来模拟登录。但是如果使用中文这种点击的方式,就无法采用这种方法了,通过观察参数我们发现,captcha_type
这个参数为 cn
的时候返回的验证码是中文,如果我们将这个值改成 en
是不是就是字母了呢,修改后发送请求如下所示(浏览器的调试功能里面,有一个 edit and send 的功能)
获取的验证码就是数字加字母了,这样,我们就能通过先获取验证码,再输入的方式模拟登录了,好了,说了这么多,那么如何获取验证码图片呢?我们在回头来看下验证码的请求
验证码的请求为https://www.zhihu.com/captcha.gif?r=1501223240460&type=login&lang=en
,中间 r=1501223240460
这个参数是一个随时间变动的参数,可以通过时间 time.time()
获取,代码如下
checkcode_url = "https://www.zhihu.com/captcha.gif?r=" + str(int(time.time()*1000)) + "&type=login&lang=en"
resp = session.get(checkcode_url, headers=header).content
with open("checkcode.gif", "wb") as f:
f.write(resp)
f.close()
try:
im = Image.open("checkcode.gif")
im.show()
im.close()
except:
pass
注意:这个里面有一个比较大的坑,就是 request 和 session 的问题。只能使用
session.get
,而不能使用requests.get
, 因为一次 session 就是一次会话,在后面继续使用验证码登录时,是在同一个 session 中的,session 会记录很多值,cookie 等。但是,如果使用的是 requests ,前后就不再同一个会话中,那么后面将带有验证码的post_data
发送过去时,会显示验证码无效,登录失败
上面的代码,就是将验证码图片获取之后,保存在 checkcode.gif
这个图片中,然后通过 Image 类的 show 方法打开图片,然后后面构造 post_data
的时候加上验证码参数即可
post_data["captcha"] = input("输入验证码: ")
Image 这个类,是在 PIL 这个库中的,如果没有安装,需要先安装
pip install pillow
然后,在使用的时候,import 进来就可以了
from PIL import Image
好了,介绍了这么多,知乎模拟登录就完成了。最后,我们可以加上 cookie,在登录之前,加载cookie,如果没有,那么在发送 post_data
之后,通过 session 将 cookie 保存起来,在 python2 中,是通过 cookielib 来实现的, python3 中发生了变化,这个方法被移到了 cookiejar 中。完整的代码如下所示
import requests
import re
import time
from PIL import Image
__author__ = ""
# python 中的 cookie lib,能够读取本地的 cookie 文件,生成 cookie,然后赋值给requests 的 cookie
try:
import cookielib # python 2版本
except ImportError:
import http.cookiejar as cookielib # python3 版本
session = requests.session() # 某一次连接,是一个长链接,keep-alive,这样就不需要每一次 request 的时候,都要去建立一次连接
# 使用 cookielib 实例化 session的cookies,然后才能使用save() 保存cookies,不然在后面就会报错
session.cookies = cookielib.LWPCookieJar(filename="cookies.txt")
try:
# 成功加载 load cookie 之后,将不再返回首页
session.cookies.load(ignore_discard=True)
except Exception:
print("can not load cookies")
agent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"
header = {
"Host": "www.zhihu.com",
"Referer": "https://www.zhihu.com/",
"Upgrade-Insecure-Requests": "1",
"User-Agent": agent
}
def get_xsrf_token():
response = session.get("https://www.zhihu.com", headers=header) # 这个参数必须是 headers
match_obj = re.search('.*name="_xsrf" value="(.*?)"', response.text) # match vs search
if match_obj:
return match_obj.group(1)
else:
return ""
def zhihu_login(account, password):
"""
模拟知乎登录
"""
post_data = {}
post_url = ""
if re.match("^1\d{10}", account): # 手机号码
print("phone login")
post_url = "https://www.zhihu.com/login/phone_num"
post_data = {
"_xsrf": get_xsrf_token(),
"phone_num": account,
"password": password
}
print(header)
print(post_data)
else:
if "@" in account:
print("email login")
post_url = "https://www.zhihu.com/login/email"
post_data = {
"_xsrf": get_xsrf_token(),
"email": account,
"password": password
}
# get check code image
checkcode_url = "https://www.zhihu.com/captcha.gif?r=" + str(int(time.time()*1000)) + "&type=login&lang=en"
resp = session.get(checkcode_url, headers=header).content
with open("checkcode.gif", "wb") as f:
f.write(resp)
f.close()
try:
im = Image.open("checkcode.gif")
im.show()
im.close()
except:
pass
post_data["captcha"] = input("checkcode:")
response_text = session.post(post_url, data=post_data, headers = header)
session.cookies.save() # 将服务器返回的 cookies 保存到本地,下一次登录的时候,直接从文件中获取
后面进入知乎后,通过 session.get
就可以获取登录后的数据。
scrapy 模拟登录
在 python 中,我们可以通过 requests.session()
获取 session,然后模拟登录,但是在 scrapy 中是没有 session,的,那么如何实现模拟登录呢。我使用的是 scrapy 中的 basic 模板。
scrapy 的流程是:
- 入口函数是
start_requests()
,对start_urls
这个列表中的每一个 url 进行处理,返回一个 Request(),这个函数将 url 给 scrapy 下载器下载,如果没有传递callback
,将调用默认的回调函数parse
函数进行解析。
那么,我的思路就是,重载 start_requests()
函数,在 start_requests
函数中,获取验证码,然后返回 Request 中,调用登录的回调函数,传入 header 和 post_data
(此时是带有验证码参数的)进行模拟登录,这样就能保证验证码获取后的登录是在一个会话中了。
完整代码如下所示:
def start_requests(self):
"""
返回登录页面,在回调函数中,提取 xsrf 的值
"""
return [scrapy.Request('https://www.zhihu.com/#signin', callback=self.login, headers=self.headers)]
def login(self, response):
response_text = response.text
xsrf = ""
match_obj = re.search('.*name="_xsrf" value="(.*?)"', response_text)
if match_obj:
xsrf = match_obj.group(1)
if xsrf:
post_url = "https://www.zhihu.com/login/phone_num"
post_data = {
"_xsrf": xsrf,
"phone_num": "13530210085",
"password": "wuzhenyu25977758",
"captcha": ""
}
t = str(int(time.time() * 1000))
checkcode_url = "https://www.zhihu.com/captcha.gif?r={0}&type=login".format(t)
"""
必须保证验证码的使用,与得到验证码的链接请求是在同一个会话连接中,此处使用 Request 的方法,
通过 scrapy 将请求下载之后,使用回调函数,在回调函数中进行处理,保证回调函数中的 response 与获取验证码时
是在同一个会话中
"""
yield scrapy.Request(checkcode_url, meta={"post_data": post_data}, headers=self.headers,
callback=self.login_with_checkcode)
def login_with_checkcode(self, response):
post_url = "https://www.zhihu.com/login/phone_num"
post_data = response.meta.get("post_data", {})
with open("checkcode.gif", "wb") as f:
f.write(response.body)
f.close()
try:
im = Image.open("checkcode.gif")
im.show()
# im.close() # no close function
except:
pass
post_data["captcha"] = input("checkcode: ")
return [scrapy.FormRequest(
url=post_url,
formdata=post_data,
headers=self.headers,
callback=self.check_login
)]
def check_login(self, response):
"""
检查登录是否成功
"""
text_json = json.loads(response.text)
if "msg" in text_json and text_json["msg"] == "登录成功":
for url in self.start_urls:
yield scrapy.Request(url, dont_filter=True, headers=self.headers) # 默认回调函数为 parse
首先,在 start_requests
函数中,通过 scrapy 下载登录页面,通过回调函数获取 xsrf_token
的值,然后再构造验证码的url,通过 scrapy 下载,注意验证码图片数据此时保存在了 response.body
中,通过回调函数 login_with_checkcode
构造 post_data
,然后使用 scrapy.FormRequest
模拟登录,在 check_login
中对登录结果进行验证,查看登录是否成功。
最后,在 check_login
函数中,如果登录成功,再回到之前 start_requests
本身应该处理的内容中,将 start_urls
中的 url 进行处理,调用默认回调函数 parse
对页面进行解析。 parse
中是实际的页面解析结果,如何写这部分,大家可以看我的另一篇博客 爬取伯乐在线所有文章