2020.10.20 利用POST请求模拟登录知乎

前两天学习了Python的requests模块的相关内容,对于用GET和PSOT请求访问网页以抓取需要的内容有了初步的了解,想要再从一些复杂的网站积累些经验。最开始我采用最简单的get(url)方法想要抓取知乎热搜的标题,想着是个很简单的任务。但是耗费了我五天的时间才堪堪解决模拟登录知乎的问题,期间还查阅了十几个相关网站,解决了一堆问题,还没有实现抓取热搜的目的,不过最难的一步解决了,相信之后的提取网页内容的问题也会解决。

至于为什么学习内容会从“抓取知乎热搜”变成“模拟登录知乎”,是因为知乎它比较坑,不登录的话不显示热搜的内容,哪怕你访问了热搜的URL——www.zhihu.com/hot,它也给你跳到登录页面——www.zhihu.com/signin,所以要想抓取内容,就绕不开登录知乎。

也许还有其他简单的模块selenium,但本次并没有涉及到,等到以后有机会再去学习了解^_^。

接下来我就按照步骤说明我是如何一步一步的在前人文档的帮助下模拟登录知乎的,也许以后写下的代码会随着知乎的更新而失效,但是在此过程中的思想和思考方式却对类似问题的解决有所帮助。

一、查看请求的Headers

我们首先要清楚登录请求是发给谁的

①打开知乎登录页面,输入错误的账号和密码;同时F12打开开发者模式,监视在此过程中发送的各项请求。

 

②看到一个POST请求sign_in,看一下它的Response

 

 说明没错,就是这个Post请求;一般Post请求都会带一个FormData,检查后边,发现这个请求也不例外

 但是这个参数不是常见的Key:Value格式,说明做了加密处理,那么我们现在就需要找到它的加密逻辑,然后模拟出这个参数。

 

二、加密方法

①加密函数

目前已知的就只有这个sign_in,在开发者模式的Source模块中,通过全局搜索CTRL+SHIFT+F搜索一下sign_in

 只有这么一个js文件里边有sign_in,点进去之后,通过CTRL+F看看sign_in在这个文件中具体在哪。这里需要注意的是,网页为了节省资源,通常会省去语法格式,而将大量代码挤到一段中去,比如这样:

 这样我们的开发就显得十分不便,好在我们可以通过左下方的一个按钮来还原它们的格式:

 接着正文讲,我们在这个js文件中搜索到的sign_in有三个。可以通过调试+设置断点的方法判断哪个是我们需要的。这里就不贴调试的过程了。

运行到其中一个断点处,通过右边的Scope监视局部变量Local,可以发现这部分的局部变量包含了很多信息,可能以后会用到它。

 信息有了相当于加密函数的部分参数就有了,那么这些参数是通过哪个函数被加密从而形成了FormData了呢?这里的加密方法的寻找,我参考了别人的方法:加密一般都是用encrypt之类的名字,所以可以直接搜索encrypt:

 

 把鼠标停留在这个return后边的值,会自动显示这个值;将它与FormData相比较,发现一模一样,说明调用return后边的代码,起到了把信息加密的作用。

②加密函数的输入参数

接上文,如果我们把鼠标放在参数e上,

 

 可以得到加密函数的输入参数,即下面这些参数用&连接构成的字符串:

复制代码
client_id=c3cef7c66a1843f8b3a9e6a1e3160e20
grant_type=password
timestamp=1603276798679
source=com.zhihu.web
signature=d570b4b3cd3b7e473933ed5e9a10f714c383aa81
username%2B8615947657687
password=1111111
captcha=5e9x
lang=en
utm_source=
ref_source=other_https%3A%2F%2Fwww.zhihu.com%2Fsignin%3Fnext%3D%252F
复制代码

其中只有timestampsignaturecaptcha是会变化的,经过分析,发现:

timestamp:13位时间戳——Python自带的时间戳函数生成的是10位时间戳,乘以1000即可

captcha:动态输入的验证码——验证码的获取会在第三大部分的第②模块请求验证码中介绍,你也可以直接跳过去看。

signature:则是一个经过加密的属性,它和captcha都要通过额外编写函数来获取,不是可以直接得到的。但是获取方法并不难,下面介绍如何获取signature:

step1、在网页源码中,全局搜索signature,寻找看上去像进行加密的位置

 

 

 step2、再调试一次,分别在该处和之前的加密函数encrypt处设置断点,观察encrypt的输入参数esignature的值是否和该处signature的值相同,比较结果如下:

 

 

 

 

 一模一样,看来这里就是给signature进行加密的地方了。这就有两个问题了——如何加密,对谁加密

观察这部分代码,可以得到上边两个问题的答案:

a、如何加密——HMac加密模式,Hash函数:SHA-1

 

 

 b、对谁加密——就是面代码中的a

a的内容呢?

 

 

 可以看到,一共5项,分别是①“d1b964811afb40118a12068ff74a12f4”;②"password";③clientID;④"com.zhihu.web"timestamp

所以我们在Python中编写对Signature的加密函数时,就是利用上面提到的五项参数HMac函数进行的。这部分的代码如下:

复制代码
def get_signature(self):
    #获取Signature
    clientID=b'c3cef7c66a1843f8b3a9e6a1e3160e20'
    SK=b'd1b964811afb40118a12068ff74a12f4'
    h=hmac.new(SK,digestmod=hashlib.sha1)
    h.update(clientID)
    h.update(b'password')
    h.update(b'com.zhihu.web')
    h.update(self.timestamp.encode())
    return h.hexdigest()
复制代码

 

 知道了加密函数的位置,我们就可以把参与加密的所有js方法都提取出来,放在一个html文件内执行就可以了。

向上寻找这个return所在函数的头,把这个函数的内容全部复制到一个JS文件中, 总共400多行。

为了看看这个函数正确与否,我们可以把函数中的内容直接拿出来,就是去掉最外层function(module,exports,webpack_require),并把exports相关代码去掉(不去掉exports的话,是无法输出到html文件中的)。然后调用下面的函数b,把我们的FormData传进去。

将上边的JS文件嵌入一个html文件中,放在script标签内即可。

这里贴出检验时的HTML文件和JS文件的写法:

HTML

复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body style="word-break:break-all;">
    <h1>获得参数</h1>
        <script type="text/javascript" src="T.js">
        </script>
</body>
</html>
复制代码

T.js

//中间代码和之前400多行代码相同,我就不写了,只是注释掉所有和exports相关的代码,注意是 "所有",你可以通过CTRL+F查找与exports相关的代码
var b = function (e) {
  return __g._encrypt(encodeURIComponent(e));
};
document.write(b("client_id=c3cef7c66a1843f8b3a9e6a1e3160e20&grant_type=password&timestamp=1603252105039&source=com.zhihu.web&signature=1f47545c2389e3356a47837daadef46d63c8530d&username=%2B8615947657687&password=111111&captcha=&lang=en&utm_source=&ref_source=other_https%3A%2F%2Fwww.zhihu.com%2Fsignin%3Fnext%3D%252F"))

 

用浏览器打开,就可以看到加密的字符串也就是发送的FormData了。它与我们之前Request Header中的FormData的值相同(这里由于我是第二天做的,所以和前一天的FormData不同)。

 

 

 搞完这个后,我们就可以继续使用Python来操作了,因为加密方法格式化后有400多行,实在太多,也全都是混淆,不太可能用Python一个一个实现,所以这里选择用Python的execjs来直接执行JavaScript代码从而获得FormData,简单且方便。

不过要注意的是,前边我们写的JS文件是用来嵌入HTML并在网页中打开的,所以给这个JS文件提供的是Web环境。但是想在Python中通过execjs执行的JS是需要Node环境,所以需要对之前的JS代码稍加修改。

修改一、开头添加抬头

const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
window = dom.window;
document = window.document;

修改二、atob => window.atob

 

之后运行一下JS检查一下结果(这里我是在Pycharm中执行的JS文件,环境是NodeJS。在这个JS文件最后一行用console.log(b("client=..."))代替我们之前写的document.write将FormData输出到命令行检查)

 

可见Node环境下配置正确,运行结果正确。

这就是提取的加密函数并且进行加密的部分。

另外还有个问题我们一直忽略了,那就是加密函数的参数问题,是的,光有加密函数还不行,你得知道它是对哪些参数进行了加密才得到了最后的FormData;换言之,我们现在只是知道了输入-处理-输出中的处理输出部分,接下来,我们要看看输入的参数是哪些。

为什么我把输入放在最后说呢?是因为我们有个取巧的方法帮助我们获得这些参数,使我可以用较短的篇幅来说明有哪些参数。

①还是回到我们之前的encrypt部分的代码

 

 

 

三、模拟登录

①请求头headers信息

必须要有三个要素:User-AgentContent-Typex-zse-83

        headers.update({
            'content-type': 'application/x-www-form-urlencoded',
            'x-zse-83': '3_2.0',
            'x-xsrftoken': self._get_xsrf(),
            'User-Agent':'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'
        })

不带Content-Type,会出现错误:

Missing argument grant_type

不带x-zse-83,会出现错误:

请求参数异常,请升级客户端后重试

User-Agent就不用说了

 

②请求验证码

登录时的请求顺序是:

  • 请求验证码地址,看需不需要填写验证码,如果需要再请求一次,而且还需要再再请求一次查看验证码输入的正确与否不正确重复上述步骤。不需要填写验证码的时候就可以直接请求登录网址了。

此外,上述三次请求的验证码地址的请求方法都是不同的,从前到后分别是GETPUTPOST,且这三次请求的内容都是通过json编码,可以在请求之后通过json()方法解码为dict类型的对象。

 

 

 

 

 

 cptcha验证码,是通过GET请求单独的API接口返回是否需要验证码(无论是否需要,都要请求一次),如果是True则需要再次PUT请求获取图片的base64编码

知乎验证码有两种形式,一种是点击倒立文字,另一种是输入英文字符串。区别在于传入的参数langcn还是en。这里只实现第二种验证方式。

复制代码
def getcapture(self):
    api='https://www.zhihu.com/api/v3/oauth/captcha?lang=en'
    message=self.session.get(api).json() #第一次请求
    if message['show_captcha']=='False': #此时不需要验证码
        self.picture=''
    else: 
        print('需要验证码:')
        while True:
            self.picture_url=self.session.put()api.json()#第二次请求,获得图片的base64编码
            with open('captchar.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(api,data={'input_text':self.picture}) #第三次请求,POST提交验证码
        if message1.status_code==201:
                break
        else:
                print(f'{message1.status_code} 请提交正确的验证码')
复制代码

 

四、补充内容

a、验证验证码

验证验证码时的请求头只需要有User-Agent字段就可以了。

b、请求的所有阶段都要带上Cookie

知乎的Cookie值是验证码票据,来源于第一次GET请求验证码地址,就是第一次GET请求Response头部中的set-cookie要素中的那个。

 如果不带Cookie请求或者请求顺序不一样就有可能返回错误:

{"error":{"message":"缺少验证码票据","code":120002,"name":"ERR_CAPSION_TICKET_NOT_FOUND"}}

 

参考(按对我帮助由大到小排列):

最新 Python 模拟登录知乎

知乎最新版模拟登陆详解

2020年最新 Python 模拟登录知乎 支持验证码

爬虫之爬取B站视频及破解知乎登录方法(进阶)

python爬虫执行js代码-execjs

代码来自Github:

2020 年最新 Python 模拟登录知乎 支持验证码和 Cookies

posted @   ShineLe  阅读(1547)  评论(1编辑  收藏  举报
编辑推荐:
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示