ID3v2 タグを自力で読んでみる
ID3 タグを扱うライブラリはいくつかあるけれども、それらを使わずに ID3v2 タグを読んでみた。
パソコンさわりはじめた 10 年前の自分の感覚だと、 ID3 を読むのはえらい人が作ったツールをつかって行うものだ、といったところ。けれども、5 年前ならがんばれば自力で読める、今なら数時間、数百行の Python コードでさくっと読める、と変わってきた。成長できている、のかな?
とりあえず対象バージョンは id3v2.4.0 で。タイトルとかちょっと覗き見できる程度の駄スクリプト、役には立たない。でも ID3 タグについて詳しくなったのはよかったと思う。The ID3v2 documents をつたない英語力でゆっくり読みつつ作成。
実用的なスクリプトにするには仕様から外れているファイルをどう読むか、こまかく定めていく必要がありそう。手元のファイルにも、 Shift-JIS 、 '\x00' 終端していないテキスト入りの ID3v2.3.0 をいくつか発見した(2/12追記、 v2.3.0 では '\x00' 終端する必要はない)。作成されたハード、ソフトしだいで独自仕様はたくさんありそう。
(2/18追記、フレームのサイズの読み込み処理などに誤りあり。数値を記録する箇所全般では Synchsafe 整数値が用いられているので struct.unpack('I', '\x00\x00\x00\x10') のような整数扱いをするのは間違い。)
# coding: utf-8 import struct class ID3Header(object): def __init__( self, raw_data, identifier, mejor_version, revision_number, flags, size): self.raw_data = raw_data self.identifier = identifier self.mejor_version = mejor_version self.revision_number = revision_number self.flags = flags self.size = size @classmethod def parse(cls, raw_data): temp = struct.unpack('>3s2BB4B', raw_data) identifier = temp[0] mejor_version = temp[1] revision_number = temp[2] flags = temp[3] size = 0 for i in temp[4:8]: size *= 128 size += i return cls( raw_data, identifier, mejor_version, revision_number, flags, size) @property def unsynchronisation(self): return bool(self.flags & 0b01000000) @property def extended_header(self): return bool(self.flags & 0b00100000) @property def experimental_indicator(self): return bool(self.flags & 0b00010000) class ID3ExtendedHeader(object): def __init__(self, raw_data, size, flags, size_of_padding): self.raw_data = raw_data self.size = size self.flags = flags self.size_of_padding = size_of_padding @classmethod def parse(cls, raw_data): temp = struct.unpack('>I2BI', raw_data) size = temp[0] flags = temp[1:3] size_of_padding = temp[3] return cls(raw_data, size, flags, size_of_padding) class ID3Frame(object): codes = { '\x00': 'cp932', #'\x00': 'latin_1', '\x01': 'utf_16', '\x02': 'utf_16be', '\x03': 'utf_8'} tarminater = { '\x00': '\x00', '\x01': '\x00\x00', '\x02': '\x00\x00', '\x03': '\x00'} def __init__(self, raw_data, id, size, flags, data): self.raw_data = raw_data self.id = id self.size = size self.flags = flags self.data = data @classmethod def parse(cls, raw_data): frames = [] pos = 0 end = len(raw_data) raw_frame_header_size = 10 while pos < end: frame_header_end = pos + raw_frame_header_size raw_frame_header = raw_data[pos:frame_header_end] temp = struct.unpack('>4sI2B', raw_frame_header) id = temp[0] size = temp[1] flags = temp[2:4] if id == '\x00\x00\x00\x00': break frame_end = pos + size + 10 data = raw_data[frame_header_end:frame_end] frames.append( cls(raw_data[pos:frame_end], id, size, flags, data)) pos = frame_end return frames def __str__(self): return str(self.__unicode__()) def __unicode__(self): if self.id[0] == 'T' and self.id[0:4] != 'TXXX': encode = self.codes[self.data[0]] s = self.data[1:].split(self.tarminater[self.data[0]], 1)[0] return u'%s: %s' % ( self.id, s.decode(encode, 'ignore')) else: return u'%s: %r' % (self.id, self.data) class ID3Tag(object): version = u'ID3v2.4.0' def __init__(self, raw_data): start = pos = raw_data.find('ID3') raw_header_size = 10 raw_header = raw_data[pos:pos+raw_header_size] self.header = ID3Header.parse(raw_header) pos += raw_header_size if self.header.extended_header: raw_extended_header_size = 10 raw_extended_header = rawdata[pos:pos+raw_extended_header] self.extended_header = ID3ExtendedHeader.parse( raw_extended_header) pos += raw_extended_header_size raw_frames = raw_data[pos:start+self.header.size] self.frames = ID3Frame.parse(raw_frames) @classmethod def frompath(cls, path): with open(path, 'rb') as f: rawdata = f.read() return cls(rawdata) def main(): import sys for i, path in enumerate(sys.argv[1:]): if i: print u'----------------' print u'path: %s' % path id3 = ID3Tag.frompath(path) print u'tag version: %s' % id3.header.mejor_version print for frame in id3.frames: print frame if __name__ == '__main__': main()