为了抓取弹幕,你需要知道的一些二进制数据常识
- 2020 年 3 月 30 日
- 笔记

摄影:产品经理
春暖花开
文本不会讲具体某个网站的弹幕抓取方法。而是描述抓取到二进制的弹幕信息以后,如何进行处理。
不少直播网站会使用 websockets 来传输弹幕,当我们使用某种方式抓取到弹幕以后,你看到的弹幕可能是这样的:
b'x00x00x00x1ax00x10x00x01x00x00x00x08x00x00x00x01{"code":0}' b'x00x00x00x14x00x10x00x01x00x00x00x03x00x00x00x01x00x00x19xfd' b'x00x00x01xedx00x10x00x02x00x00x00x05x00x00x00x00xxda|R]kxdc0x10<(}xe9Sx7fxc3x16xfcxa4x9cexd9:xebx1cLIxdax04B!x94x92x06Zx04Bx96txb1x1ax7fx08Yxf65x84xfcxf7xa2xbbx12(m#Xxa1xddx19xcd.xcbxacVxafxdexacxdexaexe2yx1dxafGPxbdx86nxce>xdc\xdd^xdd|x13xe7gxd7xd7x17_xc4xd7xcfx1fxcfn.xc4-x01x04Zx06txd5#XrU^2x04xc1x86xce@x05|.x1bxa2Xxc9gZ`rx08xd4xb8x18x0fx15x00x82Fxaaxfb;?xceCToCpSxc5Sx9eZxbcnxf5xd45k5xf6<mvx13Oxa5nvxb1xe1xe1xc4u2xf0tn2Xxc5Sx82xb3-xdexe2x82xa7r5$xc3fxb7ax86Rx85x0bxb2+pxc9Jxacral[4<xcdx8axe5xfb\xb6[xf7ixedx86;@xf0cxeex9dx98}xf7Gxefxce.fxddxd8xcexc68x0exe0xxdax86xbe;b'xd2xb9x13/x87{5{ox86xc0S;hxf3sx1dx19xefxed$"Gxb4xb2xdbx89xbdix16kxf6ux96xb4x0fx8dxb7xfaXx9dmx9d!x8ax18uxa8xc4x0e]^^x90mx8e0xca1xcapx8cSx82x08xcaxc9!xfdx1bxc7xa7xc5xcbxf0x06Qxb4xa1x0emxfe-Nx11Exf4xffpxfexf2hxe5xcbxe2x89x1cT;z1[]x13xb6ax84x16x89x9dxc4`xf6"nLxa8qx08xd2x0exc6xd7Y"xbdx91b!xc2xea:xc7xe5sxead\xeaxa1x9ax1cxfex84x07gxea^Nxc1xxe1x8dxecx82xedx8d8xb0xdbqxf6xc9xf3xabxcexe0xb7xe7x84x1axbb1x1axecx1d;xa7,+xa3xe5xbaq2xb2x89~xccxa2xebx86xc1xf8x832Tx05x82xbdxb1wmx80*cx08xa4xd6xe2x88Cx85x9fx9e~x05x00x00xffxff)bxe55'
遇到这些二进制数据,如何把它解析为人眼能够看得懂的内容呢?
对于第一个bytes 型的数据,你可能会这样操作:
>>> data = b'x00x00x00x1ax00x10x00x01x00x00x00x08x00x00x00x01{"code":0}' >>> data[16:] b'{"code":0}' >>> data[16:].decode() '{"code":0}'
那第二条数据又应该怎么解析?第三条数据呢?第一条这个16是怎么来的呢?
为了解释这个问题,我们需要知道 Python 的struct 模块。这个模块可以使用Python的 bytes 型数据来表示 C 语言的结构体。
一般通过 websocket 传过来的弹幕,前面会有若干个字节作为头部数据,记录数据包的长度、头部长度、数据包的类型等等信息。
今天我们要作为例子的这个弹幕网站,它的弹幕头部格式如下:
I |
H |
H |
I |
I |
---|---|---|---|---|
包体长度 |
头部长度 |
包体数据类型 |
操作符 |
序列 id |
这个头部对应的结构体为:
struct.Struct('>I2H2I')
其中,>
表示这是一个大端序,二进制高位在左边。I
表示无符号整型数字,占4个字节,H
表示无符号短整型,占用2个字节。2H
是HH
的简化写法,类似的还有3I
表示III
以此类推。
什么叫做大端序和小端序呢?这是由 CPU 架构决定的一种二进制数据储存方式。我们现在的 X86电脑,是小端序。
有一个数字7,它的二进制数据为111只占用3位。但是当我们使用整型的时候,一般会使用4字节,也就是32位二进制位。于是数字7会写为:00000000 00000000 00000000 00000111
。其中最左边的8位是高位。最右边的8位是低位。这就是大端序。
但数字7在我们的电脑上,真正储存为00000111 00000000 00000000 00000000
。最左边是低位,最右边是高位。这就是小端序。
我们可以用struct
模块来测试一下:
>>> struct.pack('>I', 7) b'x00x00x00x07' >>> struct.pack('<I', 7) b'x07x00x00x00'
这里返回的结果是十六进制,x07
表示十六进制的7.
看到这里,可能有同学会问,数字7本来用3位就能表示,为什么要搞个4字节32位?这不是浪费吗?
这是因为,当我们提前定义好数据的类型以后,就可以提前知道数据的长度。例如两个整型数100和67,他们各占用4字节,于是总长度是8字节。现在我把这两个数字都改了,改成3999和6785,两个数字都变大了。但是由于没有超过4字节能表示的最大范围,所以这两个数字占用的空间仍然是8字节。
回到我们开头说的弹幕网站。通过技术手段,我知道了它的头部有5个部分,分别用1个4直接无符号整数、两个无符号短整数、2个无符号整数表示。
所以我们可以提前构造头部的结构体,并知道头部的长度:
>>> head = struct.Struct('>I2H2I') >>> head.size 16
那么,对于第一种情况,我们先截取头部,并把它还原为数字:
>>> data = b'x00x00x00x1ax00x10x00x01x00x00x00x08x00x00x00x01{"code":0}' >>> header = head.unpack_from(data[:head.size]) >>> header (26, 16, 1, 8, 1)
返回的元组中,第一个元素26
表示包体长度为26个字节(包含头部),第三个元素1
表示数据类型为未压缩的版本。所以我们直接获取数据的第16-26字节即可:
>>> data = b'x00x00x00x1ax00x10x00x01x00x00x00x08x00x00x00x01{"code":0}' >>> data[16: 26] b'{"code":0}'
同理。我们来看一下第二段数据:
>>> data = b'x00x00x00x14x00x10x00x01x00x00x00x03x00x00x00x01x00x00x19xfd' >>> header = head.unpack_from(data) >>> header (20, 16, 1, 3, 1) >>> value_bytes = data[16: 20] >>> value_bytes b'x00x00x19xfd' >>> int_value = int.from_bytes(value_bytes, 'big') >>> int_value 6653
对头部解包以后,可以知道整个包体长度是20,那么获取第16到20字节的数据。这个数据是被转为 bytes 型数据的整数,所以需要把它重新转回int 型。由于数据是大端储存,所以代码需要写为int.from_bytes(value_bytes, 'big')
.
这里为什么我知道需要把这个数据转成整数呢?这是因为头部里面第4位数字3表示这条消息是当前视频的热度,就是一个数字。
第三段就留做作业给大家来解决了。给一个提示:解析出头部以后,你会发现头部的第3位对应数字2,表示后面的数据是经过压缩的。你可以使用 Python 的zlib。decompress(data[16: 数据包长度])
对它进行解压缩。解压缩以后,你会惊讶地发现本文是用哪个网站的弹幕数据来进行举例。