replica
初衷是想要整理iphone中的音乐。IOS(我自己的手机还是IOS8.3,新版本的系统可能有变化了)自带的音乐软件中所有音乐文件都存放在/var/mobile/Media/iTunes_Control里面。不过很令人抓狂的是首先这个目录被分隔成了从F00-Fxx的多个子目录,我的手机上总共到F49,mp3文件都放在这些子目录中。其次,mp3文件名全部都被点窜了,是看起来毫无规律的随机四位大写字母。每隔一段时间我都想从手机中把音乐备份出来然后放到电脑上,但是不知道文件名的话维护起来很麻烦。所以需要一个小程序来根据mp3的信息改变文件名。
程序的目标非常简单,就是首先提取MP3文件中的信息,主要提取其歌名和演唱者两个字段,然后把这个文件重命名成 “歌名 - 演唱者.mp3”这种格式。
■ 关于mp3信息的提取方法
mp3除了音频信息的部分外,在文件的某些地方还会存放有关于这个音频的一些基本信息比如作者,创作时间,专辑名,曲序号,专辑图片等等。这些信息被统称为ID3标签。ID3标签被分成两代,第一代ID3v1只存储一些很简单的信息,占用文件末尾的128个字节(下面的直接读文件的就是默认是ID3v1的情况)。而ID3v2位于文件的开头,并且包含很全的信息比如加上一张专辑插图。
网上搜到最多的使用ID3或者类似的一些模块进行提取,也有很多人根据mp3的文件结构特征直接对文件进行读取操作然后过滤出信息。因为我手上的资源很杂而且试了一下第二种方法发现很多文件提取信息都不全或者错误,所以还是用了第一种方法。下面这个是从网上摘来的,某种比较简洁的第二种方法示例:这个函数读取一个文件,然后从文件内容的特定位置解析出tag信息并返回一个字典。
def getID3(filename): fp = open(filename, 'r') fp.seek(-128, 2) fp.read(3) # TAG iniziale title = fp.read(30) artist = fp.read(30) album = fp.read(30) anno = fp.read(4) comment = fp.read(28) fp.close() return {'title':title, 'artist':artist, 'album':album, 'anno':anno}
和网上很多人用ID3或者eyed3之类的模块不同,我用了个相对比较冷门的replica,他一共就只有三个模块文件cli.py , tagger.py , cloner.py。我们用到的主要是tagger。基本用法如下:
from replica import tagger tags = tagger.get_tags("PATH.mp3") #获取一个mp3文件的ID3标签信息 tagger.set_tags(tags,"ANOTHER.mp3")
get_tags方法得到的是一个mutagen的MP3对象(replica是基于mutagen的,mutagen是更基本一些的音频处理模块)。这个对象所属的类应该实现了__getattr__方法,所以你可以像一个字典一样去访问这个对象中的一些键值。而如果打印这个这个对象看到的就是一个字典:
for k,v in tags.items(): if k == u"APIC:": #跳过U'APIC:'这个键是因为这个键的值是专辑图片,如果用字符来表示的话太大了这里显示不下 continue
print k,v print repr(k),repr(v) ###打印结果###
TDRC 2011
TIT2 ゆりゆららららゆるゆり大事件
ゆりゆららららゆるゆり 大事件
TRCK 1/4
TPE1 七森中☆ごらく部
TALB ゆりゆららららゆるゆり大事件
TSRC JPPC01101395
TCON Anime
TXXX:DISCID 28036204
###repr结果### 'TDRC' TDRC(encoding=<Encoding.LATIN1: 0>, text=[u'2011'])
'TIT2' TIT2(encoding=<Encoding.UTF16: 1>, text=[u'u3086u308au3086u3089u3089u3089u3089u3086u308bu3086u308au5927u4e8bu4ef6'])
u'USLT::eng' USLT(encoding=<Encoding.UTF16: 1>, lang='eng', desc=u'', text=u'u3086u308au3086u3089u3089u3089u3089u3086u308bu3086u308a u3086u308au3086u3089u3089u3089u3089u3086u308bu3086u308a u3086u308au3086u3089u3089u3089u3089u3086u308bu3086u308a u5927u4e8bu4ef6 u3066u3093u3066u3053u307eu3044u306eu4ecau65e5u660eu65e5u5927u7206u767a u305du3093u3067u8eabu9577u4f38u3073u306au3044u3084u3042u3042u3069u3046u3057u3088 u7518u3044u3082u306eu98dfu3079u3059u304du30c6u30fcu30deu30d0u30fcu30af u307bu3044u3058u3083u96a0u305bu3088u4e59u5973u3067u3069u3046u3058u3083u308d u90e8u6d3bu52d5u672cu756a u3057u3081u3057u3081u7121u9045u523b u672cu696du5b78u696du306au306bu305du308c u305du3093u306au306eu305cu3093u305cu3093u305cu3093u305cu3093u305cu3093u305cu3093u98dfu3079u308cu306au3044 u685cu54b2u304d uff08u685cu54b2u304duff09 u685cu6563u308a uff08u685cu6563u308auff09 u660eu65e5u3082u3044u3044u65e5u3068u6b4cu3046u3088 u541bu304cu597du304d uff08u541bu304cu597du304duff09 u541bu304cu3044u3044 uff08u541bu304cu3044u3044uff09 u660eu65e5u3082u3044u305fu3044u3068u601du3046u3088 u6700u7d42u624bu6bb5u3067u5c40u7720u308a u3086u308au3086u3089u3089u3089u3089u3086u308bu3086u308a u3086u308au3086u3089u3089u3089u3089u3086u308bu3086u308a u3086u308au3086u3089u3089u3089u3089u3086u308bu3086u308a u5927u4e8bu4ef6(u3060u3044u3058u3051u3093) u3044u308du306fu306bu307bu3078u3068u3067u304au307fu304fu3058Get youuff01u3042u308au3089u3073u3085u30fc u306bu3083u3093uff01u306bu3083u3093uff01u8001u306bu3083u3093uff01u82e5u306bu3083u3093uff01u7537u306bu3083uff01u5973u3093uff01 u5fc5u6b7bu306bu5bc6u66f8u3092u767au5c04u3067u5fc5u4e2du6388u696du4e2d u3067u3082u306du3042u3089u3089u3089u30c1u30e7u30fcu30afu304cu30dfu30b5u30a4u30eb u864eu7a74u306bu5165u3089u306au3044 u96e8u306au3089u30cfu30ecu30ebu30e4 u30abu30e9u30aau30b1u5272u9ad8u306au306bu305du308c u305du3093u306au306eu305cu3063u305fu3044u305cu3063u305fu3044u305cu3063u305fu3044u98df()u3079u308cu306au3044uff01uff01 u685cu54b2u304d uff08u685cu54b2u304duff09 u685cu6563u308a uff08u685cu6563u308auff09 u660eu65e5u3082u3044u3044u65e5u3068u6b4cu3046u3088 u541bu304cu597du304d uff08u541bu304cu597du304duff09 u541bu304cu3044u3044 uff08u541bu304cu3044u3044uff09 u660eu65e5u3082u3044u305fu3044u3068u601du3046u3088 u8fd1u6240u306eu30efu30f3u30b3u3068u683cu95d8 u3086u308au3086u3089u3089u3089u3089u3086u308bu3086u308a u3086u308au3086u3089u3089u3089u3089u3086u308bu3086u308a u3086u308au3086u3089u3089u3089u3089u3086u308bu3086u308a u5927u4e8bu4ef6 u30c1u30e3u30a4u30e0u304cu76eeu899au307eu3057 KIRAu2606KIRAu2606TSUN-DELE u751fu30afu30eau30fcu30e0u60d1u661f u306au306bu305du308c u305du308cu306au3089u307bu3093u3068u306bu307bu3093u3068u306bu307bu3093u3068u306bu3084u3081u308cu306au3044uff01u3084u3081u308cu306au3044uff01uff01 u685cu54b2u304d uff08u685cu54b2u304duff09 u685cu6563u308a uff08u685cu6563u308auff09 u660eu65e5u3082u3044u3044u65e5u3068u6b4cu3046u3088 u541bu304cu597du304d uff08u541bu304cu597du304duff09 u541bu304cu3044u3044 uff08u541bu304cu3044u3044uff09 u660eu65e5u3082u3044u305fu3044u3068u601du3046u3088 u6700u7d42u624bu6bb5(u3067u5c40u7720u308a u8fd1u6240u306eu30efu30f3u30b3u3068u683cu95d8 u9769u547du8d77u3053u3057u3066u5352u696d u3086u308au3086u3089u3089u3089u3089u3086u308bu3086u308a u3086u308au3086u3089u3089u3089u3089u3086u308bu3086u308a u3086u308au3086u3089u3089u3089u3089u3086u308bu3086u308a u5927u4e8bu4ef6 ')
'TRCK' TRCK(encoding=<Encoding.LATIN1: 0>, text=[u'1/4'])
'TPE1' TPE1(encoding=<Encoding.UTF16: 1>, text=[u'u4e03u68eeu4e2du2606u3054u3089u304fu90e8'])
'TALB' TALB(encoding=<Encoding.UTF16: 1>, text=[u'u3086u308au3086u3089u3089u3089u3089u3086u308bu3086u308au5927u4e8bu4ef6'])
'TSRC' TSRC(encoding=<Encoding.UTF16: 1>, text=[u'JPPC01101395'])
'TCON' TCON(encoding=<Encoding.UTF16: 1>, text=[u'Anime'])
u'TXXX:DISCID' TXXX(encoding=<Encoding.UTF16: 1>, desc=u'DISCID', text=[u'28036204'])
字典本身打印出来是这样:
{u'APIC:':'关于图片信息的内容','TSRC': TSRC(encoding=<Encoding.UTF16: 1>, text=[u'JPPC01101395']), 'TCON': TCON(encoding=<Encoding.UTF16: 1>, text=[u'Anime']), u'TXXX:DISCID': TXXX(encoding=<Encoding.UTF16: 1>, desc=u'DISCID', text=[u'28036204']),xxxx还有一些,意思一下。。}
结合各个键的值大概就可以猜出来这个键是什么意思了。对于没有设置某个标签信息的文件而言,它的tags对象中就不会有相关的键。值其实是一些对象,比如标题标签的键是TIT2,值就是一个TIT2对象,其有两个关键的属性,分别是TIT2.encoding和TIT2.text分别指出了显示标题时用的编码格式和标题文本组成的单个元素的列表。
这些对象的全图鉴可以参见mutagen/id3/_frames.py的源码。只是关于如何改变或创建这些对象,改变一个既有的MP3的标签信息这一方面还有待研究(其实是mutagen的内容了)
其实就论replica这个模块的用法的话就是以上了,然而我在之后的编写过程中又遇到了各种各样的坑,比如编码问题,windows系统对于文件名的要求等等,所以打算继续写下去。
■ 关于文本信息的编码
在tag信息的对象中,大多承载文字信息的对象都有encoding和text两个属性,且text属性中是一个单unicode元素的列表。其实这两个属性联合起来就表示了这个unicode应该用哪种编码进行encode才能成为原本的信息。
首先来看encoding这属性。这个属性其实是维护了一个mutagen中的Encoding对象,这个对象可以在源码中看到(位于mutagen/id3/_specs.py),把四种编码分别用了一个数字表示,在知道了对应关系之后我们可以在自己的脚本里添加一个同样的字典使得使用更加方便:
ENCODING = {0: "latin1", 1: "utf16", 2: "utf16be", 3: "utf8"}
因为从相关对象中取到的属性本质就是一个数,比如TIT2.Encoding其实就是1.
在四种编码格式中,比较特殊的是latin1这种格式。latin1就是ISO-8859-1,之前在en/decode以及print探索那篇文章中也提到过,被latin1 decode出来的unicode只能被latin1 encode成str,并且encode得到的东西的编码格式和最原先的是一样的。
■ windows中文件名的规则
windows中的文件名不能含有字符 /:*?"<>| 中的任意一个。如果含有这些错误的话在运用os.rename或者其他类似的重命名手段的时候会报错WindowsError123。另一方面,如果同目录下已经有相同名字文件存在时重命名会报错WindowsError183。
一般而言,对windows上的文件进行命名的时候我们可以直接用unicode类型的字符串(如果愿意,也可以用gb这个系列的编码格式的字符串进行命名,不过能用unicode的情况下应该尽量用通用性更加好的unicode)。但是就这个程序而言,我们碰到了很多由latin1编码格式得到的unicode,很遗憾在处理如果用这些unicode直接去命名文件,会出现乱码。目前我能想到的解决办法是在进行命名之前进行一个判断,如果这个文本信息的编码格式是latin1的,那么在将其输出成文件名的一部分之前先把它encode("latin1"),如果不是,那么可以直接把它作为文件名的一部分去rename。
■ 关于其他一些小改善
至此整个小程序基本功能已经实现了,接下来就是一些业务逻辑的改善了。比如tag信息不全时怎么办,重命名失败时怎么办,增加处理进度提示,对于目录中非mp3文件的处理等等。最后整个脚本如下:
#!/usr/bin/env python # coding=utf8 import os import sys import logging import re from replica import tagger reload(sys) sys.setdefaultencoding("gb18030") ENCODING = {0: "latin1", 1: "utf16", 2: "utf16be", 3: "utf8"} logging.basicConfig(filename="mp3.log", level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s ") def modify_textual(string): return re.sub(u'[\/:*?"<>|]', " ", string) def process_one_song(songName): mp3 = tagger.get_tags(u"{songname}".format(songname=songName)) try: artist = mp3.get("TPE1").text[0] enco_form = ENCODING.get(mp3.get("TPE1").encoding) if enco_form == "latin1": artist = artist.encode("latin1") except AttributeError as e: artist = u"未知艺术家" try: name = mp3.get("TIT2").text[0] enco_form = ENCODING.get(mp3.get("TIT2").encoding) if enco_form == "latin1": name = name.encode("latin1") except AttributeError as e: name = u"未知曲名" name = modify_textual(name) artist = modify_textual(artist) logging.info(u"{songname}".format(songname=songName) + u"{filename}.mp3".format( filename=os.path.dirname(songName) + os.sep.decode("utf8") + name + u"-" + artist)) try: os.rename(u"{songname}".format(songname=songName), u"{filename}.mp3".format( filename=os.path.dirname(songName) + os.sep.decode("utf8") + name + u"-" + artist)) except WindowsError as windowserror: logging.error( "error processing {name} for windows error [{error}]".format(name=songName, error=str(windowserror))) def main(): try: rootpath = sys.argv[1] except IndexError as e: rootpath = 'mp3' if not os.path.isdir(rootpath): print u"输入目录不存在" sys.exit(1) count = 0 total = 0 for root, dir, files in os.walk(rootpath): total += len(files) for root, dir, files in os.walk(rootpath): for file in files: filename = os.path.join(root.decode("gb18030"), file.decode("gb18030")) if os.path.splitext(file)[1] != '.mp3': os.remove(filename) continue count += 1 print u"正在处理{percent:.0f}%的歌".format(percent=float(count) / total * 100) process_one_song(filename)