天天看点

woff字体反爬实战,10分钟就能学会

声明:本帖子仅是用于学习用途,请勿与用于恶意破坏别人网站,本人不承担法律责任。

来继续学爬虫呀!

前言

简单描述一下这种手段,html源码的数字跟页面展示的数字是不一致的!当时就一脸黑人问号,嗯???

经过分析,当前这种字体反爬机制是:通过获取指定链接的woff字体文件,然后根据html源码的数字
去woff字体文件里面查找真正的数字,讲到底就是一个映射关系/查找字典。如html源码是123,去woff文件里面
查找出来的是:623。好了,看到这里,你一定想说:废话讲那么多干嘛?赶紧上教程啊!!
           
那先来看一下大致流程呗:
woff字体反爬实战,10分钟就能学会

分析目标网站页面(在这里我不打算贴出网站地址,请大家自己找网站练习),这里看到html源码和页面展示的数字是不一致的,如下图:

woff字体反爬实战,10分钟就能学会
tips:
一开始不知道是怎么下手,只能谷歌搜索字体反爬,一搜果然很多说法,有说woff文件的、有说CSS的、还有说svg曲线啥的,
然后我就去查看Network里面的All,就发现关键字眼woff,就开始猜测可能是属于这种类型的反爬手段,接着开始干活。
           
混淆前字体:
woff字体反爬实战,10分钟就能学会
混淆后的字体:
woff字体反爬实战,10分钟就能学会

找了一会,发现.woff2文件和woff文件前后不一样,然后开始着手解决

如需下载woff文件,请点击这里, 提取码: ghnx

但是本地打不开woff字体文件,需要借助的软件是fontcreator,这个你自己去找一下,很多破解的

woff字体反爬实战,10分钟就能学会

但是这好像看不出什么,然后我们接着需要从另外一方面下手,重点来了》将woff文件转换为xml文件

如下:

import os
import requests
from fontTools.ttLib import TTFont

base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

url = "http://xxxxxx.xxx.woff"
    
woff_dir = os.path.join(base_dir, "statics/woffs/")
file_name = url.split("/")[-1]
xml_name = file_name.replace(file_name.split(".")[-1], "xml")
save_woff = os.path.join(woff_dir, file_name)
save_xml = os.path.join(woff_dir, xml_name)

resp = requests.get(url="xxx")
with open(save_woff, "wb") as f:
    f.write(resp.content)
    f.close()
font = TTFont(save_woff)
font.saveXML(save_xml)  # 转换为xml文件
           

然后打开xml文件看,先来查看一下缩略的内容,红色圈圈的那两个是本次重点破解的分析的内容:

woff字体反爬实战,10分钟就能学会

然后先查看cmap,发现线索,里面注释的地方有标注了。然后我们大胆猜测:NINE对应的name=cid00018,code=0x39,这翻译过来就是9对应的name=cid00018,其id标记为0x39:

woff字体反爬实战,10分钟就能学会

接着来看一下code=0x39,其对应的name=cid00018,然后我们拿这个cid00018去搜索,发现在部分里面看到:

<GlyphID id="3" name="cid00018"/>

,这表明什么呢?结合前后两个映射关系,然后连起来再大胆猜测一下,可能是9对应3?

woff字体反爬实战,10分钟就能学会

为了验证这个猜想,继续再找一下其他例子,我使用已经转换为如下格式,方便你们对比,你们也可以从三张截图来对比,哪三张截图呢?分别是:①是前面包含“code=0x39,name=cid00018”的截图;②是包含“id=3,name=cid00018”的截图;③是文章的第二张截图。

你们可以①②截图来一个个列出映射关系,建议先列出①的映射关系,再列出②的映射关系,然后再将①、②的映射关系组合起来,得出一个新的映射关系,这个新的映射关系就是我们所需的,下面来给你们看一下我提取的①、②的映射关系:

①的映射关系,在这里我定义为before_code_id
②的映射关系,在这里我定义为affter_code_id,结果如下:
before_code_id =  {
    "0": "cid00019",
    "1": "cid00020",
    "2": "cid00017",
    "3": "cid00021",
    "4": "cid00022",
    "5": "cid00024",
    "6": "cid00026",
    "7": "cid00025",
    "8": "cid00023",
    "9": "cid00018"
}
affter_code_id = {
    "cid00017": 2,
    "cid00018": 3,
    "cid00019": 4,
    "cid00020": 5,
    "cid00021": 6,
    "cid00022": 7,
    "cid00023": 8,
    "cid00024": 9,
    "cid00025": 10,
    "cid00026": 11
}

然后从html源码到before_code_id, affter_code_id应用起来就是如下:
前端数字—中间人code—最终的数字,即:
"0"——"cid00019"——4
"1"——"cid00020"——5
"2"——"cid00017"——2
"3"——"cid00021"——6
"4"——"cid00022"——7
"5"——"cid00024"——9
"6"——"cid00026"——11
"7"——"cid00025"——10
"8"——"cid00023"——8
"9"——"cid00018——3

我们再简化一步,直接从html源码数字到最终的数字映射为如下(即直接省去中间的cidxxxxx这串):
"0"——4
"1"——5
"2"——2
"3"——6
"4"——7
"5"——9
"6"——11
"7"——10
"8"——8
"9"——3


但是你们发现这映射后的数字很奇怪吗,比如"6"、"7"映射之后分别为11和10,
但是在我们的正常逻辑之中不对呀,要不我们再列一下html源码跟前端的肉眼看到的数字的映射关系呗:
"0"——2
"1"——3
"2"——0
"3"——4
"4"——5
"5"——7
"6"——9
"7"——8
"8"——6
"9"——1
哇,这列出来之后不是很相似吗,跟前面的结果,要不我再放在一起给你们好对比一下呗:
xml提取的映射     html源码跟网页展示的,提取的映射
"0"——4				"0"——2
"1"——5				"1"——3
"2"——2				"2"——0
"3"——6				"3"——4
"4"——7				"4"——5
"5"——9				"5"——7
"6"——11				"6"——9
"7"——10				"7"——8
"8"——8				"8"——6
"9"——3				"9"——1
           

到此,我们发现从xml提取的映射跟html源码跟网页展示的提取的映射数值都是相差2,所以我们大胆猜测:网页上看到的数值是可以从xml提取的映射关系里面每个数字减去2所得的,即:

"0"——4-2=2
"1"——5--2=3
"2"——2-2=0
"3"——6-2=4
"4"——7-2=5
"5"——9-2=7
"6"——11-2=9
"7"——10-2=8
"8"——8-2=6
"9"——3-2=1
           

所以这就是破解了嘛,到此,这个教程总可以理解吧,写得辣么辛苦、改的辣么辛苦,赶快评论点赞收藏一套走起来

好了,别嗨了,实操才是王道,下面来看一下核心代码,如下:

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Time    : 2019/8/19 13:08
# @Author  : qizai
# @File    : crawl_woff.py
# @Software: PyCharm

# 先安装:pip3 install fontTools
import os
import requests
from fake_useragent import UserAgent
from fontTools.ttLib import TTFont  # 对字体文件进行格式转换

base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ua = UserAgent()
header = {
    "user-agent": ua.chrome,
}


def parse_woff(url=""):
    """这里是下载字体并且解析对应的值"""
    global cookie
    global header
    
    woff_dir = os.path.join(base_dir, "statics/woffs/")
    file_name = url.split("/")[-1]
    xml_name = file_name.replace(file_name.split(".")[-1], "xml")
    save_woff = os.path.join(woff_dir, file_name)
    save_xml = os.path.join(woff_dir, xml_name)

    if os.path.exists(save_woff):  # 存在本地的话直接提取本地的文件去解析即可省去下载,避免浪费资源
        font = TTFont(save_woff)
    else:
        resp = requests.get(url=url, cookies=cookie, headers=header)
        with open(save_woff, "wb") as f:
            f.write(resp.content)
            f.close()
        font = TTFont(save_woff)
        font.saveXML(save_xml)  # 转换为xml文件

    cmap = font.getBestCmap()  # 这个是xml源码里面的【数值-中间人code】映射,数值还不一定是html源码里面的数值,而是每位数经过加上一定的数值之后的
    tmp = {  # 这个是对应的才是我们需要的值,或者你也可以在每次获取的时候,将这个值对应减去48即可,就可以省去这这个映射
        48: 0,  # html源码里面的0对应xml源码里面的48
        49: 1,  # html源码里面的1对应xml源码里面的49
        50: 2,  # html源码里面的2对应xml源码里面的50
        51: 3,  # html源码里面的3对应xml源码里面的51
        52: 4,  # html源码里面的4对应xml源码里面的52
        53: 5,  # html源码里面的5对应xml源码里面的53
        54: 6,  # html源码里面的6对应xml源码里面的54
        55: 7,  # html源码里面的7对应xml源码里面的55
        56: 8,  # html源码里面的8对应xml源码里面的56
        57: 9,  # html源码里面的9对应xml源码里面的57
    }    # 注意:个人猜测以上这个tmp字典,xml源码的数字跟html源码数字的映射关系可能会定期改变的

    before_code_id = {}  # 转换之后before_code_id为:1:cid00019  key就是html源码数字,value就是用来查询的中间人code
    for k, v in cmap.items():
        if k not in set(range(48, 58)):
            continue
        before_code_id[tmp.get(k)] = v  # 这一步其实是将49:cid00019的映射格式转换为好理解的1:cid00019映射关系

    code_id_list = font.getGlyphOrder()[2:]  # 这个返回的值有11个,但是我这里只是取了第三个到最后一个,是用来取计算前端看到的真正的数值
    affter_code_id = {k:v for k,v in zip(code_id_list, range(2, 12))}  # 将每一个按照顺序映射为cid00562:2这种

    return before_code_id, affter_code_id


if __name__ == '__main__':
    """使用如下"""
    before_code_id, affter_code_id = parse_woff(url="xxxx")
    
    # html源码数字:假设为0
    html_number = 0
    tmp_code = before_code_id.get(html_number)  # 先匹配中间人code
    real_number = affter_code_id.get(tmp_code) - 2  # 再提取中间人code对应的真正的数字,记得要减去2,因为本来是每位数字已经多了2
    print("当前html源码数字html_number:{} 真正的数字为real_number:{}".format(html_number, real_number))
           

当前的woff字体反爬已经破解了,如果有不妥的地方请指出,大家一起学习。

至此本文教程写完了,希望能够帮助到各位在爬虫路上的小伙伴们,觉得不错点个赞呗
感谢认真读完这篇教程的您

先别走呗,这里有可能有你需要的文章:

CSS字体反爬实战,10分钟就能学会;

爬虫:js逆向目前遇到的知识点集合

python最好用的第三方库资源下载网址

详细讲解aiohttp异步请求及使用,高效率