们已经学到很多反爬机制以及相应的反反爬策略。使用那些手段,其实已经完全可以完成绝大多数的爬虫任务。但是,还是有极个别的情况下,会出现诸如 JS 加密和 JS 混淆之类的高深反爬机制。
如果不幸遇到这种反爬机制,一个明智之举是给站长点个赞,然后恭恭敬敬选择放弃,去别的地方找数据。
当然,还是那句话,我们可以选择不爬,但是对付 JS 加密和 JS 混淆的方法却不可以不会。
这里就以中国空气质量在线检测平台为例,介绍 JS 加密和 JS 混淆的实现和破解方法。
要爬取的网站:https://www.aqistudy.cn/html/city_detail.html
这个网站正在升级,所以页面无法正常显示。这也意味着这个网站本身的 JS 解密是有问题的(如果没问题就能显示了),所以最后我们并不能完全解析出数据来。虽然如此,这个网站仍然是学习 JS 加密和 JS 混淆的相当不错的平台。
闲话少说,开始干活!
首先浏览器打开网页,并打开调试台的抓包工具。修改查询条件(城市的名称 + 时间范围),然后点击查询按钮,捕获点击按钮后发起请求对应的数据包。点击查询按钮后,并没有刷新页面,显然发起的是 ajax 请求。该请求就会将指定查询条件对应的数据加载到当前页面中(我们要爬取的数据就是该 ajax 请求请求到的数据)。
分析捕获到的数据包
该数据包请求到的是密文数据,为何在前台页面显示的却是原文数据呢?
原来,在请求请求到密文数据后,前台接受到密文数据后使用指定的解密操作(JS 函数)对密文数据进行了解密操作,然后将原文数据显示在了前台页面。
接下来的工作流程:
首先先处理动态变化的请求参数,动态获取该参数的话,就可以携带该参数进行请求发送,将请求到的密文数据捕获到。
抽丝剥茧,首先从 getData 函数实现中找寻 ajax 请求对应的代码。在该函数的实现中没有找到 ajax 代码,但是发现了另外两个函数的调用,getAQIData() 和 getWeatherData()。ajax 代码一定是存在于这两个函数实现内部。
另外,这里记住一个参数,type==’HOUR‘,它的含义是查询时间是以小时为单位。这个参数我们后来会用到。
接下来我们就去分析 getAQIData() 和 getWeatherData(),争取能够找到 ajax 代码。
我们找到这两个函数的定义位置,还是没有找到 ajax 请求代码。不过我们却发现它们同时调用了另外一个函数,getServerData(method,param,func,0.5)。它的参数的值可以为:
下一步当然就要找 getServerData 函数了,看看那个函数里面有没有我们一致想要的发送 ajax 请求的代码。
我们尝试着在页面中搜索,却找不到这个函数。很显然,它是被封装到其他 js 文件中了。这时,我们可以基于抓包工具做全局搜索。
好消息是,我们顺利找到了 getServerData 函数!坏消息是,这货长得一点也不像是函数。
这是因为,这段 JS 函数代码被加密的。这种加密的方式,我们称为 JS 混淆。
JS 混淆,也就是对核心的 JS 代码进行加密。
JS 反混淆,则是对 JS 加密代码进行解密。
接下来我们要做的,就是 JS 反混淆,让这段我们看不懂的东西,显现出庐山真面目。
我们用的方法十分简单粗暴,也就是暴力破解。使用这个网站就可以实现对 JS 混淆的暴力破解:https://www.bm8.com.cn/jsConfusion/
将 getServerData 函数所在的那一整行代码都复制过来,粘贴到这个网址的文本输入框中,然后点击 开始格式化 即可:
终于,我们看到了 getServerData 的代码,并且在其中发现了发送 ajax 的请求:
function getServerData(method, object, callback, period) {
const key=hex_md5(method + JSON.stringify(object));
const data=getDataFromLocalStorage(key, period);
if (!data) {
var param=getParam(method, object);
$.ajax({
url: '../apinew/aqistudyapi.php',
data: {
d: param
},
type: "post",
success: function (data) {
data=decodeData(data);
obj=JSON.parse(data);
if (obj.success) {
if (period > 0) {
obj.result.time=new Date().getTime();
localStorageUtil.save(key, obj.result)
}
callback(obj.result)
} else {
console.log(obj.errcode, obj.errmsg)
}
}
})
} else {
callback(data)
}
}
从这段代码中,我们不难得出下面这几个信息:
但是我们并不打算这么做。因为再继续深挖下去,难度将会陡然增加。此时我们已经很疲惫了,如果继续下去恐怕要疯掉。而且,JavaScript 和 Python 毕竟是两种语言,它们之间的方法和各种包都不相同。JavaScript 能实现的,Python 未必能够轻松完成。所以重新写一个加密和解密的脚本,并不是明智之举。
更好的解决方案是,我们提供请求的明文数据,通过网站自己的 JS 代码进行加密,得到加密的请求参数。使用这个参数,我们发送请求给服务端。拿到加密的响应数据后,再通过网站的 JS 代码进行解密。
也就是说,我们接下来需要做的就是要调用两个 JS 函数 decodeData 和 getParam,并拿到返回结果即可。
现在的问题是,在 Python 程序中如何调用 JS 函数呢?
这就涉及到一个新的概念:JS 逆向。JS 逆向,也就是在 Python 中调用 JS 函数代码。
能够实现 JS 逆向的方式有两种:
pip install PyExecJS
接下来,我们就可以生成加密的请求数据了。
首先,把我们解析出来的那串代码保存到本地,比如名为 code.js 的文件中。在里面我们补充一个函数,比如名字叫 getPostParamCode,用来发起我们的数据请求。之所以这样做是因为使用 PyExecJS 调用 JS 函数时,传入的参数只能是字符串。而 getParam 方法的参数需要用到 JS 的自定义对象。
我们只需在 code.js 中加上下面的代码即可:
function getPostParamCode(method, type, city, start_time, end_time) {
var param={};
param.type=type;
param.city=city;
param.start_time=start_time;
param.end_time=end_time;
return getParam(method, param)
}
然后,使用 PyExecJS 调用里面的 getParam 方法,将我们的请求数据加密:
# 模拟执行decodeData的js函数对加密响应数据进行解密
import execjs
import requests
node=execjs.get()
# 请求参数
method='GETCITYWEATHER'
type='HOUR'
city='北京'
start_time='2020-03-20 00:00:00'
end_time='2020-03-25 00:00:00'
# 编译js代码
file='code.js' # js代码的路径
ctx=node.compile(open(file, encoding='utf-8').read())
# 将请求数据加密
encode_js=f'getPostParamCode("{method}", "{type}", "{city}", "{start_time}", "{end_time}")'
params=ctx.eval(encode_js)
# 使用加密的参数,发起post请求
url='https://www.aqistudy.cn/apinew/aqistudyapi.php'
response_text=requests.post(url, data={'d': params}).text
# 将响应数据解密
decode_js=f'decodeData("{response_text}")'
decrypted_data=ctx.eval(decode_js) # 如果顺利,返回的将是解密后的原文数据
print(decrypted_data) # 执行会报错:目前页面中没有数据。解密函数只是针对页面中原始的数据进行解密。
自此,我们完成了 JS 加密和 JS 混淆的处理。这里我们总结一下这几个概念:
附,ajax 请求的各个数据的含义:
头条号每天坚持更新原创干货技术文章,欢迎关注本头条号"Linux学习教程",公众号名称“Linux入门学习教程"。
如需学习视频,请复制以下信息到手机浏览器或电脑浏览器上:
zcwyou.com
随着互联网活动的增加,网络攻击变得更加复杂和具有挑战性。
毫无疑问,互联网用户的数据安全意识已经成倍增长。如果你是站长或博主,保护用户的敏感数据和隐私不受网络罪犯的恶意侵害就成了你的首要责任。
在这里,SSL证书在加强您的网站安全性方面起着最有效和最关键的作用。因此,我们很有必要了解SSL的基本知识。
https vs http
安全套接字层(SSL)是一种安全协议,为互联网上传输的数据提供加密,并对网络服务器进行身份验证。当您提交任何敏感资料时,SSL会对您的数据进行加密,以确保数据得到充分保护和安全,并且只有预期的收件人才能理解。SSL证书是由证书颁发机构颁发给网站的数字证书,它保证了用户的web浏览器和web服务器之间的所有信息都是加密的。因此,SSL证书保护您的数据免受恶意窃取或破坏,如窃听、中间人攻击等。
一旦安装SSL证书,网站协议将从HTTP转移到安全的HTTPS。此外,您可以从浏览器中看到一个信任的标识,一个挂锁,被添加到您的网站的URL中。如上图所示。这可以确保访问者使用安全连接进行通信。SSL证书增加了网站的用户体验,并帮助提高其在谷歌搜索引擎结果页面上的排名。它还可以验证网站的真实性。但是你怎么检查这些信息呢?
ssl原理
在浏览器上单击地址栏中网址前面的挂锁,可以查看SSL证书信息。它还包含关于网站身份的信息。以下是SSL证书的主要内容:
web浏览器与web服务器通信,并使用该数据文件验证网站的身份和SSL证书的状态。
SSL证书安装在您的web服务器上后,它会以公钥和私钥的形式提供一个可识别的数字识别号码,用于服务器的认证。这些只不过是一长串任意生成的数字。它还允许服务器对用户和服务器之间交换的敏感信息进行加密和解密。
根据您的安全需求和预算,您可以从Internet上许多著名的证书颁发机构(CAs)提供的各种选择中购买SSL证书。因此,即使你预算有限,也要确保你的网站安全,使用有效的、便宜的SSL证书来赢得用户的信任,保护你的品牌声誉。因此,我们要了解不同类型的SSL证书。
SSL证书如何保护用户数据和隐私
各种验证级别
SSL证书的类型
证书颁发机构(CA)根据不同的验证级别需求颁发各种SSL证书。因此,SSL证书可以根据这些不同的验证级别过程进行分类,如下所示:
DV SSL证书申请验证过程非常简单,几乎不需要复杂的申请流程。因此,在SSL市场上可用的所有其他类型中,它需要的时间最短。DV SSL证书广泛用于非金融交易或用户敏感数据的小型网站。它们是最便宜的选择,也被广泛用于保护博客。它只能提供基础的认证。
CA需要进行域验证,并验证所有者公司的真实性后,才能签发OV SSL证书。OV SSL证书显示公司的身份信息,可通过访问证书上的信息查看。该公司的详细信息显示在“主题标签”中,如下面的例子所示:
ssl certification
中型企业比较普遍使用OV SSL证书,其发放时间比DV SSL证书长。由于证书所有者公司的验证过程需要更多的时间,所以比DV SSL证书的费用更高。
EV SSL证书与最高程度的信任和声誉相关联。EV SSL证书必须经过以下严格的认证后才能签发给组织。非常严格和彻底的背景验证是签发验证程序的一部分,如其地址、目前的运营状况、法律状况等。因此,与DV和OV SSL证书相比,此SSL证书的签发时间最长,成本也更高。该公司的名称显示在地址栏,因此可以验证。可信任的标识,网站Logo在网站上的展示增加了用户的信任。EV SSL证书广泛应用于银行、大型金融机构、电子商务网站等。
使用不同类型SSL证书确保网站安全的另一个决定因素是需要保护的域和子域的数量,它们如下:
单域SSL证书为您的网站保护一个域或子域。
为了保护多个域和子域,购买具有成本效益的通配符SSL证书或多域证书提供了更好的投资回报和更好的时间管理,而不是为每个域和子域使用单一域证书。
通配符SSL证书仅使用一个证书就可以保护主域和其所有子域。通配符证书使用格式为*.example.com的通用名称,因此它将使用单个证书来保护多个子域。如果考虑到预算问题,建议购买并安装通配符SSL证书,该证书将提供与较昂贵证书相同的加密级别。
多域或SAN(主题可选名称)SSL证书仅使用一个证书就可以保护多个域/子域。您可以添加和修改Subject Alternative Name字段,并轻松保护不同域和子域之间的多个名称。例如,只需一个多域(SAN) SSL证书。
统一通信证书(UCC)只不过是多域SSL证书,早期设计用于保护Microsoft Exchange和Live Communications服务器。现在,它被用来保护任何多个域名,只需一个证书。UCC证书具有组织验证功能,还可以用作EV SSL证书,为用户提供最高程度的信任和安全性。
总之,我们可以说,要赢得用户的信任,并将你的在线业务或博客带到新的成功高度,关注用户数据的安全是至关重要的。要做到这一点,没有比使用廉价的SSL来保护你的网站或博客更好的方法了,它可以帮助你瞄准你的安全需求和预算。
如果喜欢本文,欢迎转发。本文已同步至博客站,尊重原创,转载时请在正文中附带以下链接:https://www.linuxrumen.com/rmxx/1994.html
在这里我就不再一一介绍每个步骤的具体操作了,因为在爬取老版数据的时候都已经讲得非常清楚了,所以在这里我只会在重点上讲述这个是这么实现的,如果想要看具体步骤请先去看我的文章内容,里面有非常详细的介绍以及是怎么找到加密js代码和api接口。
私信小编01即可获取大量Python学习资料
58同城的数据爬取非常简单,唯一有点难的就是字体的加密,除此之外其他的数据用xpath即可获取。
想爬取不同地方的直接访问链接即可:
数据在链接中,直接请求获取即可。
既然是字体加密那么就先把字体寻找出来,寻找简单,在开发者工具中的分类找到Font,然后搜索这个链接进行查找。
已经找到这个字体了,他是在请求页面的时候返回的,然后他还是个base64的,只需要转换一下再保存就可以了。
import requests
from lxml import etree
def get_data():
url="https://bj.58.com/chuzu/?PGTID=0d200001-0000-11e9-58e6-a658f219b27c&ClickID=1"
headers={
'authority': 'bj.58.com',
'method': 'GET',
'path': '/chuzu/?PGTID=0d200001-0000-11e9-58e6-a658f219b27c&ClickID=1',
'scheme': 'https',
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'zh-CN,zh;q=0.9',
'cache-control': 'max-age=0',
'cookie': 'f=n; commontopbar_new_city_info=1%7C%E5%8C%97%E4%BA%AC%7Cbj; commontopbar_ipcity=bj%7C%E5%8C%97%E4%BA%AC%7C0; userid360_xml=C0E9739B2022549506AFBC01231A1DAA; time_create=1606640420140; xxzl_cid=f4a781439d9247f393d0a1629bec00df; xzuid=e0e5ea78-ac5a-491b-819d-a869ab37a7a7; xxzl_deviceid=2G3xFS3qwOviMHxtC%2FVEituhpmiI%2FJ%2BAmJ08cPBulZSe7LcSgT98WgFcyNDbzMXJ; id58=c5/nfF+bz1xVS0tAA7tjAg==; 58tj_uuid=116f1ed0-7c25-477e-8887-be3602fa2389; new_uv=1; utm_source=; spm=; init_refer=https%253A%252F%252Fbj.58.com%252Fchuzu%252Fsub%252Fpn70%252F%253Fpagetype%253Dditie%2526PGTID%253D0d3090a7-0000-1b87-3e2e-c6efe8d19973%2526ClickID%253D2; wmda_uuid=13712f08f0e555f110b1b2684ce9d709; wmda_new_uuid=1; wmda_session_id_11187958619315=1604046685879-d3ad7e5f-77f6-29d7; wmda_visited_projects=%3B11187958619315; als=0; f=n; new_session=0',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'none',
'sec-fetch-user': '?1',
'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36'
}
response=requests.get(url=url, headers=headers)
return response.text
def get_font(data):
html=etree.HTML(data)
script=html.xpath('//script[2]/text()')[0]
ttf=re.findall(".*?src:url\(\'data:application/font-ttf;charset=utf-8;base64,(.*?)\'\).*?",script,re.S)[0]
with open('fangchan-secret.ttf','wb') as f:
f.write(base64.b64decode(ttf))
if __name__=='__main__':
data=get_data()
get_font(data)
import base64
from fontTools.ttLib import TTFont
import re
import requests
from lxml import etree
def get_data():
url="https://bj.58.com/chuzu/?PGTID=0d200001-0000-11e9-58e6-a658f219b27c&ClickID=1"
headers={
'authority': 'bj.58.com',
'method': 'GET',
'path': '/chuzu/?PGTID=0d200001-0000-11e9-58e6-a658f219b27c&ClickID=1',
'scheme': 'https',
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'zh-CN,zh;q=0.9',
'cache-control': 'max-age=0',
'cookie': 'f=n; commontopbar_new_city_info=1%7C%E5%8C%97%E4%BA%AC%7Cbj; commontopbar_ipcity=bj%7C%E5%8C%97%E4%BA%AC%7C0; userid360_xml=C0E9739B2022549506AFBC01231A1DAA; time_create=1606640420140; xxzl_cid=f4a781439d9247f393d0a1629bec00df; xzuid=e0e5ea78-ac5a-491b-819d-a869ab37a7a7; xxzl_deviceid=2G3xFS3qwOviMHxtC%2FVEituhpmiI%2FJ%2BAmJ08cPBulZSe7LcSgT98WgFcyNDbzMXJ; id58=c5/nfF+bz1xVS0tAA7tjAg==; 58tj_uuid=116f1ed0-7c25-477e-8887-be3602fa2389; new_uv=1; utm_source=; spm=; init_refer=https%253A%252F%252Fbj.58.com%252Fchuzu%252Fsub%252Fpn70%252F%253Fpagetype%253Dditie%2526PGTID%253D0d3090a7-0000-1b87-3e2e-c6efe8d19973%2526ClickID%253D2; wmda_uuid=13712f08f0e555f110b1b2684ce9d709; wmda_new_uuid=1; wmda_session_id_11187958619315=1604046685879-d3ad7e5f-77f6-29d7; wmda_visited_projects=%3B11187958619315; als=0; f=n; new_session=0',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'none',
'sec-fetch-user': '?1',
'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36'
}
response=requests.get(url=url, headers=headers)
return response.text
def get_font(data):
html=etree.HTML(data)
script=html.xpath('//script[2]/text()')[0]
ttf=re.findall(".*?src:url\(\'data:application/font-ttf;charset=utf-8;base64,(.*?)\'\).*?",script,re.S)[0]
with open('fangchan-secret.ttf','wb') as f:
f.write(base64.b64decode(ttf))
def parse_font():
font=TTFont('fangchan-secret.ttf')
bestcmap=font['cmap'].getBestCmap()
newmap=dict()
for key in bestcmap.keys():
value=int(re.search(r'(\d+)', bestcmap[key]).group(1)) - 1
key=hex(key)
newmap[key]=value
print(newmap)
if __name__=='__main__':
data=get_data()
get_font(data)
parse_font()
我们发现字体编号和之前的不符合,比如:0x9476=7,而这里的是2,这是什么原因呢?是因为他的字体是动态生成的,每次返回的数字编号对应的值都是不同的,但是不影响我们代码的正常运行与结果。
import base64
from fontTools.ttLib import TTFont
import re
import requests
from lxml import etree
def get_data():
url="https://bj.58.com/chuzu/?PGTID=0d200001-0000-11e9-58e6-a658f219b27c&ClickID=1"
headers={
'authority': 'bj.58.com',
'method': 'GET',
'path': '/chuzu/?PGTID=0d200001-0000-11e9-58e6-a658f219b27c&ClickID=1',
'scheme': 'https',
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'zh-CN,zh;q=0.9',
'cache-control': 'max-age=0',
'cookie': 'f=n; commontopbar_new_city_info=1%7C%E5%8C%97%E4%BA%AC%7Cbj; commontopbar_ipcity=bj%7C%E5%8C%97%E4%BA%AC%7C0; userid360_xml=C0E9739B2022549506AFBC01231A1DAA; time_create=1606640420140; xxzl_cid=f4a781439d9247f393d0a1629bec00df; xzuid=e0e5ea78-ac5a-491b-819d-a869ab37a7a7; xxzl_deviceid=2G3xFS3qwOviMHxtC%2FVEituhpmiI%2FJ%2BAmJ08cPBulZSe7LcSgT98WgFcyNDbzMXJ; id58=c5/nfF+bz1xVS0tAA7tjAg==; 58tj_uuid=116f1ed0-7c25-477e-8887-be3602fa2389; new_uv=1; utm_source=; spm=; init_refer=https%253A%252F%252Fbj.58.com%252Fchuzu%252Fsub%252Fpn70%252F%253Fpagetype%253Dditie%2526PGTID%253D0d3090a7-0000-1b87-3e2e-c6efe8d19973%2526ClickID%253D2; wmda_uuid=13712f08f0e555f110b1b2684ce9d709; wmda_new_uuid=1; wmda_session_id_11187958619315=1604046685879-d3ad7e5f-77f6-29d7; wmda_visited_projects=%3B11187958619315; als=0; f=n; new_session=0',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'none',
'sec-fetch-user': '?1',
'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36'
}
response=requests.get(url=url, headers=headers)
return response.text
def get_font(data):
html=etree.HTML(data)
script=html.xpath('//script[2]/text()')[0]
ttf=re.findall(".*?src:url\(\'data:application/font-ttf;charset=utf-8;base64,(.*?)\'\).*?",script,re.S)[0]
with open('fangchan-secret.ttf','wb') as f:
f.write(base64.b64decode(ttf))
def parse_font():
font=TTFont('fangchan-secret.ttf')
bestcmap=font['cmap'].getBestCmap()
newmap=dict()
for key in bestcmap.keys():
value=int(re.search(r'(\d+)', bestcmap[key]).group(1)) - 1
key=hex(key)
newmap[key]=value
return newmap
def parse_data(data,newmap):
for key,value in newmap.items():
key_=key.replace('0x','') + ';'
if key_ in data:
data=data.replace(key_,str(value))
html=etree.HTML(data)
house_list=html.xpath('//ul[@class="house-list"]/li')[:-1]
for house in house_list:
room=house.xpath('.//p[@class="room"]/text()')[0]
money=house.xpath('.//b[@class="strongbox"]/text()')[0]
print(room,money)
if __name__=='__main__':
data=get_data()
get_font(data)
newmap=parse_font()
parse_data(data,newmap)
*请认真填写需求信息,我们会在24小时内与您取得联系。