python爬虫——猫眼 fonts 字体破解

学习之前,看了一下发现网上有教程,应该不难,但是现在都不行了,因为以前反爬虫字体只是简单的换了下字体名称,所有的参数都没有改变,所以有 TTFont 库,直接提取数值相等就可以判断这是代表哪一个字符,但是目前字体文件参数反爬做了随机偏移修改,所以网上的好像都不适合了。

一、前提

编译IDA:pycharm 社区版
python版本:python3.7.4
用到的库:requests、re、os、TTFont

二、分析

以其中一部电影为例(获取评分,人数,票房):https://maoyan.com/films/1190122

1、分析界面
根据请求响应,可以知道这直接就是 HTML 界面,数据可以在界面直接提取,但是数字进行了加密。
在这里插入图片描述

2、字体解密
所以其它方面我们可以先不考虑,难点在于如何对 fonts 字体进行破解。
(1)我们先打开两个 woff 文件查看对应代表的数字
发现虽然能够对应,但是找不到关联(其实就是用来确定一个基准的)
在这里插入图片描述
(2)我们将 woff 文件转换格式为 xml 查看

font = TTFont('3b6875f2fb78e9f5bc1e16486febd9c22300.woff')
font.saveXML('3b6875f2fb78e9f5bc1e16486febd9c22300.xml')

网上很多例子是以前的,打开查看对应数字的参数是一样的,所以只需要及简单的判断全部相等就可以了,有对应的的函数使用,但是现在 fonts 文件参数都做了随机修改,所以参数就需要一个一个判断相似得到相等才可以

根据上面对应,找到在第一个里面 1 对应的是 uniE707,第二个里面 1 对应的是 uniF637,可以看到虽然代表的一样,但是具体的参数点做了随即修改。(而且有的代表的是一样的,但是描述的点个数还不一样)
在这里插入图片描述
即左右两张 1,还是有细微的差别的。
在这里插入图片描述

(3)想法
正是因为这种相似,且没有偏差太多,所以我就想能不能以其中一个为基准,只要以后的 fonts 文件坐标参数相差不是很大就行了,我们认为规定他在一个误差范围内,即可认定他们相等,只要相似的点数占总数(点个数多的为实际总数)的好多即可认为他就代表他。

(4)基本步骤

  1. 设立一个基准 fonts 文件(woff格式)
  2. 基准字体文件把数字和对应名称建立字典
  3. 通过解析基准文件和新的字体文件,得到各个数字的 x,y,on 参数(xml格式)
  4. 通过误差分析,得到相等的参数个数
  5. 然后计算得到相似率,规定在好多以上即为相等(自己规定)
  6. 相等的就以此更新基准字典得到新的字典
  7. 用新的字典把加密数字解密即可

三、完整代码

分为两个文件
(1)用来转换字典:Dict.py

# !/usr/bin/python
# -*- coding: utf-8 -*- 
# @Time : 2019/12/21 15:51 
# @Author : ljf
# @File : Dict.py
import re
from fontTools.ttLib import TTFont


def compare(list1, list2):
    """
    比较两个列表有好多符合规则(近似),调整误差使其精确
    Args:
        list1:  第一个字符的 x,y,on 的列表
        list2:  第二个字符的 x,y,on 的列表
    Returns:    近似个数
    """
    l1 = len(list1)
    l2 = len(list2)
    # 剔除起笔不一样的,误差在 20 以内
    if ((int(list2[0][0]) - 20 < int(list1[0][0]) < int(list2[0][0]) + 20)
        or (int(list2[0][1]) - 20 < int(list1[0][1]) < int(list2[0][1]) + 20)) \
            and (int(list2[0][2]) - 20 < int(list1[0][2]) < int(list2[0][2]) + 20):
        pass
    else:
        return 0
    # 查询近似个数,误差在 15 以内
    count = 0
    for i in range(0, l1):
        for j in range(0, l2):
            if (int(list2[j][0]) - 15 < int(list1[i][0]) < int(list2[j][0]) + 15) \
                    and (int(list2[j][1]) - 15 < int(list1[i][1]) < int(list2[j][1]) + 15) \
                    and (int(list2[j][2]) - 15 < int(list1[i][2]) < int(list2[j][2]) + 15):
                count = count + 1
    return count


def update_dict(list1, list2, dict_base, name_base, name_new):
    """
    得到新的字典,根据相似比率,判断是否相等,确定映射关系
    Args:
        list1:      基准字体文件每个字体具体参数 x,y,on
        list2:      下载字体文件每个字体具体参数 x,y,on
        dict_base:  基准字体文件映射字典
        name_base:  基准字体文件各个字体名
        name_new:   下载字体文件各个字体名
    Returns:        新的映射字典
    """
    keys = []
    values = []
    for i in range(0, 11):
        for j in range(0, 11):
            if len(list1[i]) < len(list2):
                total = len(list2[i])
            else:
                total = len(list1[i])
            count = compare(list1[i], list2[j])
            # 相似比率
            if (count / total) > 0.7:
                keys.append(name_new[j])
                values.append(dict_base[name_base[i]])
                # print(list1[i][0], list2[j][0])
                # print(keys)
                # print(values)
    dict_new = dict(zip(keys, values))
    print(dict_new)
    return dict_new


def start(font_file):
    """
    Args:
        font_file:  下载字体文件
    Returns:        新的映射字典
    """
    # 打开自己保存的基准 woff 文件,设置映射关系
    font_base = TTFont("./fonts/base.woff")
    # 切片,第一个是空格,无参数
    name_base = font_base.getGlyphNames()[1:]
    values = list('8916537420.')
    # 创造初始字典对应
    dict_base = dict(zip(name_base, values))
    print(dict_base)
    # 同理打开网页 woff 文件
    font_new = TTFont("./fonts/{}".format(font_file))
    name_new = font_new.getGlyphNames()[1:]
    # 记录所有 x,y,on 参数数值
    lists_base_all = []
    lists_new_all = []
    with open('./fonts/base.xml') as f_baes:
        xml_base = f_baes.read()
    with open('./fonts/{}.xml'.format(font_file[:-5])) as f_new:
        xml_new = f_new.read()
    # 提取每个字符的 x,y,on,步骤:先切片分割,再正则表达式提取
    s_base = xml_base.split("</TTGlyph>")[:-1]
    for i in range(0, len(s_base)):
        lists_base = []
        contour = re.findall('<pt (.*?)/>', s_base[i])
        for j in range(0, len(contour)):
            x = re.findall('x=\"(.*?)\"', contour[j])
            y = re.findall('y=\"(.*?)\"', contour[j])
            on = re.findall('on=\"(.*?)\"', contour[j])
            lists_base.append(x + y + on)
        lists_base_all.append(lists_base)
    s_new = xml_new.split("</TTGlyph>")[:-1]
    for i in range(0, len(s_new)):
        lists_new = []
        contour = re.findall('<pt (.*?)/>', s_new[i])
        for j in range(0, len(contour)):
            x = re.findall('x=\"(.*?)\"', contour[j])
            y = re.findall('y=\"(.*?)\"', contour[j])
            on = re.findall('on=\"(.*?)\"', contour[j])
            lists_new.append(x + y + on)
        lists_new_all.append(lists_new)
    # 获取新的映射关系
    dict_new = update_dict(lists_base_all, lists_new_all, dict_base, name_base, name_new)
    return dict_new


if __name__ == "__main__":
    # 测试使用(自己下载即可)
    start('3b6875f2fb78e9f5bc1e16486febd9c22300.woff')

(2)用来解析界面:maoyan.py

# !/usr/bin/python
# -*- coding: utf-8 -*- 
# @Time : 2019/12/21 15:53 
# @Author : ljf
# @File : maoyan.py
import requests
import re
import os
from fontTools.ttLib import TTFont
import Dict


def modify_data(data, dict_new):
    """
    Args:
        data:       初始数据
        dict_new:   字典映射
    Returns:        转换数据
    """
    # 将获取到的网页数据中的&#x替换成uni
    for i in dict_new:
        gly = i.replace('uni', '&#x').lower() + ';'
        # 替换乱码格式
        if gly in data:
            data = data.replace(gly, dict_new[i])
    return data


class MaoYan:
    def __init__(self):
        """
        初始化
        """
        self.url = 'https://maoyan.com/films/1190122'
        self.films_headers = {
            "Host": "maoyan.com",
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
            "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
            "Accept-Encoding": "gzip, deflate, br",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0",
            "Connection": "close",
            # 自己的cookie
            "Cookie": "",
        }
        self.woff_headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0",
            "Connection": "close",
            # 自己的cookie
            "Cookie": "",
        }

    @staticmethod
    def get_html(url, headers):
        """
        发送请求
        Args:
            url:    请求网址
            headers:请求头
        Returns:    二进制格式
        """
        response = requests.get(url, headers=headers)
        return response.content

    def create_font(self, font_file):
        """
        下载 woff 字体文件
        Args:
            font_file:  字体文件格式
        Returns:        无
        """
        # 列出已下载文件
        file_list = os.listdir('./fonts')
        # 判断是否已下载
        if font_file not in file_list:
            # 未下载则下载新 woff 字体文件
            url = 'http://vfile.meituan.net/colorstone/' + font_file
            new_file = self.get_html(url, self.woff_headers)
            with open('./fonts/' + font_file, 'wb')as f:
                f.write(new_file)
        # 保存字体文件为 xml
        font = TTFont('./fonts/' + font_file)
        font.saveXML('./fonts/' + font_file[:-5] + '.xml')

    def start_crawl(self):
        """
        获取真实数据
        Returns:    无
        """
        html = self.get_html(self.url).decode('utf-8')
        # 正则匹配字体文件
        font_file = re.findall(r'vfile\.meituan\.net\/colorstone\/(\w+\.woff)', html)[0]
        self.create_font(font_file)
        dict_new = Dict.start(font_file)
        # 正则匹配评分
        star = re.findall(r'<span class="index-left info-num ">\s+<span class="stonefont">(.*?)</span>\s+</span>', html)[0]
        star = modify_data(star, dict_new)
        # 正则匹配想看的人数
        people = re.findall(r'<span class=".*?score-num.*?">(.*?)</span>', html, re.S)[0]
        people = modify_data(people, dict_new)
        # 正则匹配累计票房
        ticket_number = re.findall(r'<div class="movie-index-content box">\s+<span class="stonefont">(.*?)</span><span class="unit">(.*?)</span>\s+</div>',html)[0]
        ticket_number1 = modify_data(ticket_number[0], dict_new)
        print('用户评分: %s' % star)
        print('评分人数: %s' % people)
        print('累计票房: %s' % ticket_number1, ticket_number[1])


if __name__ == '__main__':
    MaoYan().start_crawl()

四、结果

准确率大概:80%以上
因为是近似计算,所以不是全部准确(有的判断不成功,有的还会重复),更加准确可以有两种方法
(1)使用跟小的误差值
但是要有度,否则一个都不匹配
(2)多次查询
最多两次就会正确,如果不正确,循环一次就好了。
在这里插入图片描述

五、总结

1、核心
数字最多就那样,再怎么变参数都有一定的规律,根据规则变就好了,这个只是细微修改,以后如果平移,稍作修改就可以了,规定在误差以内认为一样就行了。

2、代码和字体文件

https://github.com/2950833136/spider/tree/master/%E7%8C%AB%E7%9C%BC

3、进阶
如果还能修改的话,那么估计就需要训练模型,得到各个参数(x, y, on),然后画图得到数字,接下来使用图片识别来确认对应数字。

发布了113 篇原创文章 · 获赞 96 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/weixin_42109012/article/details/103648328