破解自定义字体反爬虫
背景介绍
众所周知,常见的反爬虫策略有以下几种:
- 限制IP限制访问频率等,这我们可以通过代理ip等方式解决
- 验证码,其中滑块验证码可以参考这篇博客解决:链接
还有一种:通过css自定义字体,来隐藏关键数据,从而拦截爬虫
如下图片所示,你会发现电影评分是一个无法识别的字体:
这次来破解一下这样的自定义字体爬虫
自定义字体原理介绍
css加密的方式具体前端是怎么实现的,可以参考如下这篇博客:
知道原理之后,接下来就具体介绍如何破解
x眼电影具体案例
以猫眼电影的票房数据为例,介绍一个具体案例
下载字体
通过搜索stonefont,我们可以找到如下style
通过其中的xxx.woff这个链接,就可以下载到字体。
具体到代码中,我们通过正则拿到字体链接,然后下载到本地。保存三种格式的字体。
def get_font_url(url):
"""
获取加密字体的下载地址
:param url: 电影请求页面的url
:return: 返回字体的名称
"""
resp = requests.get(url, headers=header)
resp_html = resp.text
e_t = etree.HTML(resp_html)
font_str = str(e_t.xpath('/html/head/style/text()'))
font_file_name = re.findall(r'//s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/(\w+\.woff)',
font_str)[0]
return font_file_name
def down_font(font_name):
"""
下载字体,保存为font.woff font.ttf 和 font.xml
:param font_name: 字体的名称
:return:
"""
font_url = f"//s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/{font_name}"
r = requests.get('http:'+font_url)
with open(f"font/font.woff", "wb") as code:
code.write(r.content)
font = TTFont(f"font/font.woff")
font.saveXML(f'font/font.xml')
decompress("font/font.woff", "font/font.ttf")
字体识别
将下载后的woff文件,通过如下网站可以进行预览,就能得到这个字体文件中,编码和数字的对应关系;还有一种方式是将woff转换成xml文件,然后通过pillow库对字体坐标进行绘图,也可以识别字体。
https://blog.luckly-mjw.cn/tool-show/iconfont-preview/index.html
如下所示:
字体对应关系如下:
font_dict = {
'uniEB19': 9, 'uniE3EC': 2, 'uniF7D2': 4, 'uniED30': 1, 'uniF3E8': 5, 'uniF11C': 3,
'uniEA60': 6, 'uniEF28': 8, 'uniEA6F': 0, 'uniE3DF': 7
}
如下图所示:(由于每次刷新页面字体都会随机变化,截图字体和我下载的不是同一款
字体本质上是由一系列的坐标定义的,将xml文件中的坐标,也就是标签中的内容复制出来,通过以下代码就可以绘图了:
import matplotlib.pyplot as plt
import re
code_str = """
<contour>
<pt x="258" y="536" on="1"/>
<pt x="106" y="592" on="0"/>
<pt x="106" y="741" on="1"/>
<pt x="106" y="853" on="0"/>
<pt x="263" y="1000" on="0"/>
<pt x="526" y="1000" on="0"/>
<pt x="686" y="844" on="0"/>
。。。此处省略一些代码
</contour>
"""
x = [int(i) for i in re.findall(r'<pt x="(.*?)" y=', code_str)]
y = [int(i) for i in re.findall(r'y="(.*?)" on=', code_str)]
print(x)
print(y)
plt.plot(x, y)
plt.show()
字体处理
对于字体的处理,参考多个博客后,大致有以下两种思路
- 虽然每次字体都不一样,但字体的坐标信息是一样的,所以可以通过比较坐标来找出字体的对应关系(尝试了很久没成功
- 每次通过绘图,然后图形识别,来识别字体(推荐该做法
下面分别对这两种方式进行介绍。
方案一:比较字体坐标(失败
制作模板字体字典
首先手动下载一个字体,把他作为模板字体,可以参考上文的方法得到,比如:
font_dict = {
'uniEB19': 9, 'uniE3EC': 2, 'uniF7D2': 4, 'uniED30': 1, 'uniF3E8': 5, 'uniF11C': 3,
'uniEA60': 6, 'uniEF28': 8, 'uniEA6F': 0, 'uniE3DF': 7
}
比较字体
每次访问新页面时,下载新的字体,然后进行比较,找出新的字体词典,代码如下:
import requests
from fontTools.ttLib import TTFont
def comp_font(font_name):
if font_name == "e3dfe524.woff":
print("本次字体和第一次一样")
return font_dict
font_url = f"//s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/{font_name}"
r = requests.get('http:'+font_url)
with open(f"font/{font_name}", "wb") as code:
code.write(r.content)
print("字体不一致,下载新的字体")
# 打开基准字体
demo_font = TTFont("font/e3dfe524.woff")
demo_list = demo_font.getGlyphNames()[1:-1]
demo_uni_list = demo_font.getGlyphOrder()[2:]
# 打开新字体
new_font = TTFont(f"font/{font_name}")
new_font.saveXML(f"font/{font_name}.xml")
# new_font = TTFont("font/20a70494.woff")
new_list = new_font.getGlyphNames()[1:-1]
new_uni_list = new_font.getGlyphOrder()[2:]
# 通过两层循环进行比较,找出新的对应关系
new_dict = dict()
for n_uni in new_uni_list:
n_obj = new_font['glyf'][n_uni]
for demo_uni in demo_uni_list:
demo_obj = demo_font['glyf'][demo_uni]
if n_obj == demo_obj:
new_dict[n_uni] = font_dict[demo_uni]
print(new_dict)
return new_dict
结论:失败
在网上很多都是推荐这种方法,但,结论是问题出在这一步:
if n_obj == demo_obj:
我猜测可能TTFont的[’glyf’]对象不能直接这样简单粗暴的比较吧。如果有小伙伴知道原因,麻烦告诉我。谢谢。
方案二:通过ddddocr库进行图片识别(推荐
简单粗暴,代码如下:
def read_num_by_draw(woff_font):
"""
绘制图片通过ddddocr库进行图片识别
:param woff_font: 字体文件
:return: 字体字典
"""
ttf_font = "font/font.ttf"
img_size = 512
font = TTFont(woff_font)
font_img = ImageFont.truetype(ttf_font, img_size)
ocr = ddddocr.DdddOcr(show_ad=False)
font_dict = dict()
for cmap_code, glyph_name in font.getBestCmap().items():
# 实例化一个图片对象
img = Image.new('1', (img_size, img_size), 255)
# 绘制图片
draw = ImageDraw.Draw(img)
# 将编码读取成字节
txt = chr(cmap_code)
x, y = draw.textsize(txt, font=font_img)
draw.text(((img_size - x) // 2, (img_size - y) // 2), txt, font=font_img, fill=0)
bytes_io = BytesIO()
img.save(bytes_io, format="PNG")
# 识别字体
word = ocr.classification(bytes_io.getvalue())
# print(cmap_code, glyph_name, word)
font_dict[glyph_name.replace('uni', '&#x').lower()] = word
return font_dict
结语
拿到字体和编码的对应字典后,就可以对网页爬取的内容进行替换了。不过这里还有另一个问题需要解决,直接访问电影链接的话,实际上得到的是如下的母版页,并不包含电影信息:
电影详细信息是ajax请求得到的,这又是另一个问题了。
参考链接
完整代码请参考如下链接:
https://github.com/h-kayotin/hanayo_pixiv/tree/master/maoyan
本文还参考了如下博客:
https://mp.weixin.qq.com/s?__biz=Mzg2NzYyNjg2Nw==&mid=2247489977&idx=1&sn=83dcaee1ecbfd79048b38ab5b006cee2&source=41#wechat_redirect
https://github.com/CasterWx/python-maoyan-spider
https://mp.weixin.qq.com/s?__biz=Mzg2NzYyNjg2Nw==&mid=2247489991&idx=1&sn=54c11b1685d4cf7755a83f75cda84601&source=41#wechat_redirect
https://www.cnblogs.com/tjp40922/p/10603768.html
用图像识别解决:https://juejin.cn/post/7091958812378136590