T10 字体SVG挑战

简要说一下SVG字体反爬:服务器将数字画成矢量图形(SVG 路径),前端通过特殊字体渲染,让你无法直接从 HTML 或 JSON 里拿到明文数字

一:抓包分析

翻页后查看接口返回

image-20260508221049658

接口返回json,包含一个svg_content字段,一大段XML,网页上显示的是正常数字。下一个blob请求,直接返回的是当前数字

仔细分析下svg_content值,可以看到path下的代码,同时一共有40个,对应每一个字母

image-20260508222008716

涉及到这种图像的字体,一般想到的会使用ocr识别这些。

而本文中我们使用的是模版匹配,同一个网站使用的SVG字体是固定的。也就是说映射是不变的。我们找到首页的数字与svg_content生成一个模版,后续的字母都是在本模版上进行匹配。

二:获取模版样本

本文以第二页为模版

image-20260508222506642

值为: [1779, 7315, 4463, 1366, 7080, 5154, 7302, 1472, 8363, 8264]

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
def extract_paths_from_svg(svg_content):
"""从 SVG 中提取所有 path 的 d 属性,并按 transform translate X 排序"""
root = etree.fromstring(svg_content.encode('utf-8'))
ns = {'svg': 'http://www.w3.org/2000/svg'}
paths_with_x = []
for elem in root.xpath('//svg:path', namespaces=ns):
d = elem.get('d')
trans = elem.get('transform', '')
m = re.search(r'translate\(([^,]+),', trans)
x = float(m.group(1)) if m else 0.0
paths_with_x.append((x, d))
paths_with_x.sort(key=lambda t: t[0])
return [d for _, d in paths_with_x]

def build_template(svg_content, actual_numbers):
"""
actual_numbers: 整数列表,长度应与 SVG 中的 path 数量一致
返回模板列表 [(path_string, digit), ...]
"""
paths = extract_paths_from_svg(svg_content)
# 将实际数字展开为单个数字列表(例如 1779 -> [1,7,7,9])
digits = [int(d) for num in actual_numbers for d in str(num)]
if len(paths) != len(digits):
raise ValueError(f"路径数量({len(paths)})与数字数量({len(digits)})不匹配")
templates = list(zip(paths, digits))
return templates

def save_templates(templates, filepath='templates.pkl'):
with open(filepath, 'wb') as f:
pickle.dump(templates, f)

data = {
"svg_content": "“
}
actual = [1779, 7315, 4463, 1366, 7080, 5154, 7302, 1472, 8363, 8264]

# 实际使用时,请替换为你的真实数据
templates = build_template(data['svg_content'], actual)
save_templates(templates)

代码就不讲解了,这些就是保存模版。

通过模版匹配新的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def recognize_with_templates(svg_content, templates):
"""
templates: 模板列表 [(path_string, digit), ...]
返回识别出的整数列表(每4位一组)
"""
paths = extract_paths_from_svg(svg_content)
result_digits = []
for path in paths:
best_dist = float('inf')
best_digit = None
for tmpl_path, digit in templates:
dist = Levenshtein.distance(path, tmpl_path)
if dist < best_dist:
best_dist = dist
best_digit = digit
result_digits.append(best_digit)
# 每4个数字一组转成整数
result_numbers = []
for i in range(0, len(result_digits), 4):
num = int(''.join(map(str, result_digits[i:i+4])))
result_numbers.append(num)
return result_numbers

测试识别的时候用的第一页,

image-20260508222854467

image-20260508222848877

结果一致

三:验证

看下前三页识别的数据

image-20260508224156039

image-20260508224208821

image-20260508224216390

完全一致

image-20260508224252900

更多内容也在公众号更新:码字的秃猴

tuhou