本主题主要通过知乎的登录,来讲述反爬虫的破解技术,在反爬虫技术中最难点的还是是签名与登录加密,因为这两个都牵涉加密相关技术,更加重要的还需要从网站的茫茫Javascript脚本中找到加密的代码,这些代码千丝万缕,要剥离出来也是难事。万事都有规律,只要从一个网站的登录破解开始,很多网站的破解道理都一样,从浏览器的角度说,只要浏览器能访问的,爬虫就能访问,就看技术成本是多少。要掌握本文的技术,需要掌握如下技术:
- 浏览器的Javascript调试技术(本主题使用的Safari浏览器,其他浏览器大同小异);
- 简单的加密解密技术:BASE64,SHA1;
- 掌握Python调用Javascript(PyExecJS\ [ 支持Node\ ]或者js2py [ 支持 纯Javascript Core - ES6 ] )
- 掌握HTTP请求Python模块:requests;
- HTTP 协议与Web App与Web服务器的常识;
- 本例子使用Qt实现了一个界面;
说明:本例子只实现校验,登录与主页主题爬取;其他的功能可以根据需要开发。截图如下:
例子效果
一、知乎的加密脚本调试过程:
- 这个脚本来自:浏览器调试的结果,从HXR的url开始定位调试,可以跟踪到签名的生成,提交表单数据的加密。下面是调试的几个截图
1.1. 从登录的提交之前的行为开始:
从登录按钮开始
1.2. 找到按钮事件调用的函数名:
注意那个name后面的o,就是事件函数
1.3. 从按钮的click事件绑定的o函数跟踪
- 在这儿可以设置断点。
调试的第一个断点,慢慢执行,总有惊喜
1.4. 跟踪到签名代码
签名生成函数
1.5. 跟踪到加密函数
加密函数调用
1.6. 加密函数的生成
Q函数就是加密函数的封装,并exorts为default
1.7. 加密过程
image.png
1.8. 其他
- 过程跟踪很痛苦,大家自己调试,才是最好的理解之路。
二、脚本
- 脚本文件名:p05_custom_atob.js
window = {
'encodeURIComponent':encodeURIComponent
};
navigator = {
'userAgent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36'
}
function atob(e){
return new Buffer(e,'base64').toString('binary');
}
function s(e) {
return (s = "function" == typeof Symbol && "symbol" == typeof Symbol.t ? function(e) {
return typeof e
} : function(e) {
return e && "function" == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e
})(e)
}
// ...... 这里省略若干代码,自己可以拷贝保存。
var Q = function(e) {
return __g._encrypt(e)
};
- 上面代码中:window,navigator,atob是自己加的,不是来自知乎脚本。加这window,navigator是因为脚本需要BOM对象,atob是浏览器中window对象中的base64加密与解密函数(我们利用Node的Buffer来实现)。
三、加密函数的实现
# encoding = utf-8
import execjs
with open('p05_custom_atob.js') as fd:
js_Q = fd.read()
# 编译没有问题
ctx = execjs.compile(js_Q)
# 调用
re = ctx.call('Q','abcsdefgh')
print(re)
四、签名函数的实现过程
# encoding = utf-8
import execjs
import hmac
from hashlib import sha1
import time
def get_signature(now_):
# 签名由clientId,grantType,source,timestamp四个参数生成
h = hmac.new(
key='d1b964811afb40118a12068ff74a12f4'.encode('utf-8'),
digestmod=sha1)
grant_type = 'password'
client_id = 'c3cef7c66a1843f8b3a9e6a1e3160e20'
source = 'com.zhihu.web'
now = now_
h.update((grant_type + client_id + source + now).encode('utf-8'))
return h.hexdigest()
timestamp = str(int(time.time()*1000))
signature = get_signature(timestamp)
print(signature)
五、最终知乎登录的实现
- 利用上面两个函数
# coding = utf-8
import requests
import json
import base64
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import itchat
import sys
from login import Ui_dlg_login
import hmac
from hashlib import sha1
import time
import execjs
import bs4
class ZhihuCaptcha(QThread):
# 校验码信号(发送校验码图像)
sign_need_captcha = pyqtSignal(str, bytes)
# 校验码服务url,后面的lang表示用户是英文用户,校验码会返回英文数字校验
url_captcha='https://www.zhihu.com/api/v3/oauth/captcha?lang=en'
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15'
}
def __init__(self):
super(ZhihuCaptcha,self).__init__()
# 发起校验码查询请求- 第一次请求(是否需要校验码)
self.session = requests.Session()
# 设置HTTP请求头:User-Agent
self.session.headers = self.headers
def run(self):
response_1st = self.session.request(
method='get',
url=self.url_captcha)
# 返回的数据大致这个格式,可以使用抓包工具与浏览器分析处这个数据格式
# {
# "show_captcha": false
# }
is_captcha = json.loads(response_1st.content.decode())['show_captcha']
# 如果需要检验码,则需要下载校验码,并校验
if is_captcha:
print('这次登录需要校验码!')
# 下载校验码(使用的是put请求方法,可以从浏览器或者抓包工具分析出来)
# 第二次请求:下载校验码(png格式的图像,数据样例见下面注释:图像内容是压缩的字节序列)
# {
# "img_base64": "......"
# }
response_2nd = self.session.put(self.url_captcha)
# 得到Base_64加密的图像
img_base64 = json.loads(response_2nd.content.decode())['img_base64']
# 解密图像
img_bytes = base64.b64decode(img_base64)
# 保存或者显示图像
self.sign_need_captcha.emit('captcha', img_bytes)
# 校验校验码
else:
# 不需要校验码,不需要任何,处理,下面直接使用用户信息登录
print('这次登录,不需要校验码校验!')
self.sign_need_captcha.emit('no captcha', b'')
print('校验码线程结束')
# 第三次请求:校验校验码
def login_rquest(self, captcha_code, username, password):
#
data = {
'input_text': captcha_code
}
print(data)
res_captcha = self.session.post(self.url_captcha, data)
print(res_captcha.content.decode())
# 201 响应码表示已经接收请求,还需要继续处理
# 请求已经被实现,而且有一个新的资源已经依据请求的需要而建立,
# 且其 URI 已经随Location 头信息返回。
# 假如需要的资源无法及时建立的话,应当返回 '202 Accepted'。
if res_captcha.status_code == 201:
captcha_ok = json.loads(res_captcha.content.decode())['success']
if not captcha_ok:
print("校验码失败")
return 'captcha_err'
else:
print("校验码通过")
# 开始登录
timestamp = str(int(time.time() * 1000))
signature = self.get_signature(timestamp)
encrypt = self.get_encrypt(username, password, timestamp, signature, captcha_code)
print(encrypt)
data = encrypt
login_headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.1 Safari/605.1.15',
'Content-Type': 'application/x-www-form-urlencoded',
'x-zse-83': '3_1.1'}
url_login = 'https://www.zhihu.com/api/v3/oauth/sign_in'
res_login = self.session.post(url_login, data=data, headers=login_headers)
print(res_login.status_code, res_login.content.decode())
if 200 <= res_login.status_code < 300:
print('登录成功!')
# 爬取主页内容
url_home = 'https://www.zhihu.com'
res_home = self.session.post(url_home)
content_home = res_home.content.decode('utf-8')
# 解析
bs_content = bs4.BeautifulSoup(content_home, 'html.parser')
list_topics = bs_content.find_all('div',attrs={
"class": "ContentItem AnswerItem",
})
print()
print(len(list_topics))
if len(list_topics)>0:
# 第一条
topic = list_topics[0].get('data-zop')
json_topic = json.loads(topic)
print(json_topic['title'])
return json_topic['title']
# 签名函数
def get_signature(self, now_):
# 签名由clientId,grantType,source,timestamp四个参数生成
h = hmac.new(
key='d1b964811afb40118a12068ff74a12f4'.encode('utf-8'),
digestmod=sha1)
grant_type = 'password'
client_id = 'c3cef7c66a1843f8b3a9e6a1e3160e20'
source = 'com.zhihu.web'
now = now_
h.update((grant_type + client_id + source + now).encode('utf-8'))
return h.hexdigest()
# 提交信息加密函数
def get_encrypt(self, username, password, timestamp, signature, captcha):
str_login = ""
str_login += "client_id=c3cef7c66a1843f8b3a9e6a1e3160e20&"
str_login += "grant_type=password&"
str_login += F"timestamp={timestamp}&"
str_login += "source=com.zhihu.web"
str_login += F"&signature={signature}&"
str_login += F"username={username}&"
str_login += F"password={password}&"
str_login += F"captcha={captcha}&"
str_login += "lang=en&"
str_login += "ref_source=homepage&"
str_login += "utm_source="
with open('p05_custom_atob.js', 'r') as fd:
js_zhihu = fd.read()
ctx = execjs.compile(js_zhihu)
encrypt_ = ctx.call('Q', str_login)
return encrypt_
# 校验码窗体
class LoginDialog(QDialog):
# 初始化窗体
def __init__(self, parent=None):
super().__init__()
self.th = ZhihuCaptcha()
self.ui = Ui_dlg_login()
self.setGeometry(100, 100, 400, 300)
self.setWindowTitle('用户登录')
self.ui.setupUi(self)
self.ui.edt_username.setText('你的电话')
self.ui.edt_password.setText('密码')
# 处理信号
self.th.sign_need_captcha.connect(self.query_captcha)
self.th.start()
self.show()
def query_captcha(self, status, img_bytes):
if status == 'no captcha':
# 显示校验码提示
self.ui.lbl_captcha.setText('不需要验证码')
self.ui.lbl_captcha.setVisible(True)
if status == 'captcha':
# 显示校验码
img_captcha = QImage.fromData(img_bytes)
pix_captcha = QPixmap.fromImage(img_captcha)
self.ui.lbl_captcha.setPixmap(pix_captcha)
self.ui.lbl_captcha.setScaledContents(True)
self.ui.lbl_captcha.setVisible(True)
self.ui.edt_captcha.setVisible(True)
def login(self):
# 调用登录函数(登录失败会返回信号)
re_login = self.th.login_rquest(
self.ui.edt_captcha.text(),
self.ui.edt_username.text(),
self.ui.edt_password.text())
self.setWindowTitle(re_login)
app = QApplication(sys.argv)
ui_login = LoginDialog()
sys.exit(app.exec())
网友评论