写在前面
Python反反爬系列
题目
题目网址,点我去刷题
分析网页
老规矩,我们还是首先打开刷题网站,接着打开谷歌调试工具
查看【XHR】里面的内容
观察Ajax返回的数据,我们可以发现data里面有10条数据,每条数据中对应一串字符串,很明显被空格分成了四个部分,不出预料的话就是对应网页中的四个数字
将网页中的数据进行对比,我们发现网页中确实有四个数字,但是后面还有两个字母LP
这个问题,我们先放着,待会去分析Ajax的代码,应该就有答案了
在返回的数据中,我们还可以发现woff这条信息
提到woff,经常跟字体打交道的同学,应该不陌生, woff其实就是一种字体格式,当我们在网页中需要显示一些酷炫的字体时,就可以使用woff字体文件,设置对应的字体
但是,这个woff有什么用呢,我们直接进入AJax的代码,进行分析
分析Ajax代码
因为,网页返回的数据是通过AJax请求返回的
所以,我们可以直接分析AJax中的代码,得出点线索
(不知道如何查看Ajax代码的同学,可以看一我这篇文章JS混淆---回溯,中的m 和 q的值,从何而来片段,里面有图片教程)
找到AJax代码,我们发现,有些代码片段非常长,我们可以将其部分代码复制下来,在Notepad++中打开,并手动格式化一下,这样有助于我们分析代码
可以看到,woff就在其中,我们将这部分的代码扣下来分析一下
// 获取返回数据中的woff部分
ttf = data.woff;
// 通过JQuery选择器,选择带有font属性的元素
// 首先通过text('')将其内容情况,再使用append()函数添加内容
// 添加的内容是一段CSS代码,需要一点CSS基础
$('.font').text('').append(
'<style type="text/css">
@font-face {
font-family:"fonteditor";
// 这里的url,其实是通过拼接实现的,拼接了返回数据中的woff部分
src: url(data:font/truetype;charset=utf-8;base64,' + ttf + '); }
</style>');
其实这段CSS代码也很简单,我稍微解释一下
- @font-face{}---> 设置字体文件,即字体规则
- font-family ---> 设置字体的名字,方便后面调用
- src ---> 字体路径,通过指定的路径,获取字体文件
我们分析上面的代码知道,字体路径,其实是通过返回数据中的woff部分拼接起来的
我们可以手动拼接一下这条路径,并在浏览器打开试一试
当然,也可以不用手动拼接,当src拼接完成后,并且append之后,浏览器会执行这条路径的,我们可以到谷歌的调试工具查看
我们访问一下这条路径,会发现浏览器,直接给我们下载了一个文件
我们将其重命名一下,换成以.woff后缀名的文件,比如1.woff,并将其打开
打开的时候,我们需要一个专门的软件【 High-Logic FontCreator 】,点击就可以前往下载页面
使用专业的软件打开后,我们可以发现这个字体文件包含着10个数字,和一个小点
而这些数字头上还是一串数字,这不得不让我们联想到通过Ajax返回数据中的10条数据
我们截取Ajax返回数据中的10条数据,进行进行对比
去除前面的字母和符号,只对比数字的话,发现是可以完全对上的
比如,第一条数据 【 871 416 871 279 】
根据字体文件映射的话,就是数字 3236,正好跟我们网页中的数字对应
继续分析AJax代码
我们回到AJax代码,继续分析
可以发现,返回数据中的10条数值数据也被引用了
并且还定义了一个 mad 变量,分析这个变量我们发现这个,就是一个应该包含很多数据的<tr>标签,并且其它很多标签都是带有class属性的
我们拿带有 【ranking-li-span-5】属性的标签举例,并在网页中搜索一下
可以发现,这些标签里面的内容其实是与网页相对应的,至于数据是怎么变动的,那就要接着分析后面的AJax代码了
我们还可以得出一个结论:mad中定义的<tr>标签,就是网页中的一行数据,如下图
我们已经知道了一个<tr>标签就代表一行数据,那么网页中要想呈现10条数据,必须要通过循环遍历出结果
继续,分析后面的AJax代码,就可以证明我们的猜想
$.each(data, function (index, val) {
let ppo = mad;
for (let imgnum = 1; imgnum <= 5; imgnum++) {
ppo = ppo.replace('img_number', yyq * window.page + imgnum_arr[imgnum])
}
html += ppo.replace('九不想乖', name[yyq + (window.page - 1) * 10])
.replace('win_number', imgnum_arr[yyq] * level_arr[window.page] * 88 + '场')
.replace(/win_rank/g, imgnum_arr[yyq] + 60 + level_arr[window.page] + '%')
.replace('random_level', imgnum_arr[yyq] * level_arr[window.page] + 100 * level_arr[window.page])
.replace('img_number', yyq * window.page)
// replace()这个函数,我们已经不陌生了,作用就是替换内容
// 通过替换 mad中的内容,达到呈现不同网页数据的效果
// 下面的代码就是将random_rank_number 替换成,返回的value数据,并且带上一个LP
.replace('random_rank_number', val.value.replace(/ /g, '') + 'LP');
yyq += 1;
img_num += 1
});
通过分析上面的代码,我们就可以知道,要想通过一条指定的<td>标签,呈现不同的内容,我们就可以通过循环加上replace()的方式,改变网页的内容
这也就解释了,为什么数字后面会多两个字母了 (LP)
这个时候,我们又发现了一个新问题
我们知道,通过AJax返回的数值数据是这个样子的
字体文件将乱码变成数字
通过前面的分析,我们知道
- 通过Ajax返回的woff数据,网页中是添加了一行CSS样式代码的
- 而这串CSS代码定义了名为fonteditor的字体规则
- 通过专业软件,我们知道字体文件的数字和乱码其实是一一对应的
- 所以,我相信大家就明白了字体文件是如何将乱码变成数字的
接着我们来验证一下
查看网页源码,可以发现包含胜点数值的标签,是带有fonteditor属性的
我们搜索一下fonteditor属性,就可以发现这样一行代码
.fonteditor{font-family:"fonteditor"!important;
CSS中“!important”可以使其当前的样式优先执行
看到这里,相信大家已经茅塞顿开了吧~~~
思路解析
经过上面的一段分析,我们来缕一缕思路
- 点击刷题网址,发起Ajax请求
- 请求成功后,返回对应页面的数据
- 执行 $('.font').text('').append()代码,会请求一个字体文件网址,并且添加一段css样式代码
- 获取返回的data代码,通过each循环加replace()将定义好的mad<td>标签代码中的内容进行替换
- 最后通过('.append_result').text('').append(html) 将替换的数据,呈现到网页上
- 而字体文件可以解析返回的乱码数值,这也就是题目提到的动态字体---随风漂移
获取映射字典
最终,我们想要解出答案,肯定是要写爬虫代码的,获取接口数据很简单,连一点反爬机制都没有;但是如果通过字体文件,找到数字的映射关系,这就是我们需要解决的问题
在解决问题,之前我们先要了解一个第三方库 【 FontTools 】
因为,我们需要通过这个第三方库将字体文件(.woff)转换成xml文件进行分析
from fontTools.ttLib import TTFont
# 读取字体文件
font = TTFont(f'1.woff')
# 转为xml文件
font.saveXML(f'1.xml')
转换成功后,我们打开xml文件,可以发现有很多标签
可能有些同学,看到这段代码后,决定映射关系已经出来了
其实并不然,你通过前面提到的软件进行对比就会发现是不正确的
我在文章开头为什么说这道题目坑呢?
就是因为按照以前的做法是行不通的,将xml文件划到最下面,你会看到这么一段代码
之前的做法就是,我图片中标出来的映射关系,可以通过标签出现的顺序直接确定数字
但是,时代变了呀,这种映射关系已经不存在了
新方法
正所谓,道高一尺魔高一丈,虽然这种按照顺序出来的映射关系不存在,肯定是可以通过其他方法找到映射关系的
在xml文件<glyf>标签下有11个<TTGlyph>标签
而这个11个标签,刚好对应11个数字文字图像
由于第一个是个小点,我们直接打开第二个<TTGlyph>标签
可以发现里面有一些下x,y的属性,我猜测的话应该是字体的路径
我通过多个xml文件发现,同一个数字的x,y属性,是不一样的,但是里面的on属性却是一样的
那么,我们是不是就可以通过每个<TTGlyph>标签里面的on属性来确定每一个数字呢
通过on属性确定的映射关系如下
cipher_nums = {'1010010010': 0, '1001101111': 1, '1001101010': 2,
'1010110010': 3, '1111111111': 4, '1110101001': 5,
'1010101010': 6, '1111111': 7, '1010101011': 8, '1001010100': 9}
基本上多数数字的on属性值很多,所以我只选取了前面十位,但是7这个数字,只有7属性值,也就只取了7位
所以,我们就可以编写一个函数,得出映射关系
from xml.dom.minidom import parse
from fontTools.ttLib import TTFont
def get_real_nums():
cipher_nums = {'1010010010': 0, '1001101111': 1, '1001101010': 2,
'1010110010': 3, '1111111111': 4, '1110101001': 5,
'1010101010': 6, '1111111': 7, '1010101011': 8, '1001010100': 9}
# 加载字体文件
online_font = TTFont('cipher.woff')
# 转为xml文件
online_font.saveXML('cipher.xml')
font = parse(r'cipher.xml') # 读取xml文件
xml_list = font.documentElement # 获取xml文档对象,就是拿到DOM树的根
# getElementsByTagName()
# 获取xml文档中的某个父节点下具有相同节点名的节点对象的集合,返回的是list
all_ttg = xml_list.getElementsByTagName('TTGlyph')[1:]
cipher_dict = {}
for TTGlyph in all_ttg:
name = TTGlyph.getAttribute('name')[4:] # 获取节点的属性值
pt = TTGlyph.getElementsByTagName('pt')
num = ''
if (len(pt) < 10):
for i in range(len(pt)):
num += pt[i].getAttribute('on')
else:
for i in range(10):
num += pt[i].getAttribute('on')
num = cipher_nums[num]
cipher_dict[name] = num
return cipher_dict
#输出结果(例子){'279': 6, '416': 2, '469': 8, '871': 3, '258': 9, '347': 0, '876': 7, '385': 5, '619': 1, '483': 4}
通过字体文件得出数字的映射关系,那么就这道题目就很简单了,我就直接上代码啦
解出答案
# @BY :Java_S
# @Time :2021/1/11 17:13
# @Slogan :够坚定够努力大门自然会有人敲,别怕没人赏识就像三十岁的梵高
import os
import re
import base64
import requests
from xml.dom.minidom import parse
from fontTools.ttLib import TTFont
def get_data(page):
url = f'http://match.yuanrenxue.com/api/match/7?page={page}'
headers = {'User-Agent': 'yuanrenxue.project'}
response = requests.get(url=url, headers=headers)
woff_data = response.json()['woff']
value_data = response.json()['data']
value_data = [re.findall(r"\d+\.?\d*", i['value'].replace('&#x', '').replace(' ', '')) for i in value_data]
# 下载字体文件
download_woff(woff_data)
return value_data
def download_woff(woff_data, ):
with open('cipher.woff', mode='wb') as file:
file.write(base64.b64decode(woff_data.encode()))
file.close()
def get_real_nums():
cipher_nums = {'1010010010': 0, '1001101111': 1, '1001101010': 2,
'1010110010': 3, '1111111111': 4, '1110101001': 5,
'1010101010': 6, '1111111': 7, '1010101011': 8, '1001010100': 9}
# 加载字体文件
online_font = TTFont('cipher.woff')
# 转为xml文件
online_font.saveXML('cipher.xml')
font = parse(r'cipher.xml') # 读取xml文件
xml_list = font.documentElement # 获取xml文档对象,就是拿到DOM树的根
# getElementsByTagName()
# 获取xml文档中的某个父节点下具有相同节点名的节点对象的集合,返回的是list
all_ttg = xml_list.getElementsByTagName('TTGlyph')[1:]
cipher_dict = {}
for TTGlyph in all_ttg:
name = TTGlyph.getAttribute('name')[4:] # 获取节点的属性值
pt = TTGlyph.getElementsByTagName('pt')
num = ''
if (len(pt) < 10):
for i in range(len(pt)):
num += pt[i].getAttribute('on')
else:
for i in range(10):
num += pt[i].getAttribute('on')
num = cipher_nums[num]
cipher_dict[name] = num
return cipher_dict
def real_data(value_data, cipher_dict):
num_list = []
for data in value_data:
num = ''
for i in data:
num += str(cipher_dict[i])
num_list.append(int(num))
return num_list
if __name__ == '__main__':
rank_list = []
for i in range(1, 6):
real_num_list = real_data(get_data(i), get_real_nums())
print(f'第{i}页胜点数据:{real_num_list}')
rank_list.extend(real_num_list)
print(f'最高的胜点数是:{max(rank_list)}')
# 删除下载和转换的文件
os.remove('cipher.xml')
os.remove('cipher.woff')
if (len(pt) < 10):
for i in range(len(pt)):
num += pt[i].getAttribute('on')
else:
for i in range(10):
num += pt[i].getAttribute('on')
这点是不是多此一举了啊, 我没看出来这点的精髓, 当大于10的时候, 就不能去完整的 on吗?
难道是因为你上面的那个字典局限吗?
我没记错的话是xml文件中的数据,有些不同,这样处理是为了后面处理更加方便
re.findall(r"\d+\.?\d*", i['value'].replace('', '').replace(' ', '')) for i in value_data] 这段有些不懂,求解
就是通过正则进行一系列的字符串替换得到想要的结果,你可以打印value_data看看,便于理解