mp3文件的标签和字节存取顺序
接下来我们一起来探索如何从一个mp3文件里解析出相应的标签信息, 比如曲目专辑艺术家发行年份等等。
首先一个mp3 文件本身存取了这些信息, 有的在文件的开头, 有的在文件的结尾。
这些信息有个吓人的名称叫id3, 文件开头的叫id3v2, 文件结尾的叫id3v1, 围绕id3也有一些乱七八糟的术语, 咱先不用理会, 两者的区别也非常明显。
v一在mp3文件的最后128字节的位置处, 好处就是所有的信息都是固定的, 从头到尾顺序读取就ok了, 坏处就是承载不了多少信息。
v2好处就是扩展性好, 只要愿意可以把mp3有关的所有信息都塞进里面, 比如曲目的封面和歌词等, 现在这是主流, 坏处就是解析起来比较麻烦而已。
这里我们解析v2版本, v1没什么好说的, 开头的三个固定字符TAG, 紧跟着读取3-32解码字符串, 读取33-62读取解码字符串, 63-92读取解码字符串, 93-96读取解码整数就ok了, 至于第97个字节后面的随意, 我没有看过。
如果你对解析v1没有头绪的话, 看完v2解析原理后手到擒来……
字节序……字节存取顺序
如果我们在内存上保存一个四个字节的32位整数, 可以采取两种策略。
第一种, 按照我们平常的读写习惯从左至右一个个放置, 这就是所谓的大端序, 也叫网络序。
长这样:
bytes([0 0, 0, 100]) |
第二种我们从右至左反着来, 这个叫小端序, 也叫主机序, 长这样。
bytes([100, 0, 0, 0]) |
如果你当前CPU平台的字节序和文件里保存的字节序不一样的话我们需要手动转换, 否则的话解析出来的数据肯定是大错特错了。
具体编写一个简单程序验证一下我们的想法, 首先找个mp3, 用记事本打开看看开头是不是ID3, 如果不是找一个这样的文件, 当然90%应该都是id3开头的。
然后编写如下python脚本
from struct import unpack |
输出如下:
b'\x00\x00\x00\x1f' |
从上面的程序运行结果里可以得出如下结论。
mp3文件里存取数据和python变量存取数据不一样, 他们刚好是反过来的
所以读取数据的时候不注意很容易出现错误, 一个曲目不可能是几亿个字节, 所以错误显而易见, 如果是其他场景呢
我们的python变量使用了小端序, 而mp3采用的是大端序, 我们常见的X86_AMD64架构用的是小端序, ARM不确定, 有的平台可以自行配置。
我们常见的JVM dotnet这些平台基本上采用小端序, 我们每天离不开的TCP数据包采用的是大端序, 只是我们没有意识到他们之间的转换而已。
我们也不用过于纠结这些玩意儿, 在实战中遇到问题的时候知道有这回事就行了, 在上面的程序里我们用两种办法正确解包了一个int类型的数据。
第一个办法是用了struct.unpack方法的格式化说明符> , 第二个办法直接反转数据之后读到的四个字节。
在unpack方法里>采用大端序, <采用小端序, 省略平台默认。
此外如果你手头没有方便的函数, 可以手动反转。
结合上面的程序多看几个文章, 这样我们对这些内容心里有数了……
手工解析二进制
如果手头没有struct这样的模块该如何解析二进制数据呢?
首先我们采用稍微笨拙的办法, 这样我们容易理解程序是怎么运行的, 接下来用二进制运算来提高速度, 我这边翻了一番。
为了方便我们针对的是int类型的数据, 也就是32bit的四个字节无符号整数, 其他类型完全可以举一反三。
首先看打包, 也就是把一个int值转换为四个字节的二进制数据。
# --*-- Encoding: UTF-8 --*-- |
我这边运行良好, 字符串版本28秒, 二进制版本14秒。
那么如何编写一个从字节解包int值的函数呢?
原理和我们从1到10嵌套循环里输出1-100的序列没有太大区别。
import struct |
这里我们可以继续扩展, 比如mp3的标签头里保存了所有标签帧的总长度, 所谓的标签帧就是一个个信息块, 他们紧跟着十个字节的标签头的后面。
不能理解的是这个重要数据用28bit来保存, 更麻烦的是每个字节保存了7bit的有效值, 最高位需要丢弃。
那么我们该怎么获取每个有效的7bit值, 然后生成一个int值呢?
此外我们如何解包一个long或short值, 遇到负数又该如何。
这些都是有趣的底层知识, 未完待续……