题目地址:http://www.glidedsky.com/level/crawler-font-puzzle-1

写一下之前处理过的两个字体反爬实战,也是很常见的一种反爬类型,这是第一篇

先来看一下题目

image.png

源码拿到的数字,和实际显示在网页的数字,明显不一样的

image.png

注意到两个现象

  • 每一次刷新,源码中的数字就跟着变动,说明每请求一次页面,就使用了新的ttf字体文件
  • 数字看起来无序,其实是有映射关系的,比如 122 变成了 277226 变成了 773

所以这里的解题思路就是,解析ttf文件,得到数字之间的映射关系,然后结合网页源码提取到的数字,就能获取真实的数字了

作者已经提示,ttf文件内嵌在源码的base64中

image.png

复制那一大串字符,进行解码,然后保存成 ttf文件

1
2
3
4
content = base64.b64decode(b64_str)

with open("page-1.ttf", "wb") as f:
f.write(content)

使用专门的字体查看工具打开文件,这里我使用的是 FontCreator 9.1

image.png

通过比对网页数字、源码数字,我们发现映射关系是一致的,比如:8 显示成 1,4 显示成 0

image.png

接下来就是要怎么拿到这些映射关系了?根据ttf文件提示,我们只要拿到上面那一栏小标题 four one eight ...,然后按顺序找到就能对应的真实数字,比如 four 对应 0, one 对应 1

那么怎么拿到解析 ttf文件拿到小标题呢?有两种方式,一种是解析xml,一种是使用专门的字体解析库 fontTools

这里使用 fontTools,后面其他的文章我会介绍xml的

参考:https://fonttools.readthedocs.io/en/latest/ttLib/ttFont.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from fontTools.ttLib import TTFont

def parse_ttf():
"""
return
{
源码数字:真实数字
}
"""
font = TTFont("page-1.ttf")
# 获取节点名列表
name_lst = font["cmap"].tables[0].ttFont.getGlyphOrder()
en2num = {
"nine": 9,
"eight": 8,
"two": 2,
"six": 6,
"three": 3,
"four": 4,
"seven": 7,
"one": 1,
"five": 5,
"zero": 0,
}
# 生成映射表
map_list = {
str(en2num[word]): str(index) for index, word in enumerate(name_lst[1:])
}
return map_list

拿到映射表后,我们提取源码中的数字就能得到真实的数字了

代码整理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# -*- coding:utf-8 -*-

import base64
import requests

from io import BytesIO
from fontTools.ttLib import TTFont
from parsel import Selector


def glidedsky_login():
"""
网站登录,才能看到题目
注意题目域名也必须是 www.glidedsky.com
"""
EMAIL = ""
PASSWORD = ""
LOGIN_URL = "http://www.glidedsky.com/login"

session = requests.session()
resp = session.get(LOGIN_URL)
dom = Selector(resp.text)
_token = dom.css("meta[name='csrf-token']::attr(content)").get()
form_data = {
"_token": _token,
"email": EMAIL,
"password": PASSWORD,
}
session.post(LOGIN_URL, data=form_data)
return session


def parse_ttf(b64_str):
"""
return
{
源码数字:真实数字
}
"""
content = base64.b64decode(b64_str)
# 使用 BytesIO 构建一个临时文件对象
font = TTFont(BytesIO(content))

# 获取节点名列表
name_lst = font["cmap"].tables[0].ttFont.getGlyphOrder()
en2num = {
"nine": 9,
"eight": 8,
"two": 2,
"six": 6,
"three": 3,
"four": 4,
"seven": 7,
"one": 1,
"five": 5,
"zero": 0,
}
# 生成映射表
map_list = {
str(en2num[word]): str(index) for index, word in enumerate(name_lst[1:])
}
return map_list


def parse_html(html_text):
dom = Selector(text=html_text)
b64_str = dom.css("style::text").re_first(r"base64,(.+?)\)")
mappings = parse_ttf(b64_str)

fake_nums = dom.css(".col-md-1::text").re(r"\d+")
for num in fake_nums:
real_num = "".join(mappings[n] for n in num)
yield real_num


def main():
session = glidedsky_login()

for page in range(1, 11):
url = "http://www.glidedsky.com/level/web/crawler-font-puzzle-1?page=" + str(page)
resp = session.get(url)
print(f"page {page}: ", [num for num in parse_html(resp.text)])


if __name__ == "__main__":
main()

涉及到的知识点:

运行结果

image.png