写在前面

Python反反爬系列

  1. JS混淆---源码乱码
  2. JS混淆---动态Cookie
  3. 访问逻辑---推心置腹
  4. CSS加密---样式干扰
  5. JS混淆---回溯

解题代码

题目


题目网址,点我去刷题

采集这5页中胜点列的数据,找出胜点最高的召唤师,将召唤师姓名填入答案中

分析网页

老规矩,我们还是首先打开刷题网站,接着打开谷歌调试工具
查看【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返回的数值数据是这个样子的

而使用val.value,获取的也是一串符号加数字,那么网页中是如何显示正确数字的呢

字体文件将乱码变成数字

通过前面的分析,我们知道

  • 通过Ajax返回的woff数据,网页中是添加了一行CSS样式代码的
  • 而这串CSS代码定义了名为fonteditor的字体规则
  • 通过专业软件,我们知道字体文件的数字和乱码其实是一一对应的
  • 所以,我相信大家就明白了字体文件是如何将乱码变成数字的

接着我们来验证一下

查看网页源码,可以发现包含胜点数值的标签,是带有fonteditor属性的
我们搜索一下fonteditor属性,就可以发现这样一行代码

.fonteditor{font-family:"fonteditor"!important;
CSS中“!important”可以使其当前的样式优先执行

看到这里,相信大家已经茅塞顿开了吧~~~

思路解析

经过上面的一段分析,我们来缕一缕思路

  1. 点击刷题网址,发起Ajax请求
  2. 请求成功后,返回对应页面的数据
  3. 执行 $('.font').text('').append()代码,会请求一个字体文件网址,并且添加一段css样式代码
  4. 获取返回的data代码,通过each循环加replace()将定义好的mad<td>标签代码中的内容进行替换
  5. 最后通过('.append_result').text('').append(html) 将替换的数据,呈现到网页上
  6. 而字体文件可以解析返回的乱码数值,这也就是题目提到的动态字体---随风漂移

题目的思路其实很简单,很清晰;坑就坑在如何通过字体文件,找到数字的映射关系

获取映射字典

最终,我们想要解出答案,肯定是要写爬虫代码的,获取接口数据很简单,连一点反爬机制都没有;但是如果通过字体文件,找到数字的映射关系,这就是我们需要解决的问题

在解决问题,之前我们先要了解一个第三方库 【 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')

世界因代码而改变 Peace Out
最后修改:2021 年 01 月 18 日 08 : 02 PM
如果觉得我的文章对你有用,请随意赞赏