分析登录页面的请求
在浏览打开一个隐私模式的浏览窗口,主要是为了避免浏览器已经保存的cookies的干扰,然后再打开知乎登录页面并打开控制台,故意输错手机号或密码便于观察请求,然后登录
注意红色的名为sing_in的文件,这就是知乎登录的API接口,也是我们模拟登录的链接。我们需要模拟构建POST请求所需要的Headers和Form-Data这两个对象
构建 Headers
继续看Requests Headers信息,和登录页面的 GET 请求对比发现,这个 POST 的头部多了三个身份验证字段,经测试x-xsrftoken是必需的,x-xsrftoken则是防 Xsrf 跨站的 Token 认证。首先我们去Cookie里面看一下有没有这个值。
可以看到在Cookie的_xsrf里面找到了这个值,既然在我们未登录的情况下已经有了这个Cookie值,那么服务器肯定在返回的请求中设置了这个Cookie。我们在之前的请求中查找一下。
可以看到在我们访问首页时从Response Headers的Set-Cookie字段中可以找到了这个字段(注意:需要在浏览器没有Cookie的情况下才能看到)。
构建Form-Data
找出Form-Data的完整键值对
可以看到Form部分目前已经是加密的,无法再直观看到,登陆url是https://www.zhihu.com/api/v3/oauth/sign_in。那我们可以直接搜 sign_in 试试。搜了发现和上面的 signature 是在同一个JS文件上的,可以在 JS代码 里打断点的方式调试。
然后我们逐个构建上图这些参数:
经过调试发现会变的就只有timestamp,signature,username,password,captcha这几个字段。
timestamp 时间戳,这个很好解决,区别是这里是13位整数,Python 生成的整数部分只有10位,需要额外乘以1000
timestamp = str(int(time.time()*1000))
signature 通过 Crtl+Shift+F 搜索找到是在一个 JS 里生成的,是通过 Hmac 算法对几个固定值和时间戳进行加密,那么只需要在 Python 里也模拟一次这个加密即可。
Python的内置Hmac函数可以非常方便的模拟加密,需要注意的是这里的时间戳需要与后面Form-Data中的时间戳保持一致,否则会登录失败。
def get_signature(self):
#伪造signature
client_id="c3cef7c66a1843f8b3a9e6a1e3160e20"
sk="d1b964811afb40118a12068ff74a12f4"
h=hmac.new(sk.encode("utf-8"),digestmod=sha1)
h.update('password'.encode("utf-8"))
h.update(client_id.encode("utf-8"))
h.update("com.zhihu.web".encode("utf-8"))
h.update(self.timestamp.encode("utf-8"))
return h.hexdigest()
captcha 验证码,是通过 GET 请求单独的 API 接口返回是否需要验证码(无论是否需要,都要请求一次),如果是 True 则需要再次 PUT 请求获取图片的 base64 编码。
知乎的验证码有两种形式,一种是点击倒立文字,还有一种是英文字符串。这里只实现第二种验证方式。
def getcapture(self):
#获取验证码
message=self.session.get(url="https://www.zhihu.com/api/v3/oauth/captcha?lang=en").json()
if message["show_captcha"]=="False":
self.picture=""
else:
print("需要验证码")
while True:
self.picture_url=self.session.put(url="https://www.zhihu.com/api/v3/oauth/captcha?lang=en").json()
with open("captcha.jpg","wb") as f:
f.write(base64.b64decode(self.picture_url["img_base64"]))
image=Image.open('captcha.jpg')
image.show()
self.picture=input("请输入验证码:")
time.sleep(2)
message1=self.session.post(url="https://www.zhihu.com/api/v3/oauth/captcha?lang=en",data={'input_text':self.picture})
# print("{},{}".format(message1.status_code,message1.text))
if message1.status_code==201:
break
else:
print("{} 请提交正确的验证码".format(message1.status_code))
加密Form-Data
现在知乎必须先将 Form-Data 加密才能进行 POST 传递,所以我们还要解决加密问题,可由于我们看到的 JS 是混淆后的代码,想窥视其中的加密实现方式是一件很费精力的事情。
加密一般都用到 encrypt 名字之类的,可以直接根据这个名字搜。
通过调试,可以看到,我们需要的加密字符串出来了。
找出Form-Data的所有加密方法
知道位置后,我们可以直接把这个加密的 js 方法都扣出来,放在一个 html 文件内执行就好。可以将js源码复制到vscode编辑器里查找。
格式化后有400多行,里面全是混淆
为了看看这个正确不正确,我们可以把函数里面的内容直接拿出来,就是去掉最外层的function (module, exports, webpack_require) ,并把exports相关的代码去掉。然后调用下面的函数 b,把我们的 fromdata 传进去
将上面的 JavaScript 弄到一个 html 文件中,放在 script 标签内即可
用浏览器打开就可以看到加密的字符串了。
接下来就是用Python来操作了。由于这个加密的方法有400多行,还全是混淆,所以用Python来模拟是不太可能的,我们就用Python的 execjs 来执行 JavaScript 代码直接获得就可以了。
但是这里又会有一个问题,我们用浏览器打开的是为它提供了一个浏览器的运行环境,我们在 python 使用的 execjs 提供的是 node 环境,两个环境的不一样,就会产生不同的效果,下面我们可以选择使用 webstorm 编辑器来提供 nodejs 环境来进行尝试以下。
修改加密代码
需要修改的地方有两个
这里它会先去判断有没有 window 这个对象来判断是不是在浏览器上面运行的,所以我们可以直接把它修改成 true 或者其他表示成 true 的值都可以
这个 atob 是将 base64 加密的字符串给解密,在 node 环境下是没有这个方法的,我们需要使用 Buffer.toString()替代即可,因为在浏览器上的 base64 加密的是 binary 编码,解密之后也就同样需要使用 binary 编码。
可以看到,成功的模拟出了加密字符串,但是不知道是什么原因我每次模拟出来的加密字符串的末尾都会多出4位没用的字符。所以在Python中调用加密函数得到加密字符串后需要用切片将这4位多余的字符串去掉。
接下来就是用Python环境测试了
注意需要先安装 PyExecJS 库
登录顺序是先获取Cookie,然后在获取验证码,最后登录。
下面是完整的Python代码:
import os
from urllib import parse
import requests
import base64
import execjs
from PIL import Image
import time
import hmac
from hashlib import sha1
class zhihu_login(object):
def __init__(self):
self.session = requests.session()
self.header = {
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36 Edg/80.0.361.69"
}
self.session.headers.update(self.header)
self.timestamp=None
self.picture=None
self.signature=None
self.picture_url=None
def getcapture(self):
#获取验证码
message=self.session.get(url="https://www.zhihu.com/api/v3/oauth/captcha?lang=en").json()
# print(message["show_captcha"])
if message["show_captcha"]=="False":
self.picture=""
else:
print("需要验证码")
while True:
self.picture_url=self.session.put(url="https://www.zhihu.com/api/v3/oauth/captcha?lang=en").json()
with open("captcha.jpg","wb") as f:
f.write(base64.b64decode(self.picture_url["img_base64"]))
image=Image.open('captcha.jpg')
image.show()
self.picture=input("请输入验证码:")
time.sleep(2)
message1=self.session.post(url="https://www.zhihu.com/api/v3/oauth/captcha?lang=en",data={'input_text':self.picture})
# print("{},{}".format(message1.status_code,message1.text))
if message1.status_code==201:
break
else:
print("{} 请提交正确的验证码".format(message1.status_code))
def getcookies(self):
#获取cookice
homepage = "https://www.zhihu.com/"
self.session.post(homepage)
def get_signature(self):
#伪造signature
client_id="c3cef7c66a1843f8b3a9e6a1e3160e20"
sk="d1b964811afb40118a12068ff74a12f4"
h=hmac.new(sk.encode("utf-8"),digestmod=sha1)
h.update('password'.encode("utf-8"))
h.update(client_id.encode("utf-8"))
h.update("com.zhihu.web".encode("utf-8"))
h.update(self.timestamp.encode("utf-8"))
return h.hexdigest()
def login(self):
self.getcapture()
post_url = "https://www.zhihu.com/api/v3/oauth/sign_in"
self.timestamp = str(int(time.time()) * 1000)
self.getcookies()
xsrf=self.session.cookies.get("_xsrf")
post_data={
"client_id": "c3cef7c66a1843f8b3a9e6a1e3160e20",
'grant_type':'password',
"timestamp": self.timestamp,
"source": "com.zhihu.web",
"signature": self.get_signature(),
"username": "+86你的账户名",
"password": "你的密码",
"captcha": self.picture,
"lang": "en",
"utmSource":"",
"refSource":"https://www.zhihu.com/signin?next/",
}
header={
"x-xsrftoken":xsrf,
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36 Edg/80.0.361.69",
"content-type": "application/x-www-form-urlencoded",
"x-zse-83": "3_2.0",
}
self.session.headers.update(header)
post_data=parse.urlencode(post_data)#将字典转换为字符串
post_data=self.get_formdata(post_data)
response = self.session.post(post_url,data=post_data)
if response.status_code==201:
print(response.status_code,response.text)
print("登录成功")
else:
print("登录失败")
def get_formdata(self,post_data):
#调用formdata加密方法
with open("zhihu.js") as f:
jsdata=f.read()
ctx=execjs.compile(jsdata)
enprty_data=ctx.call('b',post_data)[:-4]
return enprty_data
if __name__=="__main__":
zh=zhihu_login()
data=zh.login()
可以看到已经登录成功了,可以进行后续的数据爬取操作了。
参考
https://zhuanlan.zhihu.com/p/57375111
https://zhuanlan.zhihu.com/p/34073256