spiderdemo-T8字体反扒

打乱

拿到映射文件

这里是不一样的

md,我还没太理解,ai就给我跑出来了

🎯 字体反爬完整破解流程总结

让我为你详细总结整个破解过程,从0到1完整分析:

📖 一、反爬机制原理

1.1 网站的工作流程

用户访问页面

浏览器加载 HTML + CSS + JavaScript

JavaScript 发送 AJAX 请求到 API

API 返回两个关键数据:

① page_data: ["3184", "9980", "2437", ...] ← 混淆的随机数字

② b64Font: "AAEAAA..." ← Base64编码的字体文件

JavaScript 动态创建 @font-face 样式

浏览器使用自定义字体渲染数字

用户看到真实的数字!

1.2 核心机制:字体映射被打乱

标准字体的结构:

GlyphOrder(字形在字体文件中的存储顺序)

'.notdef', 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'

↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑

显示0 显示1 显示2 显示3 显示4 显示5 显示6 显示7 显示8 显示9

CMAP(Unicode 字符码点 → 字形名称)

{

48: 'zero', # 字符 '0' (ASCII 48) → 字形 'zero'

49: 'one', # 字符 '1' (ASCII 49) → 字形 'one'

50: 'two', # 字符 '2' (ASCII 50) → 字形 'two'

...

}

反爬字体的结构:

GlyphOrder(被打乱!)

'.notdef', 'seven', 'nine', 'zero', 'three', 'four', 'six', 'eight', 'one', 'five', 'two'

↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑

显示0 显示1 显示2 显示3 显示4 显示5 显示6 显示7 显示8 显示9

CMAP(保持标准,不修改!)

{

48: 'zero', # 字符 '0' → 字形 'zero'

49: 'one', # 字符 '1' → 字形 'one'

50: 'two', # 字符 '2' → 字形 'two'

...

}

关键点:浏览器如何显示数字?

用户看到 "3184" 这个字符串

浏览器处理过程:

  1. 字符 '3' (ASCII 51)
  2. 查 CMAP: 51 → 'three'
  3. 查 GlyphOrder: 'three' 排在第4位(索引3)
  4. 显示为数字:3 ✓

但是!如果 GlyphOrder 被打乱了:

'.notdef', 'seven', 'nine', 'zero', 'three', ...

索引0 索引1 索引2 索引3 索引4

那么:

  • 字符 '0' → 'zero' → 索引3 → 显示为 3
  • 字符 '1' → 'one' → 索引7 → 显示为 7
  • 字符 '2' → 'two' → 索引9 → 显示为 9
  • 字符 '3' → 'three' → 索引4 → 显示为 4
    🔍 二、破解思路
    2.1 核心思路
    目标: 建立一个映射表,把混淆数字转换为真实数字

方法:

获取字体文件

解析 GlyphOrder 的顺序

找出每个数字字形在 GlyphOrder 中的位置

建立映射:字符码点 → 真实数字

2.2 详细步骤

步骤1:获取API数据

import requests

import json

请求API

url = "https://spiderdemo.cn/font_anti/api/font_anti_challenge/page/2/"

response = requests.get(url, cookies={'sessionid': '...'})

data = response.json()

得到:

page_data = ["3184", "9980", "2437", ...] # 混淆的数字

b64_font = "AAEAAA..." # Base64字体

步骤2:解析字体文件

import base64

from fontTools.ttLib import TTFont

import io

解码字体

font_data = base64.b64decode(b64_font)

font = TTFont(io.BytesIO(font_data))

获取关键信息

cmap = font.getBestCmap()

cmap = {

48: 'zero',

49: 'one',

50: 'two',

51: 'three',

52: 'four',

53: 'five',

54: 'six',

55: 'seven',

56: 'eight',

57: 'nine',

}

glyph_order = font.getGlyphOrder()

glyph_order = [

'.notdef',

'seven', # 索引1

'nine', # 索引2

'zero', # 索引3

'three', # 索引4

'four', # 索引5

'six', # 索引6

'eight', # 索引7

'one', # 索引8

'five', # 索引9

'two', # 索引10

]

步骤3:建立映射表

3.1 找出所有数字字形(0-9对应的字形)

digit_glyphs = {}

for code_point, glyph_name in cmap.items():

if 48 <= code_point <= 57: # '0'-'9'

digit_glyphs[glyph_name] = code_point - 48

digit_glyphs = {

'zero': 0,

'one': 1,

'two': 2,

'three': 3,

'four': 4,

'five': 5,

'six': 6,

'seven': 7,

'eight': 8,

'nine': 9,

}

3.2 按 glyph_order 中的位置排序

sorted_glyphs = sorted(

digit_glyphs.keys(), # ['zero', 'one', 'two', ...]

key=lambda g: glyph_order.index(g)

)

sorted_glyphs = [

'seven', # glyph_order[1] → 显示为 0

'nine', # glyph_order[2] → 显示为 1

'zero', # glyph_order[3] → 显示为 2

'three', # glyph_order[4] → 显示为 3

'four', # glyph_order[5] → 显示为 4

'six', # glyph_order[6] → 显示为 5

'eight', # glyph_order[7] → 显示为 6

'one', # glyph_order[8] → 显示为 7

'five', # glyph_order[9] → 显示为 8

'two', # glyph_order[10] → 显示为 9

]

3.3 建立最终映射:字符码点 → 真实数字

char_to_real = {}

for idx, glyph_name in enumerate(sorted_glyphs):

找到这个字形对应的Unicode码点

for code_point, gp in cmap.items():

if gp == glyph_name and 48 <= code_point <= 57:

char_to_real[code_point] = idx

break

char_to_real = {

48: 2, # '0' → 显示为 2

49: 7, # '1' → 显示为 7

50: 9, # '2' → 显示为 9

51: 3, # '3' → 显示为 3

52: 4, # '4' → 显示为 4

53: 8, # '5' → 显示为 8

54: 5, # '6' → 显示为 5

55: 0, # '7' → 显示为 0

56: 6, # '8' → 显示为 6

57: 1, # '9' → 显示为 1

}

步骤4:解密数字

def decrypt_number(obfuscated_num, mapping):

"""

将混淆的数字转换为真实数字

复制代码
例如:"3184" → "3764"
"""
result = []
for char in obfuscated_num:
    code_point = ord(char)  # '3' → 51
    if code_point in mapping:
        result.append(str(mapping[code_point]))
    else:
        result.append(char)
return ''.join(result)

示例

mapping = {48: 2, 49: 7, 50: 9, 51: 3, 52: 4, ...}

decrypted = decrypt_number("3184", mapping)

过程:

'3' → code_point=51 → mapping[51]=3 → '3'

'1' → code_point=49 → mapping[49]=7 → '7'

'8' → code_point=56 → mapping[56]=6 → '6'

'4' → code_point=52 → mapping[52]=4 → '4'

结果:"3764"

💻 三、完整代码实现

3.1 核心类封装

class FontAntiSpider:

def init (self, sessionid):

self.sessionid = sessionid

self.session = requests.Session()

self.session.cookies.set('sessionid', sessionid)

复制代码
def fetch_page(self, page=1):
    """获取指定页面数据"""
    url = f"https://spiderdemo.cn/font_anti/api/font_anti_challenge/page/{page}/"
    response = self.session.get(url)
    return response.json()

def parse_font_mapping(self, b64_font):
    """解析字体,建立字符到真实数字的映射"""
    font_data = base64.b64decode(b64_font)
    font = TTFont(io.BytesIO(font_data))
    
    cmap = font.getBestCmap()
    glyph_order = font.getGlyphOrder()
    
    # 找出数字字形
    digit_glyphs = {}
    for code_point, glyph_name in cmap.items():
        if 48 <= code_point <= 57:
            digit_glyphs[glyph_name] = code_point - 48
    
    # 按glyph_order排序
    sorted_glyphs = sorted(
        digit_glyphs.keys(),
        key=lambda g: glyph_order.index(g)
    )
    
    # 建立映射
    char_to_real = {}
    for idx, glyph_name in enumerate(sorted_glyphs):
        for code_point, gp in cmap.items():
            if gp == glyph_name and 48 <= code_point <= 57:
                char_to_real[code_point] = idx
                break
    
    return char_to_real

def decrypt_number(self, obfuscated_num, mapping):
    """解密单个数字"""
    result = []
    for char in obfuscated_num:
        code_point = ord(char)
        if code_point in mapping:
            result.append(str(mapping[code_point]))
        else:
            result.append(char)
    return ''.join(result)

def crawl_all_pages(self, total_pages=100):
    """爬取所有页面"""
    all_data = []
    
    for page in range(1, total_pages + 1):
        data = self.fetch_page(page)
        mapping = self.parse_font_mapping(data['b64Font'])
        
        decrypted_page = [
            self.decrypt_number(num, mapping)
            for num in data['page_data']
        ]
        
        all_data.extend(decrypted_page)
    
    return all_data

3.2 使用示例

创建爬虫实例

spider = FontAntiSpider("6cn92feun22342isgqer8d8hmcxrtpu1")

爬取所有页面

result = spider.crawl_all_pages(total_pages=100)

计算总和

total = sum(int(num) for num in result)

print(f"总和: {total}") # 输出: 5636267

📊 四、实际运行结果

4.1 单页测试结果

原始混淆数据:

  1. 3184
  2. 9980
  3. 2437
  4. 8692
  5. 5225

字体映射关系:

GlyphOrder: ['.notdef', 'seven', 'nine', 'zero', 'three', 'four', ...]

字形→数字: {'seven': 0, 'nine': 1, 'zero': 2, 'three': 3, ...}

解密后数据:

  1. 3764
  2. 1162
  3. 9430
  4. 6519
  5. 8998

本页总和: 58077

4.2 100页完整爬取结果

================================================================================

开始爬取 100 页数据

OK\] 第 1 页: 获取 10 条 \| 已采集 10 条 \| 本页示例: 8716 \[OK\] 第 10 页: 获取 10 条 \| 已采集 100 条 \| 本页示例: 5165 \[OK\] 第 20 页: 获取 10 条 \| 已采集 200 条 \| 本页示例: 3492 -\> 进度已保存: progress_page_20.json ... \[OK\] 第 100 页: 获取 10 条 \| 已采集 1000 条 \| 本页示例: 2981 ## ================================================================================ 爬取完成! 总采集条数: 1000 结果文件: \[OK\] final_result.json - 完整数据 \[OK\] answer.txt - 答案(总和) \[SUM\] 所有数字总和: 5636267 (可直接提交 answer.txt 中的内容) 🔑 五、关键要点总结 5.1 核心原理 ✅ 字体反爬的本质: 修改 GlyphOrder 的顺序,而非修改 CMAP ✅ 每次请求动态生成字体: 每次请求返回不同的字体文件 ✅ 数据随机混淆: 每次请求返回不同的随机数字 ✅ 浏览器渲染规则: 字形在 GlyphOrder 中的位置 = 显示的数字 5.2 破解关键步骤 1. 获取 API 响应(b64Font + page_data) ↓ 2. 解析字体文件(使用 fontTools) ↓ 3. 提取 GlyphOrder 和 CMAP ↓ 4. 建立映射表(字符码点 → 真实数字) ↓ 5. 解密所有数字 ↓ 6. 计算总和 5.3 重要细节 GlyphOrder 的顺序决定显示: 第一个数字字形显示为0,第二个为1,依此类推 CMAP 保持标准: 不修改 Unicode → 字形的映射 动态字体: 每次请求需要重新解析字体 Base64 编码: 字体文件通过 Base64 编码传输 📁 六、文件结构 8-字体反爬/ ├── spider_all.py # ⭐ 完整爬虫(100页自动化) ├── final_solution.py # 单页测试脚本 ├── font_anti_solver.py # 字体破解器类 ├── test_font_anti.py # 测试脚本 ├── full_spider.py # 网页分析脚本 ├── font.py # 基础示例 ├── README.md # 完整文档 ├── answer.txt # 最终答案:5636267 └── final_result.json # 完整数据(1000条) 🎓 七、扩展学习 7.1 其他字体反爬类型 字形名称混淆 使用 glyph_001, glyph_002 代替 one, two 需要通过字形坐标特征识别 CSS 类名映射 7 → 显示为5 需要解析 CSS 文件 动态 SVG 字体 使用 SVG 路径绘制数字 需要 OCR 识别 7.2 推荐工具 fontTools: Python字体解析库 FontForge: 字体编辑器(可视化分析) Tesseract: OCR引擎(识别字形) 这就是完整的字体反爬破解流程!核心是理解 GlyphOrder 的顺序决定了数字的显示,通过解析字体文件建立映射表即可破解。🎉 暂时先这样,后期有兴趣继续弄

相关推荐
青小莫2 小时前
C++之模板
android·java·c++
装不满的克莱因瓶2 小时前
Android Studio 的模拟器如何上传本地图片到手机相册
android·智能手机·android studio
三金121383 小时前
深入解析MySQL EXPLAIN
android
_李小白3 小时前
【Android 美颜相机】第十一天:GPUImageFilter解析
android·数码相机
dawudayudaxue4 小时前
sqlite在安卓下使用ndk的交叉编译
android·数据库·sqlite
YIN_尹4 小时前
【MySQL】表的约束(下)
android·数据库·mysql
爱编码的傅同学4 小时前
【线程的同步与互斥】初识互斥量与锁
android·java·开发语言
_李小白4 小时前
【Android 美颜相机】第十天:YUV420SP和RGB
android·数码相机
2501_944526424 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 收藏功能实现
android·java·开发语言·javascript·python·flutter·游戏