0%

mp3标签解析和字节序 (下)

mp3文件的标签和字节存取顺序 下

在看ID3V2

有了字节序有关的铺垫, 解析mp3的id3就简单了。

请注意接下来统一从0开始计数。

首先一个id3v2文件的开头四个字节, 也就是0-3的值固定是:

b"ID3\x03"

我们可以用这样的方式判定该文件是否为mp3文件。

文件的6-9保存了mp3所有信息块的总长度, 不包括文件开头的10个字节。

长度占用28bit; 丢弃每个字节的最高位, 大端序。

从第10个字节开始顺序保存每个信息块。

比如第一个信息块从第10个字节开始, 名称站四个字节, 长度站四个字节32bit, 这是和总长度不一样的点。

接下来是两个字节的其他信息, 这里不做解释。 十个字节结束后紧跟着信息块的真正的值。

比如:

  • 10-13 | TIT2(曲目)

  • 14-17 | b\x00\x00\x00\x20长度

  • 20-39 | b…..(真正的值)

    如此类推, 信息块的前面十个字节固定长度, 但是后面的值就不固定了, 不过必须至少占用一个字节。

    所以要想拿到后面的信息块, 那么首先要拿到前面的信息块的长度才行。

    简单介绍一下id3v2的术语吧。

    开头的十个字节称之为标签头,

    接下来是一个个的标签帧, 但是称之为信息块更易懂。

    帧头占用10个字节, 最前面的4字节是名称, 接下来四字节是长度, 从第10个字节开始就是帧体。

    我们首先获取值, 而后在解码。

    id3v2没有采用统一的字符串编码, 我这边大概有140个mp3文件样本, 主流是Unicode和gbk, 此外还有少量的其他编码, 比如UTF-8; ISO-8859-1等等, 可以称得上是五花八门乱七八糟。

思路

知道了id3后我们大概的思路是这样。

1.判断当前文件是否是mp3id3v2,
2.获取28bit的标签体长度,
3.获取第一个标签帧的名称和长度, 解码帧体里保存的实际值, 定位下一个标签帧的开头,
4.重复第三步, 处理完所有的帧为止……

知道怎么搞, 接下来直接编写一个程序实现就好了。

完整python代码

# --*-- encoding: UTF-8 --*--
import chardet
import sys
import struct
import os

# 定义标题, 艺术家 专辑和发行年份
TITLE = "TIT2"
ARTIST = "TPE1"
ALBUM = "TALB"
YEAR = "TYER"

# 字符串解码
def decode(buf: bytes) -> str:
charset = str(chardet.detect(buf)["encoding"])
try:
return buf.decode(charset)
except:
return "???"

# 从二进制序列里提取int值, 丢弃每个字节的最高位
# 效率很低, 这里主要用于调试
def getLengthStr(buf: bytes) -> int:
binStr = ["{:08b}".format(b)[1: ] for b in buf]
return int("0000" + "".join(binStr), 2)


# 从二进制序列里提取int值, 丢弃每个字节的最高位, 可以配合着getLengthStr函数一起看
def getLength(buf: bytes) -> int:
t = 0
i = len(buf)
n = 0
for b in buf:
i -= 1
n = 128 ** i
t += b * n

return t


# 解析id3v2
def readId3(path: str) -> dict:
tag = {TITLE: "*", ARTIST: "*", ALBUM: "*", YEAR: "*"}
buffer: bytes
try:
fp = open(path, "rb")
buffer = fp.read()
fp.close()
if buffer[: 3].decode() != "ID3":
raise Exception("NotSupportFile")

except:
return tag

# 定位第一个id3信息块
pos = 10
# 获取所有信息块的大小, 不包括开头的10字节的标签头, id3v2标准规定大小为28bit整数, 占用每个字节的低7bit
# 所以需要丢弃最高位
length = getLength(buffer[6: 10])
# 验证我们的二进制运算对不对
#assert(length == getLengthStr(buffer[6: 10]))

# 开始处理每个信息块
while pos < length + 10:
# 获取当前信息块大小, 这里不用丢弃最高位
fieldLength: int = struct.unpack(">i", buffer[pos + 4: pos + 8])[0]
if fieldLength >= 1:
# 获取每个信息块的名称大小和实际内容
name: str = decode(buffer[pos: pos + 4])
tagBuf: bytes = buffer[pos + 11: pos + 10 + fieldLength]
value = decode(tagBuf)
# 匹配歌曲的标题 艺术家 专辑 和 年份
if name == TITLE or ARTIST or ALBUM or YEAR:
tag[name] = value

# 定位下一个信息块
pos += fieldLength + 10

return tag


if __name__ == "__main__":
path: str
if len(sys.argv) > 1:
path = sys.argv[1]
else:
path = "D:/music/music1"

for p in os.listdir(path):
fullPath = os.path.join(path, p)
if os.path.isfile(fullPath) and p[-3: ].lower() == "mp3":
tag = readId3(fullPath)
print("{} {} {} {}".format(tag[TITLE], tag[ARTIST], tag[ALBUM], tag[YEAR]))

因为文本编码过于繁杂, 这个解析工具效果不是特别好, 不过写程序么, 只要能解决绝大多数的问题也就达到了目的……