一、常见反爬手段和解决思路:
1. 明确反反爬的主要思路:
反反爬的主要思路就是:尽可能的去模拟浏览器,浏览器在如何操作,代码中就如何去实现。
例如:浏览器先请求了地址url1,保留了cookie在本地,之后请求地址url2,带上了之前的cookie,代码中也可以这样去实现。
2.通过headers字段来反爬:
headers中有很多字段, 这些字段都有可能会被对方服务器拿过来进行判断是否为爬虫
2.1 通过headers中的User-Agent字段来反爬:
- 反爬原理: 爬虫默认情况下没有User-Agent;
- 解决方法: 请求之前添加User-Agent即可; 更好的方式是使用User-Agent池来解决(收集一堆User-Agent的方式, 或者是随机生成User-Agent);
impoer random
def get_ua():
first_num = random.randint(55, 62)
third_num = random.randint(0, 3200)
fourth_num = random.randint(0, 140)
os_type = [
'(Windows NT 6.1; WOW64)', '(Windows NT 10.0; WOW64)', '(X11; Linux x86_64)',
'(Macintosh; Intel Mac OS X 10_12_6)'
]
chrome_version = 'Chrome/{}.0.{}.{}'.format(first_num, third_num, fourth_num)
ua = ' '.join(['Mozilla/5.0', random.choice(os_type), 'AppleWebKit/537.36','(KHTML, like Gecko)', chrome_version, 'Safari/537.36'])
return ua
2.2 通过referer字段或者其他字段来反爬:
- 反爬原理: 爬虫默认情况下不会带上referer字段;
- 解决方法:添加referfer字段
- 例如: 豆瓣美剧爬虫
#coding=utf-8
import random
import requests
import jsonpath
import json
import time
def get_ua():
first_num = random.randint(55, 62)
third_num = random.randint(0, 3200)
fourth_num = random.randint(0, 140)
os_type = [
'(Windows NT 6.1; WOW64)',
'(Windows NT 10.0; WOW64)', '(X11; Linux x86_64)',
'(Macintosh; Intel Mac OS X 10_12_6)'
]
chrome_version = 'Chrome/{}.0.{}.{}'.format(first_num, third_num,fourth_num)
ua = ' '.join(['Mozilla/5.0',
random.choice(os_type),
'AppleWebKit/537.36',
'(KHTML, like Gecko)',
chrome_version, 'Safari/537.36'
])
return ua
class Douban():
def __init__(self):
self.start_url = "https://m.douban.com/rexxar/api/v2/subject_collection/tv_american/items?os=windows&for_mobile=1&callback=jsonp1&start={}&count={}&loc_id={}"
self.headers= {
"User-Agent": get_ua(),
"Referer": "https://m.douban.com/tv/american"
}
self.contents = []
def get_content(self, start=0,count=20):
"""获取网页"""
url = self.start_url.format(start, count,int(time.time()))
response = requests.get(url=self.start_url, headers=self.headers)
html_str = response.content.decode()[8:-2]
jsonobj = json.loads(html_str)
return jsonobj["subject_collection_items"]
def get_list(self, json_obj):
"""获取美剧的信息"""
content_list = []
for item in json_obj:
content_item = {}
content_item['title'] = jsonpath.jsonpath(item,'$..title')
content_item['image_url'] = jsonpath.jsonpath(item,'$..cover[url]')
content_item['year'] = jsonpath.jsonpath(item, '$..year')
content_item['directors'] = jsonpath.jsonpath(item, '$..directors')
content_item['actors'] = jsonpath.jsonpath(item, '$..actors')
content_list.append(content_item)
return content_list
def save_contents(self):
"""数据保存逻辑"""
for item in self.contents:
print(item)
def run(self):
"""执行爬虫"""
index = 0
while index <= 10:
json_obj = self.get_content(start=index*20)
content_list = self.get_list(json_obj)
for item in content_list:
self.contents.append(item)
index += 1
time.sleep(1)
if __name__ == "__main__":
douban = Douban()
douban.run()
douban.save_contents()
2.3 通过cokie来反爬:
- 如果目标网站不需要登录, 每次请求带上前一次返回的cookie, 比如requests模块的session;
- 如果目标网站需要登录,准备多个账号,通过一个程序获取账号对应的cookie,组成cookie池, 其他程序使用这些cookie;
3. 通过js来反爬:
普通的爬虫默认情况下无法执行js, 获取js执行之后的结果, 所以很多时候对方服务器会通过js技术实现反爬.
3.1通过js实现跳转来反爬:
- 反爬原理: js实现页面跳转, 肉眼不可见;
- 解决方法: 在chrome中点击perserve log按钮,观察页面跳转情况;
在这些请求中,如果请求数量很多,一般来讲,这有那些response中带cookie字段的请求是有用的, 意味着通过这个请求,对方服务器有设置cookie到本地;
3.2 通过js生成请求参数:
- 反爬原理: js生成请求参数;
- 解决方法: 分析js, 观察加密的实现过程,通过js2py获取js的执行结果,或使用selenium来实现;
3.3 通过js实现数据加密:
- 反爬原理: js实现了数据的加密
- 解决方法: 分析js, 观察加密的实现过程,通过js2py获取js的执行结果,或使用selenium来实现;
4. 通过验证码来反爬:
- 反爬原理: 对方服务器通过弹出验证码强制验证用户浏览行为
- 解决方法:打码平台或者是机器学习的方法识别验证码,其中打码平台廉价易用,更值得推荐
5 通过ip地址来反爬
- 反爬原理:正常浏览器请求网站,速度不会太快,同一个ip大量请求了对方服务器,有更大的可能性会被识别为爬虫
- 解决方法:对应的通过购买高质量的ip的方式能够解决问题
6 通过用户行为来反爬
- 反爬原理:通过浏览器请求数据,很多用户行为会在浏览器中是很容易实现或者无法实现.比如浏览器请求额外的图片地址,服务端进行记录,出现意味着不是爬虫(爬虫中不会主动请求图片)
- 解决方法:通过获取数据的情况来观察请求,寻找异常出现的可能请求
7 其他的反爬方式
7.1 通过自定义字体来反爬
下图来自猫眼电影电脑版:
解决思路:切换到手机版
7.2 通过css来反爬:
猫眼去哪儿电脑版
解决思路:计算css的偏移
8.小结:
- 反爬的手段非常多,但是一般而言,完全的模仿浏览器的行为即可
二、打码平台的使用:
1 为什么需要了解打码平台的使用
现在很多网站都会使用验证码来进行反爬,所以为了能够更好的获取数据,需要了解如何使用打码平台爬虫中的验证码
2 常见的打码平台
2.1 云打码:
网址: http://www.yundama.com/
能够解决通用的验证码识别
2.2 极验验证码智能识别辅助:
网址: http://jiyandoc.c2567.com/
能够解决复杂验证码的识别
3 云打码的使用
下面以云打码为例,了解打码平台如何使用
3.1 云打码官方接口
下面代码是云打码平台提供,做了个简单修改,实现了两个方法:
- indetify:传入图片的响应二进制数即可
- indetify_by_filepath:传入图片的路径即可识别
其中需要自己配置的地方是:
username = 'whoarewe' # 用户名
password = '***' # 密码
appid = 4283 # appid
appkey = '02074c64f0d0bb9efb2df455537b01c3' # appkey
codetype = 1004 # 验证码类型
云打码官方提供的api如下:
#yundama.py
import requests
import json
import time
class YDMHttp:
apiurl = 'http://api.yundama.com/api.php'
username = ''
password = ''
appid = ''
appkey = ''
def __init__(self, username, password, appid, appkey):
self.username = username
self.password = password
self.appid = str(appid)
self.appkey = appkey
def request(self, fields, files=[]):
response = self.post_url(self.apiurl, fields, files)
response = json.loads(response)
return response
def balance(self):
data = {'method': 'balance', 'username': self.username, 'password': self.password, 'appid': self.appid,
'appkey': self.appkey}
response = self.request(data)
if (response):
if (response['ret'] and response['ret'] < 0):
return response['ret']
else:
return response['balance']
else:
return -9001
def login(self):
data = {'method': 'login', 'username': self.username, 'password': self.password, 'appid': self.appid,
'appkey': self.appkey}
response = self.request(data)
if (response):
if (response['ret'] and response['ret'] < 0):
return response['ret']
else:
return response['uid']
else:
return -9001
def upload(self, filename, codetype, timeout):
data = {'method': 'upload', 'username': self.username, 'password': self.password, 'appid': self.appid,
'appkey': self.appkey, 'codetype': str(codetype), 'timeout': str(timeout)}
file = {'file': filename}
response = self.request(data, file)
if (response):
if (response['ret'] and response['ret'] < 0):
return response['ret']
else:
return response['cid']
else:
return -9001
def result(self, cid):
data = {'method': 'result', 'username': self.username, 'password': self.password, 'appid': self.appid,
'appkey': self.appkey, 'cid': str(cid)}
response = self.request(data)
return response and response['text'] or ''
def decode(self, filename, codetype, timeout):
cid = self.upload(filename, codetype, timeout)
if (cid > 0):
for i in range(0, timeout):
result = self.result(cid)
if (result != ''):
return cid, result
else:
time.sleep(1)
return -3003, ''
else:
return cid, ''
def post_url(self, url, fields, files=[]):
# for key in files:
# files[key] = open(files[key], 'rb');
res = requests.post(url, files=files, data=fields)
return res.text
username = 'whoarewe' # 用户名
password = '***' # 密码
appid = 4283 # appid
appkey = '02074c64f0d0bb9efb2df455537b01c3' # appkey
filename = 'getimage.jpg' # 文件位置
codetype = 1004 # 验证码类型
# 超时
timeout = 60
def indetify(response_content):
if (username == 'username'):
print('请设置好相关参数再测试')
else:
# 初始化
yundama = YDMHttp(username, password, appid, appkey)
# 登陆云打码
uid = yundama.login();
print('uid: %s' % uid)
# 查询余额
balance = yundama.balance();
print('balance: %s' % balance)
# 开始识别,图片路径,验证码类型ID,超时时间(秒),识别结果
cid, result = yundama.decode(response_content, codetype, timeout)
print('cid: %s, result: %s' % (cid, result))
return result
def indetify_by_filepath(file_path):
if (username == 'username'):
print('请设置好相关参数再测试')
else:
# 初始化
yundama = YDMHttp(username, password, appid, appkey)
# 登陆云打码
uid = yundama.login();
print('uid: %s' % uid)
# 查询余额
balance = yundama.balance();
print('balance: %s' % balance)
# 开始识别,图片路径,验证码类型ID,超时时间(秒),识别结果
cid, result = yundama.decode(file_path, codetype, timeout)
print('cid: %s, result: %s' % (cid, result))
return result
if __name__ == '__main__':
pass
3.2 代码中调用云打码的接口
下面以豆瓣登录过程中的验证码为例,了解云打码如何使用
豆瓣登录
4 常见的验证码的种类
4.1 url地址不变,验证码不变
这是验证码里面非常简单的一种类型,对应的只需要获取验证码的地址,然后请求,通过打码平台识别即可
4.2 url地址不变,验证码变化
这种验证码的类型是更加常见的一种类型,对于这种验证码,需要思考:
在登录的过程中,假设输入的验证码是对的,对方服务器是如何判断当前输入的验证码是显示在屏幕上的验证码,而不是其他的验证码呢?
在获取网页的时候,请求验证码,以及提交验证码的时候,对方服务器肯定通过了某种手段验证我之前获取的验证码和最后提交的验证码是同一个验证码,那这个手段是什么手段呢?
很明显,就是通过cookie来实现的,所以对应的,在请求页面,请求验证码,提交验证码的到时候需要保证cookie的一致性,对此可以使用requests.session来解决
三、JS的解析:
1. 确定JS的位置:
对于人人网的案例,url地址中有部分参数,但是参数是如何生成的呢?
毫无疑问,参数肯定是js生成的,那么如何获取这些参数的规律呢?
1.1 观察按钮的绑定js事件
在这里插入图片描述
通过点击按钮,然后点击Event Listener,部分网站可以找到绑定的事件,对应的,只需要点击即可跳转到js的位置
1.2 通过search all file 来搜索
部分网站的按钮可能并没有绑定js事件监听,那么这个时候可以通过搜索请求中的关键字来找到js的位置,比如livecell
点击美化输出选项
可以继续在其中搜索关键字
2 观察js的执行过程
找到js的位置之后,可以来通过观察js的位置,找到js具体在如何执行,后续可以通过python程序来模拟js的执行,或者是使用类似js2py直接把js代码转化为python程序去执行
观察js的执行过程最简单的方式是添加断点
添加断点的方式:在左边行号点击即可添加,对应的右边BreakPoints中会出现现有的所有断点
添加断点之后继续点击登录,每次程序在断点位置都会停止,通过如果该行有变量产生,都会把变量的结果展示在Scoope中
在上图的右上角有1,2,3三个功能,分别表示:
- 1:继续执行到下一个断点
- 2:进入调用的函数中
- 3:从调用的函数中跳出来
3. js2py的使用
在知道了js如何生成想要的数据之后,那么接下来就需要使用程序获取js执行之后的结果了
3.1 js2py的介绍
js2py是一个js的翻译工具,也是一个通过纯python实现的js的解释器,github上源码与示例
3.2 js的执行思路
js的执行方式大致分为两种:
- 在了解了js内容和执行顺序之后,通过python来完成js的执行过程,得到结果;
- 在了解了js内容和执行顺序之后,使用类似js2py的模块来执js代码,得到结果;
但是在使用python程序实现js的执行时候,需要观察的js的每一个步骤,非常麻烦,所以更多的时候会选择使用类似js2py的模块去执行js,接下来使用js2py实现人人网登录参数的获取
3.3 具体的实现
定位进行登录js代码
formSubmit: function() {
var e, t = {};
$(".login").addEventListener("click", function() {
t.phoneNum = $(".phonenum").value,
t.password = $(".password").value,
e = loginValidate(t),
t.c1 = c1 || 0,
e.flag ? ajaxFunc("get", "http://activity.renren.com/livecell/rKey", "", function(e) {
var n = JSON.parse(e).data;
if (0 == n.code) {
t.password = t.password.split("").reverse().join(""),
setMaxDigits(130);
var o = new RSAKeyPair(n.e,"",n.n)
, r = encryptedString(o, t.password);
t.password = r,
t.rKey = n.rkey
} else
toast("公钥获取失败"),
t.rKey = "";
ajaxFunc("post", "http://activity.renren.com/livecell/ajax/clog", t, function(e) {
var e = JSON.parse(e).logInfo;
0 == e.code ? location.href = localStorage.getItem("url") || "" : toast(e.msg || "登录出错")
})
}) : toast(e.msg)
})
}
从代码中知道:
- 要登录需要对密码进行加密和获取rkey字段的值
- rkey字段的值直接发送请求rkey请求就可以获得
- 密码是先反转然后使用RSA进行加密, js代码很复杂, 希望能通过在python中执行js来实现
实现思路:
- 使用session发送rKey获取登录需要信息
url: http://activity.renren.com/livecell/rKey
方法: get - 根据获取信息对密码进行加密 :
- 2.1 准备用户名和密码;
- 2.2 使用js2py生成js的执行环境:context;
- 2.3 拷贝使用到js文件的内容到本项目中;
- 2.4 读取js文件的内容,使用context来执行它们;
- 2.5 向context环境中添加需要数据;
- 2.6 使用context执行加密密码的js字符串;
- 2.7 通过context获取加密后密码信息;
3 使用session发送登录请求
- URL: http://activity.renren.com/livecell/ajax/clog
- 请求方法: POST
- 数据:
phoneNum: xxxxxxx
password: (加密后生产的)
c1: 0
rKey: rkey请求获取的
具体代码:
需要提前下载几个js文件到本地:
BigInt.js
RSA.js
Barrett.js
import requests
import json
import js2py
# - 实现思路:
# - 使用session发送rKey获取登录需要信息
# - url: http://activity.renren.com/livecell/rKey
# - 方法: get
# 获取session对象
session = requests.session()
headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Mobile Safari/537.36",
"X-Requested-With": "XMLHttpRequest",
"Content-Type":"application/x-www-form-urlencoded"
}
# 设置session的请求头信息
session.headers = headers
response = session.get("http://activity.renren.com/livecell/rKey")
# print(response.content.decode())
n = json.loads(response.content)['data']
# - 根据获取信息对密码进行加密
# - 准备用户名和密码
phoneNum = "131..."
password = "****"
# - 使用js2py生成js的执行环境:context
context = js2py.EvalJs()
# - 拷贝使用到js文件的内容到本项目中
# - 读取js文件的内容,使用context来执行它们
with open("BigInt.js", 'r', encoding='utf8') as f:
context.execute(f.read())
with open("RSA.js", 'r', encoding='utf8') as f:
context.execute(f.read())
with open("Barrett.js", 'r', encoding='utf8') as f:
context.execute(f.read())
# - 向context环境中添加需要数据
context.t = {'password': password}
context.n = n
# - 执行加密密码的js字符
js = '''
t.password = t.password.split("").reverse().join(""),
setMaxDigits(130);
var o = new RSAKeyPair(n.e,"",n.n)
, r = encryptedString(o, t.password);
'''
context.execute(js)
# - 通过context获取加密后密码信息
# print(context.r)
password = context.r
# - 使用session发送登录请求
# - URL: http://activity.renren.com/livecell/ajax/clog
# - 请求方法: POST
# - 数据:
# - phoneNum: 15565280933
# - password: (加密后生产的)
# - c1: 0
# - rKey: rkey请求获取的
data = {
'phoneNum': '131....',
'password': password,
'c1':0,
'rKey':n['rkey']
}
# print(session.headers)
response = session.post("http://activity.renren.com/livecell/ajax/clog", data=data)
print(response.content.decode())
# 访问登录的资源
response = session.get("http://activity.renren.com/home#profile")
print(response.content.decode())
4. 小结
- 通过在chrome中观察元素的绑定事件可以确定js
- 通过在chrome中search all file 搜索关键字可以确定js的位置
- 观察js的数据生成过程可以使用添加断点的方式观察
- js2py的使用
- 需要准备js的内容
- 生成js的执行环境
- 在执行环境中执行js的字符串,传入数据,获取结果
四、小结: