mp3文件的标签和字节存取顺序 下 在看ID3V2 有了字节序有关的铺垫, 解析mp3的id3就简单了。
请注意接下来统一从0开始计数。
首先一个id3v2文件的开头四个字节, 也就是0-3的值固定是:
我们可以用这样的方式判定该文件是否为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代码 import chardetimport sysimport structimport osTITLE = "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 "???" def getLengthStr (buf: bytes ) -> int : binStr = ["{:08b}" .format (b)[1 : ] for b in buf] return int ("0000" + "" .join(binStr), 2 ) 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 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 pos = 10 length = getLength(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]))
因为文本编码过于繁杂, 这个解析工具效果不是特别好, 不过写程序么, 只要能解决绝大多数的问题也就达到了目的……