Songqian Li's Blog

去历史上留点故事

引言

正学着scrapy呢,偶然发现模拟登录知乎的教程跟实际不同,这才发觉知乎改版后在登录上添加了很多的反爬字段,于是便诞生了这篇文章。

准备

Python 类库

执行 js 代码 pip install PyExecJS
利用 requests 上传 multipart/form-data 格式文件 pip install requests_toolbelt
用于 base64 编码图片数据的解析 import base64  #python自带类库
用于显示图片的 PIL 或 pillow pip install pillow

开始

分析登录的 request head


发现几个不同的信息头:

1.aurhorization

多测试了几次,发现这个的值没有发生变化
c3cef7c66a1843f8b3a9e6a1e3160e20

2.Content-Type

感觉像 js 生成的加密字段,先往后看

3.X-UDID

Ctrl+Shift+F 全局搜索发现:

可以用 css、xpath、正则表达式来提取出来了

4.X-Xsrftoken

全局搜索发现该字段紧跟在 UDID 后面,这时候恍然发现,这是个 json 字符串,一格式化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"loading": {
"global": {
"count": 0
},
"local": {
"token/": false,
"env/getExperiments/": false,
"config/getAppConfig/": false
}
},
"entities": {
"users": {},
"questions": {},
"answers": {},
"articles": {},
"columns": {},
"topics": {},
"roundtables": {},
"favlists": {},
"comments": {},
"notifications": {},
"ebooks": {},
"activities": {},
"feeds": {},
"pins": {},
"promotions": {}
},
"currentUser": "",
"token": {
"xsrf": "2164a9cc-e7da-400b-a2cb-2e456d189680",
"xUDID": "AAACBxCF7guPTkkPXGTN-jrD3cd7krlcxoA="
},
······(有些长后面省略了)
}

发现该字段在json_data['token']['xsrf']

分析表单字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
------WebKitFormBoundaryBcTuMAttnsBhtQPT
Content-Disposition: form-data; name="client_id"

c3cef7c66a1843f8b3a9e6a1e3160e20
------WebKitFormBoundaryBcTuMAttnsBhtQPT
Content-Disposition: form-data; name="grant_type"

password
------WebKitFormBoundaryBcTuMAttnsBhtQPT
Content-Disposition: form-data; name="timestamp"

1514945556611
------WebKitFormBoundaryBcTuMAttnsBhtQPT
Content-Disposition: form-data; name="source"

com.zhihu.web
------WebKitFormBoundaryBcTuMAttnsBhtQPT
Content-Disposition: form-data; name="signature"

5e2ca2d664e45e4a6ab34f31a87dcdf8937a4149
------WebKitFormBoundaryBcTuMAttnsBhtQPT
Content-Disposition: form-data; name="username"

+8612345678899
------WebKitFormBoundaryBcTuMAttnsBhtQPT
Content-Disposition: form-data; name="password"

hahahahaha
------WebKitFormBoundaryBcTuMAttnsBhtQPT
Content-Disposition: form-data; name="captcha"


------WebKitFormBoundaryBcTuMAttnsBhtQPT
Content-Disposition: form-data; name="lang"

en
------WebKitFormBoundaryBcTuMAttnsBhtQPT
Content-Disposition: form-data; name="ref_source"

homepage
------WebKitFormBoundaryBcTuMAttnsBhtQPT
Content-Disposition: form-data; name="utm_source"


------WebKitFormBoundaryBcTuMAttnsBhtQPT--

可以看到参数是以 payload 的形式出现的,这个时候就要结合请求头Content-Type

1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryBcTuMAttnsBhtQPT

multipart/form-data 是一种表单提交的方式,后面的 boundary=xxx 是表单分割的方式
用 k-v 方式表示就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
data = {
'client_id': 'c3cef7c66a1843f8b3a9e6a1e3160e20',
'grant_type': 'password',
'timestamp': 1514945556611,
'source': 'com.zhihu.web',
'signature':5e2ca2d664e45e4a6ab34f31a87dcdf8937a4149,
'username': +8612345678899,
'password': hahahahaha,
'captcha': "",
'lang': 'en',
'ref_source': 'homepage',
'utm_source': ''
}

发现表单里除了signature字段,其他都是用户名,密码,时间戳,验证码等固定字段。

查找signature字段

From python 模拟登陆知乎(最新版)

有先人经过辛苦调试找到了生成该字段值的 js 代码,现在就可以使用 Python 执行下 js 代码来生成。

编写代码

1
本文由于只实现登录的功能,所以就使用requests库来进行数据爬取。

1.定制 request head

定义全局变量:

1
2
3
4
5
6
7
8
9
10
s = requests.session() #用来发起请求的requests对象
s.cookies = cookielib.LWPCookieJar(filename="cookie.txt") #使用cookielib库来保存cookie
content_type = "multipart/form-data; boundary=----WebKitFormBoundaryW1NyxWWwr7BAQ5e6" #加密字符串 boundary后面值为随机值

agent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.32 Mobile Safari/537.36"
header = { #通用header
"HOST": "www.zhihu.com",
"Referer": "https://www.zhihu.com",
"User-Agent": agent
}

获取和设置特殊字段值:

1
2
3
4
5
6
7
8
9
10
res = s.get("https://www.zhihu.com", headers=header)
match_obj = re.match('.*id="data".*data-state="(.*?)".*', res.text, re.DOTALL) #正则表达式获取json字符串
json_data = {}
if match_obj:
data_state = match_obj.group(1).replace(""", '"')
json_data = json.loads(data_state) #字符串转json对象
headers = header
headers['X-UDID'] = json_data['token']['xUDID']
headers['X-Xsrftoken'] = json_data['token']['xsrf']
headers['authorization'] = "oauth c3cef7c66a1843f8b3a9e6a1e3160e20"

2. 定制 form-data

主要是获取signature字段,直接执行 js 代码,具体见文章尾的源码吧

3.获取登录验证码

正常来讲,知乎很少会验证验证码,这里是为了以防万一从而添加了英文验证码,中文验证码不考虑

验证码的验证一是需要在登录的表单里通过captha字段提交,二是需要向验证码端口https://www.zhihu.com/api/v3/oauth/captcha?lang=eninput_text字段提交。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def checkcapthca(headers, cn=False):
# 检查是否需要验证码,无论需不需要,必须要发一个请求
if cn:
url = 'https://www.zhihu.com/api/v3/oauth/captcha?lang=cn'
else:
url = 'https://www.zhihu.com/api/v3/oauth/captcha?lang=en'
show_captcha = s.get(url, headers=headers)
if show_captcha.json()['show_captcha']:
req = s.put(url, headers=headers)
print(req)
with open('captcha.jpg', 'wb') as f:
img_data = base64.b64decode(req.json()['img_base64'])
# img_src = "data:image/jpg;base64," + req.json()['img_base64']
f.write(img_data)
f.close()
try:
im = Image.open('captcha.jpg')
im.show()
im.close()
except:
print(u'请到 %s 目录找到captcha.jpg 手动输入' % os.path.abspath('captcha.jpg'))
captcha = input("请输入验证码:\n>")
else:
captcha = ''
data = {
'input_text': captcha
}
encoder = MultipartEncoder(data, boundary="----WebKitFormBoundarycGPN1xiTi2hCSKKZ")
headers['Content-Type'] = encoder.content_type
res = s.post(url, data=encoder.to_string(), headers=headers)
return captcha

整个的判断过程是:

  1. 知乎首先向验证码端口发送get请求,用来确认是否需要验证验证码
  2. 如果需要验证码,则向验证码端口发起put请求,获取base64编码的图片地址
  3. 最后向验证码端口发起post请求,用input_text字段来第一次验证验证码是否正确。
  4. 将当前输入的验证码答案加到登录表单的captha表单中通过post请求提交。
  5. 登陆成功。返回用户数据。

4.发起登录请求

1
2
3
4
5
6
7
post_url = "https://www.zhihu.com/api/v3/oauth/sign_in"
data = getdata(account, password)
data['captcha'] = checkcapthca(headers)
encoder = MultipartEncoder(data, boundary="----WebKitFormBoundarycGPN1xiTi2hCSKKZ")#将数据转换成multipart/form-data;格式
headers['Content-Type'] = encoder.content_type #添加请求头字段
res = s.post(post_url, data=encoder.to_string(), headers=headers)
s.cookies.save()//保存cookie,登录时需验证,必需

源码地址

[http://www.github.com/lisongqian/file/zhihu_login.py]

相关文章
评论
分享
  • WordCount

    I.引言 值此情人节之际,祝天下有情人终成眷属。 1The knowledage of this article is shallow. Please read it in an ornamental angle. 本文使用的是...

    WordCount
  • 《操作系统真象还原》:第十章 输入输出系统

    上一章中我们遇到的字符混乱和 GP 异常问题,根本原因是由于临界区代码的资源竞争,这需要一些互斥的方法来保证操作的原子性。 10.1 同步机制——锁 10.1.1 排查 GP 异常,理解原子操作 多线程执行刷屏时光标值越界导致...

    《操作系统真象还原》:第十章 输入输出系统
  • 《操作系统真象还原》:第九章 线程

    线程和进程将分两部分实现,本章先讲解线程。 9.1 实现内核线程 9.1.1 执行流 在处理器数量不变的情况下,多任务操作系统采用多道程序设计的方式,使处理器在所有任务之间来回切换,这称为“伪并行”,由操作系统中的任务调度器决定当...

    《操作系统真象还原》:第九章 线程
  • GPU虚拟化

    用户层虚拟化 本地 API 拦截和 API formwarding 在用户态实现一个函数库,假设叫 libwrapper, 它要实现底层库的所有 API; 让 APP 调用这个 libwrapper。如何做? libwrap...

    GPU虚拟化
  • 硬件虚拟化

    硬件虚拟化介绍 硬件虚拟化要做的事情 体系结构支持 体系结构 实现功能 作用 模式切换 Host CPU <-> Guest CPU 切换 CPU 资源隔离 二阶段地址转换 GVA-> GPA...

    硬件虚拟化
  • 《操作系统真象还原》:第八章 内存管理系统

    8.1 makefile 简介 这部分可参考阮一峰的讲解:https://www.ruanyifeng.com/blog/2015/02/make.html 8.1.1 makefile 是什么 makefile 是 Linu...

    《操作系统真象还原》:第八章 内存管理系统
  • 《操作系统真象还原》:第七章 中断

    7.1 中断是什么,为什么要有中断 运用中断能够显著提升并发,从而大幅提升效率。 7.2 操作系统是中断驱动的 略 7.3 中断分类 把中断按事件来源分类,来自 CPU 外部的中断就称为外部中断,来自 CPU 内部的中断称为内部...

    《操作系统真象还原》:第七章 中断
  • 《操作系统真象还原》:第六章 完善内核

    6.1 函数调用约定简介 咱们实验使用cdecl。这里提一下stdcall,cdecl与stdcall的区别在于由谁来回收栈空间。 stdcall是被调用者清理参数所占的栈空间。 举例来说: 12int subtract(int ...

    《操作系统真象还原》:第六章 完善内核
  • 《操作系统真象还原》:第五章 保护模式进阶——加载内核

    5.3 加载内核 5.3.1 用 C 语言写内核 第一个 C 语言代码: 1234int main(void) { while(1); return 0;} 这个内核文件什么都没做,通过while(1)这个死循...

    《操作系统真象还原》:第五章 保护模式进阶——加载内核
  • 《操作系统真象还原》:第五章 保护模式进阶——内存分页机制

    从这一刻起,我们才算开始了真正的操作系统学习之旅 5.1 获取物理内存容量 5.1.1 Linux 获取内存的方法 在 Linux 2.6 内核总是用detect_memory函数来获取内存容量的。其函数本质上是通过调用 BI...

    《操作系统真象还原》:第五章 保护模式进阶——内存分页机制