Fork me on GitHub

Go语言开发-字符串-例子: M3u2pls

3.7. 例子: M3u2pls

在本节中,我们将简单介绍一个短小但完整的程序,该程序从命令行中读取给定的后缀名为 .m3u的音乐播放列表文件并输出一个等效的.pls播放列表文件。程序中使用了很多strings包中的函数,同时也讲解了一些新的知识。

下面是从.m3u文件中摘抄出的一些内容,中间使用省略号来省略了大部分的歌曲列表。

文件的开头是一个字符串常量#EXTM3U。每一首歌用两行来表示,第一行是#EXTINF,接着是歌曲以秒为单位的持续时间,然后是一个逗号,最后是歌曲名。持续时间为-1意味着歌曲的长度是未知的(或格式是未知的);第二行是歌曲的存储路径。这里我使用开源的、无专利限制的Ogg封装(www.vorbis.com)的音频编码格式,同时使用了Unix风格的路径分隔符。

下面是从.pls文件中摘抄出的一些内容,再次使用了省略号来省略了大部分的歌曲列表。

.pls文件格式比.m3u更详尽一些,文件以字符串常量[ playlist]开始,每一首歌用3个“键-值”条目来分别表示文件名、标题和以秒为单位的持续时间。.pls格式实际上是.ini格式(Wlndows系统初始化文件格式)的一种特殊形式,其中每个键必须唯一,因此我们使用了编号,最后文件以两行元数据结束。

m3u2pls程序(在文件m3u2pls/m3u2pls.go中)在运行时需要在命令行中指定一个后缀名为.m3u的文件,然后它会将一个等效的.pls文件写到os.Stdout(即控制台)。我们可以简单的使用重定向将.pls数据写入到一个实际的文件中。下面是该程序的一种用法。

这里我们告诉程序读取Bowie-Singles.m3u文件中的数据并使用控制台重定向来将.pls格式的数据写入到Bowie-Singles.pls文件中(当然,将其转换为其它的形式也是极好的,这恰恰也是本节中的练习所涉及到的)。接下来我们会给出差不多该程序的全部代码,除了import语句。

main()函数首先会检查命令行是否指定了后缀为. m3u的文件。strings.HasSuffix()函数接受两个字符串作为参数并在第一个字符串以第二个字符串结尾时返回true。如果. m3u文件没有被指定就输出用法信息并终止程序运行。filepath.Base()函数返回给定路径的基址名(即文件名),os.Exit()函数会在程序终止时做垃圾回收工作,例如,停止所有的goroutine并关闭所有打开的文件,最后将其参数返回给操作系统。

如果已经指定了.m3u文件,我们就尝试使用ioutil.ReadFile()函数读取整个文件,该函数返回文件的所有字节([]byte类型)和一个error值,如果在读取文件的过程中没错误发生的话,error的值为nil,如果出现了错误(如文件不存在或不可读),我们就用log.Fatal()函数将错误信息输出到控制台(实际上是输出到os.Stderr),最后以退出代码1终止程序运行。

如果文件读取成功,我们将其原始字节转换成字符串,这里假定这些字节都可表示成一个7位ASCII码或UTF-8编码的Unicode字符,然后立即将该字符串作为参数传递给自定义的readM3uPlaylist()函数进行解析并返回一个Song切片([]Song类型),然后我们用自定义的writePlsPlaylist()函数将这些歌曲输出。

这里我们自定义了一个Song类型的结构体来方便的存储每首歌曲的文件格式无关的信息。

函数以.m3u文件的全部内容作为参数并返回解析出来的包含所有歌曲信息的切片。该函数首先声明了一个空的名字为song的Song类型的变量,由于Go语言总是将变量初始化为其零值,所以song的初始内容是两个空字符串和一个值为0的Song.Seconds。

该函数的核心是一个for … range循环。strings.Split()函数用于将.m3u文件中的全部数据拆分成单独的行,然后使用用for循环遍历这些行。如果行的内容为空或是首行(如以字符串常量“#EXTM3U”开头的行),就会执行continue语句,这样就可以跳过当前循环并强制开始下一次循环,或者是在没有其它行的时候结束循环。

如果行以字符串常量”#EXTINF:”开头,就将该行传递给parseExtinfLine()函数进行解析,该函数返回一个字符串和一个int类型的值,并立即将其赋值给当前song的Song.Title 和Song.Seconds字段,否则,它就被假定保存的是当前song的文件名(包括路径)。

我们调用strings.Map()函数和自定义的mapPlatformDirSeparator()函数来将目录分隔符转换成程序所运行平台的本地格式,并将由此产生的结果保存到当前song的Song.Filename字段中。strings.Map()函数接受一个签名为func(rune)rune的映射函数和一个字符串,对于字符串中的每个字符都调用该映射函数并将其用映射函数返回的字符替换掉。按照Go语言的惯例,rune类型的字符的值是该字符的Unicode码点。

如果当前song的文件名和标题都不为空且歌曲的持续时间不为0,就将当前的song追加到songs返回值中([]Song类型),同时通过将一个空的Song赋值给当前song将其设置为零值(两个空字符串和一个0)。

这个函数用于解析形如#EXTINF:duration,title这样的行,其中duration应是-1或大于0的 整数。

strings.IndexAny()函数用于查找第一个数字或负号的位置,索引位置为-1表示没有找到;而其它值表示作为第二个参数给定的字符串中的任何一个字符第一次出现的索引位置,这样变量i就保存了duration的第一个数字(或“-”号)的索引位置。

一旦我们知道了数字从哪里开始,我们就从数字开始的地方将字符串进行切分,这样就可以有效的把开头处的”#EXTINF:”丢弃掉,所以现在这行字符串就变成了形如duration,title这样的行。第二个if语句使用了strings.Index()函数来得到该行中“,”第一次出现的索引位置,如果不存在则返回-1。

title是从逗号开始到当前行结束之间的文本,要从逗号后面切分,我们需要知道逗号的索引位置(j)并需要与逗号占用的字节数(len(separator)相加。当然,我们知道逗号是一个7位ASCII码字符,所以其长度是1,但我们这里使用的方法对所有Unicode字符都有效,而无论使用了多少个字节来表示该字符。

duration是一个从行开始处到第j个字节(逗号所在的地方)但不包括该字节的数,我们使用strconv.Atoi()函数将其转换成int类型的值,如果转换失败,我们就简单地将duration设置-1,它是一个可接受的“未知的duration”值,同时将这个问题记录到日志以便用户能注意到它。

对于文件名中的每一个字符,都会使用strings.Map()函数(在readM3uPlaylist()函数中)调用该函数,它将文件名中的所有目录分隔符替换为平台相关的目录分隔符,其他字符保持原样。

像大多数跨平台编程语言和库一样,对于所有平台包括Windows,Go语言内部都是使用Unix风格的目录分隔符。然而,对于用户可见的输出和人类可读的文件数据,我们更倾向于使用平台相关的目录分隔符。要做到这一点,我们可以使用filepath.Separator常量,在类Unix系统上它的值是“/”,在Windows平台上则是“\”。

在本例中我们不知道使用的目录分隔符是斜杠还是或反斜杠,所以我们不得不对这两种都做了处理。但是如果我们确切地知道一个路径使用的斜杠,我们就可以对它调用filepath.FromSlash()函数:在类Unix系统上原样返回,但是在Windows系统上被替换为反斜杠。

该函数将歌曲数据以.pls格式输出到os.Stdout(即控制台),但是我们可以使用文件重定向将数据输出到文件中。该函数首先输出节头”[ playlist]”,接着输出每首歌曲的文件名、标题和以秒为单位的持续时间,因为每个键必须是唯一的,所以在每项后追加一个数字,该数字从1开始,最后输出两项元数据。


目录


作者:Johnson
原创文章,版权所有,转载请保留原文链接。

发表评论

电子邮件地址不会被公开。 必填项已用*标注