游戏直播
游戏直播
- 斗鱼 - 每个人的直播平台【官方指定平台
- 斗鱼斗鱼 - 每个人的直播平台【官方指定
- 斗鱼美股跌1093%斗鱼 - 每个人的直
- 甲状腺最新资讯-快科技--科技改变未来斗
- 美女斗鱼 - 每个人的直播平台【官方指定
联系我们
电话:400-123-4567
手机:138-0000-0000
邮箱:admin@youweb.com
地址:广东省广州市天河区88号
斗鱼直播
斗鱼斗鱼 - 每个人的直播平台【官方指定平台】关注人数爬取 ── 字体反爬的攻与防
- 作者:小编
- 发布时间:2026-02-27 03:11:45
- 点击:
游戏直播,电竞直播,手游直播,lol直播,英雄联盟直播,dnf直播,cf直播,绝地求生直播,王者荣耀直播,游戏直播,赛事直播,美女直播,户外直播,二次元直播,斗鱼直播,英雄联盟,绝地求生,和平精英
之前因为业务原因需要爬取一批斗鱼主播的相关数据,在这过程中我发现斗鱼使用了一种很有意思的反爬技术,字体反爬。
打开任何一个斗鱼主播的直播间,例如 这个主播,他的关注人数数据显示在右上角:
斗鱼在关注数据这里使用了字体反爬。什么是字体反爬?也就是通过自定义字体来自定义字符与渲染图形的映射。比如,字符 1 实际渲染的是 9,那么如果 HTML 中的数字是 111,实际显示就是 999。
在这种技术下,传统的通过解析 HTML 文档获取数据的方式就失效了,因为获取到的数据并不是真实数据。
Tip: 下文中所谈的具体细节高度依赖斗鱼网页的实现,很有可能当你阅读这篇文章的时候已经不再是那样。虽然代码会过时地很快,但是,技巧和方法是永远不会过时的。
感兴趣的读者可以打开控制台,看看显示关注人数的那个 span 元素里面的内容,会发现根本不是显示的数字。
从上图可以看出,显示的是 59605,这个是线,这个是虚假值。右边的 font-family 告诉我们这个元素使用了一个自定义字体。
如果大家将字体下载下来,使用如下的 HTML 代码来渲染它,我们就可以看得很清楚:
在这个字体中,字符 0 会渲染图形 0,字符 1 会渲染图形 7,以此类推,因此,HTML 中的字符 96809 渲染的图形就是 59605。
这个原理不难理解,我们甚至不需要知道字体文件内部的具体格式。因为不管怎样,字体内部一定有一个映射关系。这个关系规定了什么样的字符渲染什么样的图形。
正常情况下,字符 1 对应的图形是 1 的形状,但是通过修改字体,我们就可以让字符 1 对应的图形是 9 的形状,或者说,其他任意形状。
这样一来,HTML 中的字符就完全失去了意义,这个字符所代表的的含义要依据字体来定。
上面这个字体实际上定义了一个 0123456789 到 0741389265 的映射,拿到 HTML 中的字符我们需要根据这个映射才能得到真正的值。
刷新斗鱼你会发现,字体又变了,所以它那边肯定是有一个字体库的,但是这个不关键,解决一个就解决了所有。
使用 Headless Browser。也就是使用一个完整的浏览器来渲染整个页面,然后获取 DOM 中的值。这个是兜底办法,永远可行,但是缺点是效率太差。大家可以看一下打开斗鱼直播间页面的速度,正常情况下等到关注人数显示的时候至少需要 5 秒钟。
我们找到数据源。找到斗鱼的 JS 是请求了哪个接口获取到了数据,然后直接请求该接口。
第二个办法的效率会高很多,但是同时也困难很多。因为 JS 通过什么手段获取了数据实在是灵活性太高了,有太多的办法:
面对一堆压缩后的 JS 代码,我们想通过分析代码的方式来找到数据源是不可行的。不过我们还是有别的手段,这个后面再说。
Tip: 第一种办法我顺带提一下,使用 Selenium 或者 Puppeteer 都可以。加载网页以后,轮询直到对应的 DOM 中有值即可,通杀任何网站。
通过解析字体文件可以办到吗?不可以,字体文件中存储的是字符和图形的映射,那么对于一个图形,它是我们眼中的 “0” 还是我们眼中的 “1” 字体文件也没法知道,在字体文件眼中就是一堆坐标。
如果时光倒退 20 年回到 2000 年,恐怕只能这样做了。虽然那个时候也有图形识别技术,但是不像现在这样成熟也不像现在这样随手可得。
但是现在是 2020 年,OCR 图形识别技术已经非常成熟了,我们随便找个 OCR 库应该就够用了。
所以这个问题的解决方案也有了,我们使用字体渲染好图形,然后调用 OCR 识别图形对应的数字便可以获取到映射关系。
现在我们来找数据源,看看斗鱼的 JS 到底是从哪里获取的字体信息和假数据。
这其实是一个很有意思的过程,我建议有兴趣的同学先暂停下来,花点时间自己试着找找,看能不能找到。
我的第一个想法很简单,JS 一定是通过某个接口得到了这些数据,那么,我们把所有网络请求导出为 HAR 格式,然后在里面搜索试试。
Tip: 点击上图中标记为红色的按钮就可以导出请求为 HAR 格式,HAR 是一个文本格式,非常有利于搜索。
我试了用假数据也就是 96809 和字体 ID mpepc5unpb 来搜索,都没有任何结果。
只能说我们的运气不太好,这里情况实在太多了,有可能返回的值经过了一定的处理比如 base64 或者 rot13,也有可能是多接口返回然后再拼接组装。我们没办法进一步验证,只能放弃这条路。
Tip: 其实在大部分情况下,使用关键词搜索一下 HAR 是很有效的手段,很容易找到对应的接口。
既然此路不通,我们换个思路,请求到了数据以后,JS 代码一定会调用相关 API 去修改 DOM,能不能监听到这个动作?在它修改 DOM 的时候打上断点,这样就可以通过调用栈知道是哪段 JS 在做此操作,然后顺藤摸瓜找到对应的接口。
答案是是可以的,通过使用 MutationObserver 我们可以监听任意 DOM 的修改事件。
Tip: 之后爬取像斗鱼这样的复杂网站,应该先检查一下 WebSocket 中的消息。
至此我们的数据源问题解决了一半,我们知道了数据是来自 WebSocket 发送的响应。但是,如何程序化去获取这个响应?
分析 WebSocket 消息会发现,客户端连接以后会发送一条登录消息,然后服务端会回复多个消息,其中,就有我们感兴趣的 followed_count。
虽然是二进制消息,但是可以看到消息主体都是可读的文本,很明显,斗鱼这里是自己实现了一个内部协议格式。
开头 12 个字节暂时不清楚什么含义,然后紧跟着一段键值对数据,使用 @= 连接键和值,使用 / 分割,最后跟上 /\x00。
多查看几个直播间以后,对于开头的 12 个字节,我们不难分析出前四个字节和消息长度有关,使用 Little Endian,中间四个字节和前四个字节相同,而最后四个字节是固定值 0xb1020000。
上面的消息长度是 287 个字节,而 0x0000011b = 283,所以,长度编码的值实际上是整个消息的长度减去 4。
这里设计其实挺奇怪的,长度信息比实际长度少了四个字节,同时开头又多了四个字节的冗余数据,怎么看怎么都像是设计失误,第二个四字节是多余的。
Tip: 对于一个协议来说,作为外部人员,我们是永远无法弄清楚有些问题的成因的。可能这四个字节有其他用处,可能就是设计失误,也有可能一开始没有这四个字节,后来因为 bug 不小心加上了,然后为了后向兼容,就一直带上了。
rt 很容易发现是一个秒级时间戳,现在唯一剩下的就是 vk 这个字段。我们可以通过修改字段值的方式来大致判断字段的作用和重要性。
如果我们原封不动的使用这个请求体(在检查器中右键选择 Copy message... - Copy as hex)请求斗鱼的 WebSocket 服务,会发现一开始是有正常响应的,但是过几分钟后就报错了。
很明显,斗鱼会校验 rt 的值,如果服务器时间和 rt 时间超过一定间隔,那么会返回错误,这是一个很常见的设计。
如果我们修改了一下 vk,也会得到一个错误,这说明 vk 是类似签名的东西,而不是什么信息携带字段,服务端会校验它的有效性。
对于 dfl, ver, aver, dmbt, dmbv 这些字段,我们会发现随便修改都不会影响结果,说明我们的之前的猜测是正确的。
所以,现在剩下的问题就是要搞清楚 vk 的签名规则,这个只能从源码入手。
Chrome 检查器中对于每个网络请求都会显示它的 Initiator,也就是这个请求是什么代码发起的。
通过断点我们可以看出,登录消息就是通过这里发送的,因为 e 是登录的消息体。
现在整个消息体的结构我们都清楚了,我们试着构造消息体请求斗鱼服务器看看能不能得到响应。
获取到字体 ID 和假数据以后,接下来我们要做的就是使用字体渲染一张图片,然后调用 OCR 工具识别图片。
怎样渲染字体到图片呢?这个问题方案有很多,上文中我们利用了浏览器,这里我选择使用 SDL。
图形识别这一块我并没有什么太多经验,但是没关系,感谢开源世界。我们 Google OCR,很容易就会找到一个看起来很厉害的库 tesseract。
项目本身是 C++ 的,我们可以直接用 C++ 调用。但是因为后面我打算使用 Go 写一个完整的关注人数爬虫,所以这里我们使用 Go 来调用。
最后,我们把上面的各个步骤整合一下,使用 Go 来实现一个完整的斗鱼关注人数爬虫,最终的代码在这里 douyu-crawler-demo。
worker 爬取关注人数首先通过 WebSocket 获取字体信息和假数据下载字体到缓存目录中调用 SDL 渲染字体为图片到缓存目录中使用 OCR 识别图片得到映射关系存储映射关系(生产肯定是写入数据库,这里是 demo,我们使用文件)输出结果(同样,生产是写入数据库,这里我们写入到结果文件)
错误处理:爬虫的天然属性就是随时可能无法工作,完善的错误处理和报警机制是必须的。
数据校验:每一步获取到的数据都需要校验有效性,否则很容易在数据库中写入无效数据。
人工干预:有些环节比如 OCR 的准确率是无法做到 100% 的,要考虑到失败的情况,一旦 OCR 识别失败,需要引入人工干预流程。
最后,我们来测试一下我们的程序效果。roomids.txt 中含有 120 个斗鱼主播的 roomID,我们使用爬虫来爬取这 120 个主播的关注人数。
120 个主播,一共花费了 36s,这个速度还是非常理想的,使用 Headless Browser 是不可能有这个速度的。
但是我们会发现,其中有一些失败了,看日志主要是 WebSocket 没有返回值或者超时了,这在爬虫中很正常,直接重试一下就行了。
讲完了进攻,现在我们来看看如果我们站在防守方,需要使用这种技巧来反爬,该怎么做?
字体反爬的核心是随机生成一个映射,根据映射生成字体,然后返回字体和假数据给到前端。
这个时候我们就需要了解一下字体的文件格式了。常见的字体格式有 ttf, otf, woff。其中 woff 是一个包装格式,里面的字体不是 ttf 就是 otf 的,所以真正的存储格式只有两种,ttf 和 otf。
这两种格式很明显都是二进制格式,没法直接打开看。但是,幸运的是,字体有一个格式叫做 ttx,是一个 XML 的可读格式。
这里我们使用 fonttools 这个强大的 Python 库来进行后续的操作。
我们先来裁剪字体。安装好 fonttools 以后会默认安装几个工具,其中之一是 pyftsubset,这个工具就可以用来裁剪字体。
上面的 warning 不用介意,运行完毕之后我们得到了 hack.subset.ttf,这个便是裁剪后的字体,只支持渲染 0 ~ 9。
接下来转换字体为可读的 ttx 格式。同样,fonttools 自带了一个工具叫做 ttx,直接使用即可。
我们会发现目录下多了一个 hack.subset.ttx 文件,打开观察一下。
使用上文提到的 HTML 使用 fake.ttf 渲染 0 ~ 9,可以看到,我们成功地制作了一个混淆字体。
genfont.py 是我使用 Python 编写的脚本,可以自动生成任意数量的混淆字体。
