前言
在编写网页爬虫的时候,我们经常会遇到各种反爬,比如js加密、css位置偏移、字体反爬等。最近在学习过程中,我碰到几个网站,都是使用的字体反爬,其大致上可以分为两类:一类是字体文件是静态的,另一类字体文件是动态的变化的。今天,我们探究一下,在爬虫中如何破解字体反爬。
一、如何寻找字体文件
字体文件大致上分两种,一种是以.ttf结尾的,一种是以.woff结尾的,两者相差不大,现在的网站运用的大多是woff文件。如何寻找网页使用的字体文件,可以分两类:
1.第一类 字体URL在网页源代码中
以aHR0cHM6Ly93d3cucmVucmVuY2hlLmNvbS9iai9lcnNob3VjaGUvP3Bsb2dfaWQ9NWI5NzA3MWI1YjVhNWZkYWRkOWZhZDEzMGE0MmJiMDM=网站为例:
打开网站后,明显能看到,8被替换成了6。这种情况,我们可以尝试在网页源代码中搜索“font-face”,查看字体URL。
2.第二类 字体URL不在网页源代码中,由服务器加载
以aHR0cHM6Ly93d3cuZ3VhemkuY29tL2J1eQ==网站为例:
这里可以看到车辆的行使里程数和首付均是字体反爬,按照第一种情况,我们直接在网页源代码中搜索“font-face”
可以看到,也是有woff文件和ttf文件,但这两个文件都是“element-icons”的链接,打开(软件:FontCreator)后也就是一堆图标。
遇到这种的,可以打开浏览器Network,刷新后抓包。
可以看到这个请求返回的结果中有一个woff文件,打开后观察:
这个正是我们要找的字体文件,当然这个网站要拿到字体URL需要js逆向verify-token这个参数,这里不展开说。
二、如何应对静态字体反爬
还是以aHR0cHM6Ly93d3cucmVucmVuY2hlLmNvbS9iai9lcnNob3VjaGUvP3Bsb2dfaWQ9NWI5NzA3MWI1YjVhNWZkYWRkOWZhZDEzMGE0MmJiMDM=网站为例,经过观察,他的字体是每天更换一次,暂且当作是静态。
1.在网页源代码中搜索字体文件的URL,复制到浏览器中打开,下载字体文件。
2.用FontCreator打开文件,查看字体对应关系,手动建立对应关系字典。
# 字体对应关系 relation_table = {"zero" : "0" , "one" : "2" , "two" : "1" , "four" : "3" , "three" : "4" , "five" : "8" , "seven" : "7" ,"nine" : "9" , "six" : "6" , "eight" : "5" }
这是我学习过程中自己手动建立的对应关系。
3.请求字体链接,获取字体code和name的对应关系,然后遍历,获取网页中反爬文字的真实文字。
def woff_font (font_url) : '''获取字体真实对应关系''' newmap = {} resp = session.get(font_url) # 请求字体链接 woff_data = BytesIO(resp.content) font = TTFont(woff_data) # 读取woff数据 cmap = font.getBestCmap() # 获取字体对应关系 font.close() for k, v in cmap.items(): value = v key = str(k - 48 ) # 获取真实的key try : get_real_data = relation_table[value] except: get_real_data = ''
if get_real_data != '' : newmap[key] = get_real_data # 将字体真实结果对应 return newmap
4.替换网页中的反爬文字
如果反爬字体是10进制或16进制的建议直接替换网页代码中的字体,如果是数字的,建议逐项替换。替换的方式有用正则表达式的(这里不说,因为我正则写的也不好),也有用列表推导式的,比如:
title = "" .join(html.xpath('//*[@id="zhimaicar-detail-header-right"]/div[1]/h1/text()' )).strip() # 获取原始标题 trans_title = "" .join([i if not i.isdigit() else font[i] for i in title]) # 替换错误字体,获取真实标题
具体的用法,可以百度。
三、如何应对动态字体反爬
动态字体反爬,就是每请求一次(或一段时间后),字体的映射关系也会改变,因为第二个网站比较麻烦,这个还是以这个网站为例,将时间节点放大,把他看做是动态反爬的。
1.先下载字体文件
2.安装fontTools
安装后用
from fontTools.ttLib import TTFont font = TTFont(下载的字体文件) font.saveXML(文件名.xml)
将字体文件另存为xml文件,打开文件后会看到:
‘cmap’里存放的是字体code和name的关系
‘glyf’里存放的是字体形状和name的关系
3.手动建立字体形状和name的关系
只需手动建立一次。
f_font = TTFont("rrcttf3d6e374d48fb2cd14247257d4ae76674.woff" ) # 读取分析的字体文件 f_font_glyf = f_font['glyf' ] # 获取分析文件中的字体关系 # 建立基础的字体和字体形状的对应关系 base_font_map = { 0 : f_font_glyf['zero' ], 1 : f_font_glyf['two' ], 2 : f_font_glyf['one' ], 3 : f_font_glyf['four' ], 4 : f_font_glyf['three' ], 5 : f_font_glyf['eight' ], 6 : f_font_glyf['six' ], 7 : f_font_glyf['seven' ], 8 : f_font_glyf['five' ], 9 : f_font_glyf['nine' ], }
4.请求网页中的字体URL,获取code和name,形状和name的关系
resp = session.get(font_url) # 请求字体链接 woff_data = BytesIO(resp.content) # 保存字体数据 font = TTFont(woff_data) # 读取woff数据 glyf = font['glyf' ] # 获取请求到的字体形状 code_name_map = font.getBestCmap() # 获取请求到的字体code和name的对应关系 font.close()
5.根据形状相同,肯定字体相同的原则,获取真实对应关系
for code, name in code_name_map.items(): codestr = str(code - 48 ) # 根据分析结果需要减去48 current_shape = glyf[name] # 根据name获取字体形状 for number, shape in base_font_map.items(): # 遍历基础字体形状对应关系 if shape == current_shape: # 判断,如果两个字体形状相等 newmap[codestr] = str(number) # 将字体编码和字体添加到字典
6.完整代码
def woff_font (font_url) : newmap = {} f_font = TTFont("rrcttf3d6e374d48fb2cd14247257d4ae76674.woff" ) # 读取分析的字体文件 f_font_glyf = f_font['glyf' ] # 获取分析文件中的字体关系 # 建立基础的字体和字体形状的对应关系 base_font_map = { 0 : f_font_glyf['zero' ], 1 : f_font_glyf['two' ], 2 : f_font_glyf['one' ], 3 : f_font_glyf['four' ], 4 : f_font_glyf['three' ], 5 : f_font_glyf['eight' ], 6 : f_font_glyf['six' ], 7 : f_font_glyf['seven' ], 8 : f_font_glyf['five' ], 9 : f_font_glyf['nine' ], } resp = session.get(font_url) # 请求字体链接 woff_data = BytesIO(resp.content) # 保存字体数据 font = TTFont(woff_data) # 读取woff数据 glyf = font['glyf' ] # 获取请求到的字体形状 code_name_map = font.getBestCmap() # 获取请求到的字体code和name的对应关系 font.close() for code, name in code_name_map.items(): codestr = str(code - 48 ) # 根据分析结果需要减去48 current_shape = glyf[name] # 根据name获取字体形状 for number, shape in base_font_map.items(): # 遍历基础字体形状对应关系 if shape == current_shape: # 判断,如果两个字体形状相等 newmap[codestr] = str(number) # 将字体编码和字体添加到字典 return newmap
7.结果验证
当我在写这篇分享的时候,该网站的字体早已经换了,但是建立关系的时候我还用的是之前的文件,可见返回的结果也是没有问题的。
四、总结
静态字体反爬不用多说,面对动态反爬,一开始我也有点懵,后来在翻阅了一些资料和别人的启发后才弄明白,其实就是利用形状一样,字体肯定也一样的对应关系去破解。这是我学习过程中的一点经验总结,有不足之处还请各位指正,谢谢。