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)基本步骤
- 设立一个基准 fonts 文件(woff格式)
- 基准字体文件把数字和对应名称建立字典
- 通过解析基准文件和新的字体文件,得到各个数字的 x,y,on 参数(xml格式)
- 通过误差分析,得到相等的参数个数
- 然后计算得到相似率,规定在好多以上即为相等(自己规定)
- 相等的就以此更新基准字典得到新的字典
- 用新的字典把加密数字解密即可
三、完整代码
分为两个文件
(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 TTFontdef 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):passelse:return 0# 查询近似个数,误差在 15 以内count = 0for 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 + 1return countdef update_dict(list1, list2, dict_base, name_base, name_new):"""得到新的字典,根据相似比率,判断是否相等,确定映射关系Args:list1: 基准字体文件每个字体具体参数 x,y,onlist2: 下载字体文件每个字体具体参数 x,y,ondict_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_newdef 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("")[:-1]for i in range(0, len(s_base)):lists_base = []contour = re.findall(' ', 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("")[:-1]for i in range(0, len(s_new)):lists_new = []contour = re.findall(' ', 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_newif __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 Dictdef modify_data(data, dict_new):"""Args:data: 初始数据dict_new: 字典映射Returns: 转换数据"""# 将获取到的网页数据中的替换成unifor i in dict_new:gly = i.replace('uni', '').lower() + ';'# 替换乱码格式if gly in data:data = data.replace(gly, dict_new[i])return dataclass 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": "",}@staticmethoddef get_html(url, headers):"""发送请求Args:url: 请求网址headers:请求头Returns: 二进制格式"""response = requests.get(url, headers=headers)return response.contentdef 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_filenew_file = self.get_html(url, self.woff_headers)with open('./fonts/' + font_file, 'wb')as f:f.write(new_file)# 保存字体文件为 xmlfont = 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'\s+(.*?)\s+', html)[0]star = modify_data(star, dict_new)# 正则匹配想看的人数people = re.findall(r'(.*?)', html, re.S)[0]people = modify_data(people, dict_new)# 正则匹配累计票房ticket_number = re.findall(r'\s+(.*?)(.*?)\s+',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、进阶
如果还能修改的话,那么估计就需要训练模型,得到各个参数(x, y, on),然后画图得到数字,接下来使用图片识别来确认对应数字。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
