字体反爬
1.字体反爬概述
随着css技术的发展,在CSS3中出现了一个自定义字体的新特性。在这之前,web设计师必须使用已在用户计算机上安装好的字体。现在,通过CSS3,web设计师可以使用它们喜欢的任意字体。浏览器会下载字体信息,然后动态渲染,html页面源码中,你看到的不再是正常字符, 或者unicode而是网站使用的自定义编码。因此它也被拿来作为一种反爬的手段,这就是字体反爬。
2.案例
2.1现象
使用谷歌浏览器访问58人才网,可以看到页面正常显示的文字,在开发者调试模式中是不能正常显示的。
查看源码,会发现它是类似unicode的编码。
2.2自定义字体
查看元素的css,果然使用了自定义的字体样式
左键点击字体引用,发现字体信息就在当前页面的style标签中,经过观察发现是一个base64编码的数据。
字体文件是二进制数据,复制这部分字符串,转换成二进制文件。
def make_font_file(base64_string):
"""
创建字体文件
:param base64_string: 页码base编码数据
:return: 二进制数据
"""
bin_data = base64.decodebytes(base64_string.encode())
with open('new.ttf', 'wb') as f:
f.write(bin_data)
return bin_data
接下来要查看和处理这个字体文件,需要用到两个工具。一个软件是FontCreator,可以永利来打开ttf字体文件,查看每个字符对于的编码。打开刚才生成的字体文件。
[外链图片转存失败(img-TfElQJg1-1566994278829)(字体反爬.assets/1560060761751.png)]
打开后可以看到“王”,“生”,“男”的编码,跟html源码中的编码一致。
2.3字体文件分析
经过多次请求页面观察发现,自定义字体的字符集不变,也就是说总是那几个关键字符被编码,但是每次编码发生改变。所以需要进一步分析字体文件,这时需要使用到一个专门处理字符文件的python第三方库fontTools,利用它可以将字体文件转换成xml文件进行分析。
font = TTFont('new.ttf')
# 将解码字体保存为xml
font.saveXML("new.xml")
打开这个xml文件,它的结构如下
有兴趣可以去研究每个节点的具体含义,这里主要使用到GlyphOrder
和glyf
两个元素节点,其中GlyphOrder
是编码序号表,glyf
是图元数据,也就是字体轮廓定义。
经过分析,发现虽然编码会不停的变换,但是每个字的图元是固定不变的,也即是字体形状是不变的。所以可以通过比较图元信息来判断两个编码是否表示同一个字符。
2.4解决步骤
经过上面的分析总结如下步骤:
-
首先下载一个字体文件作为基准,根据这个文件生成一个基准的编码和文字的映射。
-
访问页面,拿到字体数据
-
解码字体数据,生成字体文件
-
根据已有的基准字体文件和映射生成新的编码文字映射
-
替换数据中的编码
2.5代码
import base64
import json
import re
from io import BytesIO, StringIO
import requests
from fontTools.ttLib import TTFont
from lxml import etree
def get_base_map():
"""
生成手动映射关系
:return:
"""
data = {}
font = TTFont('base.ttf')
res = font.getGlyphNames()
for item in res:
if 'uni' in item:
data[item] = ''
with open('base.map.json', 'w', encoding='utf-8') as f:
json.dump(data, f, indent=4)
def make_font_file(base64_string):
"""
创建字体文件
:param base64_string: 页码base编码数据
:return: 二进制数据
"""
bin_data = base64.decodebytes(base64_string.encode())
with open('new.ttf', 'wb') as f:
f.write(bin_data)
return bin_data
def convert_font_to_xml(font_bin):
"""
创建字体xml文件
:param font_bin:
:return: font obj
"""
# ByteIO把一个二进制内存块当成文件来操作,
font = TTFont(BytesIO(font_bin))
# 将解码字体保存为xml
font.saveXML("new.xml")
return font
def get_map(font):
"""
生成新的映射关系
:param font:
:return: map
"""
with open('base.map.json', 'r', encoding='utf-8') as f:
base_map = json.load(f)
map = {}
base_font = TTFont('base.ttf')
for name in font.getGlyphNames():
if 'uni' in name:
new_obj = font['glyf'][name]
for base_name in base_font.getGlyphNames():
if 'uni' in base_name:
old_obj = base_font['glyf'][base_name]
if new_obj == old_obj:
map[name] = base_map[base_name]
return map
if __name__ == '__main__':
session = requests.Session()
session.headers.update({'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'})
# 1.请求页面
response = session.get('https://bj.58.com/qzyewu/?PGTID=0d202409-0000-1aa8-92da-777b90a7dc73&ClickID=1')
html = response.text
# 2.解析网页中的字体信息,生成字体文件,和xml文件
base_str = re.findall(r'base64,(.*?)\) format\("woff"\)', html, re.S)
if base_str:
font_bin = make_font_file(base64_string=base_str[0])
font = convert_font_to_xml(font_bin)
# 3.根据basemap生成映射关系
map = get_map(font)
print(map)
# 4.替换页面内容
for item in map:
old_str = '&#x%s;' % item[-4:].lower()
print(old_str)
html = html.replace(old_str, map[item])
print(html)