目录

正文

前置分析

补环境

分析

编写爬虫

题外话——html元素进行截屏

硬解

分析

读取four坐标

matplotlib的简单使用

svg的使用

编写爬虫


正文

这是一道关于关于字体反爬的题

看看是怎么个事情。

T8-字体反爬https://www.spiderdemo.cn/font_anti/font_anti_challenge/?challenge_type=font_anti_challenge

前置分析

点击不同的页面,发现请求如下

获取第5页,需要一个woff2文件,

获取的输入如下

这个page_dat的第一个元素是0602,

但是显示如下

显示的结果是4547,看来这就是所谓的字体反爬,真实的信息被映射成虚假的信息

4547是真实的信息,而0602是虚假的信息。

因为这道题比较简单,所以笔者有两种做法

第一种是补环境——下载对应的文件,展示真实的数据,识别。

第二种是硬解——获取字体文件,展示识别。

​​​总之,需要识别。

对应识别的工具,笔者选择使用ddddocr,看个人需求。

补环境

分析

首先,笔者在某个目录下,新建一个index.html

获取网页的html,运行

里面有很多报错,笔者删除掉没用的东西,比如什么css,什么anti_automation.js,没有用

这个font_anti.js是很有用的,下载下来

修改之后,出现了新的报错

获取数据失败,有点意思,而且是一个函数generateNumbers函数

经过分析,可以发现这个函数最后反正page_data,这个虚假的数据。那么直接修改源码

直接返回

async function generateNumbers(_0x3b1417) {
    return [
      "0602",
      "2274",
      "8403",
      "8732",
      "8227",
      "4926",
      "3016",
      "3088",
      "4526",
      "4568"
    ]
}

再运行html,可以发现

页面变了,0602之类的虚假信息。

是什么调用了generateNumbers?

搜索发现

是loadPageData调用了generateNumbers

其中还有什么readerNumbers,进去看看

这是在渲染页面,哦,明白了

最关键的函数是这个readerNumbers。

因此,修改loadPageData源码

async function loadPageData() {
    let page_data=[
      "0602",
      "2274",
      "8403",
      "8732",
      "8227",
      "4926",
      "3016",
      "3088",
      "4526",
      "4568"
    ]
    renderNumbers(page_data);
}

页面没什么变化。

没有真实的信息,还缺少了什么东西???

查看返回

发现了一个b64Font,里面是base64字符串,显示就是一个字体文件,

笔者在index.html里面搜索b64Font,发现如下东西

显然,发送请求,获取字体文件,调用loadCustomFont函数。

笔者直接去掉没有用的东西,修改源码

    async function apiGetPageData(page, type = challengeType) {
        let b64Font="aaaaa"
        loadCustomFont(b64Font);
    }
    apiGetPageData()

笔者省略了真实的b64Font,因为太长了,意思一下就可以了

此时,发现页面

0602变成了4547,其他的数据也变了,哈哈哈哈哈,可以

暂时做个总结,

  1. 发送请求,获取page_data里面虚假的数据,和b64Font字符串
  2. page_data传入到renderNumbers,b64Font传入到loadCustomFont

暂时先写到这。


====新的一天=====


继续操作,获取新的数据,现在可以渲染了,还需要获取的元素,如下

可以发现是class=numbers-grid这个div元素,里面的div子元素

编写爬虫

根据前面的分析,只有两个函数,因此,新建一个index.html文件

笔者修改了一些css

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>T8-字体反爬</title>
</head>
<body>
<div class="numbers-grid" id="numbersGrid">
</div>
<script>
        function renderNumbers(_0x3bd5f) {
            const _0x2ba44a = document['getElementById']('numbersGrid');
            _0x2ba44a['innerHTML'] = _0x3bd5f['map']((_0x27a712, _0x5d9262) => '<div\x20class=\x22number-box\x22\x20style=\x22animation-delay:\x20' + _0x5d9262 * 0.1 + 's\x22>' + _0x27a712 + '</div>')['join']('');
        }
        function loadCustomFont(base64Font) {
            // 移除之前的字体样式(如果存在)
            const existingStyle = document.getElementById('custom-font-style');
            if (existingStyle) {
                existingStyle.remove();
            }
            // 创建新的字体样式
            const style = document.createElement('style');
            style.id = 'custom-font-style';
            style.textContent = `
                @font-face {
                    font-family: 'AntiSpiderFont';
                    src: url(data:font/woff2;base64,${base64Font}) format('woff2');
                    font-weight: normal;
                    font-style: normal;
                }
                
                    .numbers-grid {
                        display: inline-flex;  
                        gap: 2px;               
                        font-family: 'AntiSpiderFont', monospace;
                    }
                    .number-box {
                        display: inline-block;
                        min-width: 48px;        
                        height: 20px;
                        line-height: 20px;
                        text-align: center;
                        font-family: 'AntiSpiderFont', monospace;
                        padding: 0 2px;         
                        box-sizing: content-box;
                    }

                     .number-item, .number, .data-item {
                     font-family: 'AntiSpiderFont', monospace !important;
                     font-variant-numeric: normal;
                     font-feature-settings: normal;
                 }
                 .numbers-grid * {
                     font-family: 'AntiSpiderFont', monospace !important;
                 }
            `;
            document.head.appendChild(style);
        }
    </script>
</body>
</html>

保留上面提到的两个函数。现在使用DP调用这两个函数即可

python代码如下

import requests
import time
import base64
from DrissionPage import ChromiumPage
import json
page=ChromiumPage()
page.get("http://localhost:63342/..../index.html")
url='https://www.spiderdemo.cn/font_anti/api/font_anti_challenge/page/1'
cookie={
    'sessionid':'xxxxxxx'
}
params={
    'challenge_type':'font_anti_challenge',
}
headers={
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0'
}
res=requests.get(url=url,params=params,cookies=cookie,headers=headers)
data=res.json()
print(data)
b64_font=data['b64Font']
page_data=data['page_data']
page_data_str = json.dumps(page_data)
page.run_js('return renderNumbers(JSON.parse(arguments[0]))',page_data_str)
page.run_js('return loadCustomFont(arguments[0])',b64_font)

其中传入的参数是json字符串,不然会报错,亲自尝试一下就明白。

运行,结果如下

可以发现1861这些数字,这些就是真实的信息,现在需要获取这个信息

如何获取???

那就需要使用截屏了,即

🚤 截图和录像 | DrissionPage官网https://drissionpage.cn/dp40docs/ChromiumPage/screen/需要先获取元素,然后截屏,然后识别,即

import ddddocr
ocr = ddddocr.DdddOcr(show_ad=False)
grid =page.ele("@class=numbers-grid")
children = grid.eles("tag:div")
for child in children:
    real_number=ocr.classification(child.get_screenshot(as_bytes='png'))
    print('渲染后的数字:', real_number)

结果如下

可以发现不错,很好,全部的爬虫代码如下

import ddddocr
import requests
import time
import base64
from DrissionPage import ChromiumPage
import json
html=ChromiumPage()
ocr = ddddocr.DdddOcr(show_ad=False,beta=True)
html.get("http://../index.html")

cookie={
    'sessionid':'xxxx'
}
params={
    'challenge_type':'font_anti_challenge',
}
headers={
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0'
}
total=0
for page in range(1,101):
    url=f'https://www.spiderdemo.cn/font_anti/api/font_anti_challenge/page/{page}'
    res=requests.get(url=url,params=params,cookies=cookie,headers=headers)
    data=res.json()
    b64_font=data['b64Font']
    page_data=data['page_data']
    page_data_str = json.dumps(page_data)
    html.run_js('return renderNumbers(JSON.parse(arguments[0]))',page_data_str)
    html.run_js('return loadCustomFont(arguments[0])',b64_font)
    grid =html.ele("@class=numbers-grid")
    children = grid.eles("tag:div")
    for child in children:
        string_result=ocr.classification(child.get_screenshot(as_bytes='png'))
        total+=int(string_result)
    print('第'+str(page)+'页获取完成')
print(total)

为什么会加一个bate,这个好像是测试版本,主要是因为出现如下报错

Traceback (most recent call last):
  File "F:\code\Python\study-spider\src\spiderDemo\T8\one\main.py", line 33, in <module>
    total+=int(ocr.classification(child.get_screenshot(as_bytes='png')))
           ~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: '3O77'

看来是把0识别成了O。然后添加了beta

结果如下

没问题。


====新的一天=====


题外话——html元素进行截屏

额,昨天爬虫虽然成功了,但是,笔者在解决识别的问题上,

刚开始,不知道有bate这个参数,笔者不是这个方面的专家,比较菜。。。。

还以为是DP截屏不清楚,导致识别的错误,

因此,笔者专门搜了搜,如何对html元素进行截屏???

发现了两个工具html2Caves和modern-screenshot

modern-screenshot - npmhttps://www.npmjs.com/package/modern-screenshot直言的说,笔者都尝试了一下,感觉modern-screenshot更好

因此,丢掉DP的截屏工具,使用modern-screenshot。

需要安装——pnpm i modern-screenshot

新建一个main.html,关于modern-screenshot部分的代码如下

<script type="module">
    import {domToBlob} from './node_modules/modern-screenshot/dist/index.mjs'
    const get_shot = async () => {
        const bytesArr = [];
        const grid = document.getElementById('numbersGrid');
        const boxes = grid.querySelectorAll('.number-box');
        for (const box of boxes) {
            const blob = await domToBlob(box, {scale: 4, type: 'image/png'});
            const buffer = await blob.arrayBuffer();
            const binStr = String.fromCharCode(...new Uint8Array(buffer));
            bytesArr.push(binStr);          
        }
        return bytesArr;
    };
     window.get_shot = get_shot
</script>

返回一个数组,里面是byte字符串。

在python方面,如下

    numbers_list=html.run_js('return get_shot()')
    for number in numbers_list:
        number_bytes = number.encode('latin1')
        result=ocr.classification(number_bytes)
        total+=int(result)

当然,还是需要设置beta,不然还是会报错,为什么是latin1

因为 latin1(ISO-8859-1)的编码规则是 “码点 0-255 直接映射为字节 0x00-0xFF”,一对一、无变换、无压缩、无扩展。

可以看看DP和modern-screenshot截屏的效果

    number_bytes = numbers_list[0].encode('latin1')
    grid = html.ele("@class=numbers-grid")
    child = grid.eles("tag:div")[0]
    child.get_screenshot(path='dp.png')
    with open('mod.png','wb') as f:
        f.write(number_bytes)

上面是dp,下面是modern-screenshot

效果还是不错的。


硬解

分析

硬解字体

  1. 保存字体文件
  2. 把字体所暗含的映射找到
  3. 根据映射把虚假的信息变成真实的信息

直言的说,笔者看到下面这位大佬的解答,非常好

SpiderDemo题解系列——第5篇:字体反爬(第8题)-CSDN博客https://blog.csdn.net/xw1680/article/details/153992358慢慢来,不慌。

首先把字体文件保存下来,关键代码如下

    data=res.json()
    b64_font=data['b64Font']
    page_data=data['page_data']
    with open('font.woff2','wb') as f:
        f.write(base64.b64decode(b64_font))

从前面大佬的博客中可以发现是ttf文件。但是,笔者保存为woff2,其实没什么区别。

要操作字体文件,需要Python的一个库——fontTools

fonttools - 操作字体_python fonttools-CSDN博客https://blog.csdn.net/lovechris00/article/details/140892583

安装了这个库之后,就可以使用里面的 ttx命令了

可以简单使用一下

ttx font.woff

会生成一个font.ttx文件,这个ttx文件就是xml文件,可以指定生成xml

 ttx -o font.xml font.woff2

可以生成多个xml文件,即

 ttx -o font.xml -s font.woff2

即

直言的说,从语法形式上看, xml文件,和html文件没什么根本性区别,但是还是不同,可以参考如下

XML文件(超详细):XML文件概念、作用、写法、如何用程序解析XML、写入XML、dom4j框架、DTD文档、schema文档-CSDN博客https://blog.csdn.net/rainingCSDN/article/details/143905744实际上有许多东西了,笔者直接说重点

首先生成一个xml文件,即

 ttx -o font.xml font.woff2

注意到(attention)

Glyph是字形的意思,那么GlyphID 就是字形的id,什么意思?

在文件中搜索(ctrl+F) ,four这个关键字

可以发现这样一个东西,这里面有x,y,还有什么xMin之类的东西。说白了,这就是坐标

可以读取全部坐标,显示看看,是什么东西???

读取four坐标

笔者本来还想通过python的matplotlib库 读取里面的东西

但是

但是

但是

笔者突然看到

使用 TTF 字体文件 — Matplotlib 3.10.3 文档 - Matplotlib 绘图库https://matplotlib.net.cn/stable/gallery/text_labels_and_annotations/font_file.html笔者突然发现matplotlib可以使用ttf文件,通过实践发现,也是可以使用woff2文件,

哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈,笔者突然有个好玩的想法,哈哈哈哈

算了,先不慌,等一下再说,先把four给画了。

直接给出画图的代码

from fontTools.ttLib import TTFont
from fontTools.pens.basePen import BasePen
import matplotlib.pyplot as plt
from matplotlib.path import Path
import matplotlib.patches as patches

class MatplotlibPen(BasePen):
    def __init__(self):
        super().__init__()
        self.paths = []
        self.current_path = []

    def _moveTo(self, pt):
        if self.current_path:
            self.paths.append(self.current_path)
        self.current_path = [(pt[0], pt[1], Path.MOVETO)]

    def _lineTo(self, pt):
        self.current_path.append((pt[0], pt[1], Path.LINETO))

    def _curveToOne(self, pt1, pt2, pt3):
        self.current_path.append((pt1[0], pt1[1], Path.CURVE3))
        self.current_path.append((pt2[0], pt2[1], Path.CURVE3))
        self.current_path.append((pt3[0], pt3[1], Path.CURVE3))

    def _closePath(self):
        if self.current_path:
            self.current_path.append((0, 0, Path.CLOSEPOLY))
            self.paths.append(self.current_path)
            self.current_path = []

    def get_paths(self):
        if self.current_path:
            self.paths.append(self.current_path)
        return self.paths

# 加载字体
font = TTFont('font.woff2')
glyph_set = font.getGlyphSet()
glyph = glyph_set["four"]
pen = MatplotlibPen()
glyph.draw(pen)
paths = pen.get_paths()

# 画图
fig, ax = plt.subplots(figsize=(4, 4))
for path_data in paths:
    if not path_data:
        continue
    vertices = [(x, y) for x, y, _ in path_data]
    codes = [code for _, _, code in path_data]
    path = Path(vertices, codes)
    patch = patches.PathPatch(path, fill=True, edgecolor='black', facecolor='lightgray')
    ax.add_patch(patch)

ax.set_aspect('equal')
ax.autoscale_view()
ax.axis('off')
plt.title("four")
plt.show()

结果如下

可以发现是1,说明four对应是1

或者说在笔者目前所获取的字体文件是4对应1

再来一张

发现1是对应于7

同理可以获取全部的图像。

但是,笔者有更好玩的想法

matplotlib的简单使用

首先,笔者获取新的字体文件。

在前面,笔者说过,matplotlib可以加载woff2文件,那么下面这段代码

使用获取的字体文件

from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib import font_manager

font_path = Path('./font.woff2')
font_prop = font_manager.FontProperties(fname=font_path)

fig, ax = plt.subplots()

# 1. 背景设为白色
fig.patch.set_facecolor('white')
ax.set_facecolor('white')

ax.axis('off')

# 2. 文字用黑色
ax.text(0.5, 0.5, '0123456789',
        fontproperties=font_prop,
        fontsize=50,
        color='black',
        ha='center', va='center')
plt.savefig('digits.png', facecolor='white', dpi=300)
plt.show()

0123456789,一般会显示0123456789,但是,运行代码

得到的图片如下

直接说明4对应于3,笔者自信,没问题。

那现在就很简单了,已经找到对应关系。

但是,笔者还有其他想法。

svg的使用

看下面代码

from fontTools.ttLib import TTFont
from fontTools.pens.svgPathPen import SVGPathPen

font = TTFont('font.woff2')
gs   = font.getGlyphSet()
pen  = SVGPathPen(gs)

for name in ['zero','one','two','three','four','five','six','seven','eight','nine']:
    pen.__init__(gs)
    gs[name].draw(pen)

    g = font['glyf'][name]
    w, h = g.xMax - g.xMin, g.yMax - g.yMin

    svg_str = f'''<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="{g.xMin} {-g.yMax} {w} {h}" xmlns="http://www.w3.org/2000/svg">
  <g transform="scale(1,-1)">
    <path d="{pen.getCommands()}" fill="#000"/>
  </g>
</svg>'''

    with open(f'{name}.svg', 'w', encoding='utf-8') as f:
        f.write(svg_str)

运行,生成10个svg文件,看一下four.svg

是数字3,没问题。

然后,可以使用html加载svg文件,即新建一个test.html,代码如下

<!doctype html>
<html>
<body>
<script>
const names = ['zero','one','two','three','four','five','six','seven','eight','nine'];
names.forEach((name, i) => {
  const obj = document.createElement('object');
  obj.data = name + '.svg';
  obj.type = 'image/svg+xml';
  obj.width = 200;
  document.body.appendChild(obj);
});
</script>
</body>
</html>

显示如下

问题就已经很简单了。

编写爬虫

笔者决定使用maplotlib来搞事情,添加ddddcor 运行代码,发现识别不了

笔者考虑了一下,可能距离太近了。

以及其他细节上的考虑,笔者就不多言了,总之,爬虫代码如下

import time
from io import BytesIO
import base64
import requests
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib import font_manager
from ddddocr import DdddOcr

ocr = DdddOcr(show_ad=False,beta=True)
def get_map(page):
    font_path = Path(f'./font/font_{page}.woff2') 
    font_prop = font_manager.FontProperties(fname=font_path)
    fig, ax = plt.subplots(figsize=(12, 4))
    fig.patch.set_facecolor('white')
    ax.set_facecolor('white')
    ax.axis('off')
    text = '0123456789'
    char_spacing = 0.2
    for i, char in enumerate(text):
        x_pos = 0.01 + i * 0.5 * char_spacing
        ax.text(x_pos, 0.5, char,
                fontproperties=font_prop,
                fontsize=70,
                color='black',
                ha='center', va='center')
    buf = BytesIO()
    plt.savefig(buf, format='png', facecolor='white', dpi=300, bbox_inches='tight')
    buf.seek(0)
    png_bytes = buf.read()
    res = ocr.classification(png_bytes)
    digit_dict = {str(idx): ch for idx, ch in enumerate(res)}
    buf.close()
    plt.close()
    return digit_dict

cookie={
    'sessionid':'xxxx'
}
params={
    'challenge_type':'font_anti_challenge',
}
headers={
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0'
}
total=0
Path('./font').mkdir(exist_ok=True)
for page in range(1,101):
    url=f'https://www.spiderdemo.cn/font_anti/api/font_anti_challenge/page/{page}'
    res=requests.get(url=url,params=params,cookies=cookie,headers=headers)
    data=res.json()
    b64_font=data['b64Font']
    page_data=data['page_data']
    with open(f'./font/font_{page}.woff2','wb') as f:
        f.write(base64.b64decode(b64_font))
    mapping=get_map(page)
    print(mapping)
    for item in page_data:
        number_str = ''
        for ch in item:
            number_str += str(mapping[ch])
        print(number_str)
        total += int(number_str)
    print("第{}页获取完成".format(page))
print(total)

总之,再次成功

over!!!!!!!!

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐