学习之前,看了一下发现网上有教程,应该不难,但是现在都不行了,因为以前反爬虫字体只是简单的换了下字体名称,所有的参数都没有改变,所以有 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 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),然后画图得到数字,接下来使用图片识别来确认对应数字。