如何解析audio.cdindex

前端开发 2026-07-09

有些音频光盘里有一个名为audio.cdindex的文件,里面记录了艺术家、专辑和曲目清单。 我有一个用来把光盘拷贝到NAS的 Python工具,若在MusicBrainz或 CDDB上找不到光盘的ID,我就尝试解析audio.cdindex来正确命名文件。 幸运的是,该文件是XML,因此 xml.etree.ElementTree 通常能够很好地处理。 但现在我有一张光盘引用了 http://www.musicbrainz.org/dtd/CDInfo.dtd 作为DTD,而这个文件已经不存在。 通常Python库会忽略丢失的DTD,但在这张光盘上,曲目引用了一个实体 ü,因此解析失败。 我尝试用 http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent 的副本替换该DTD,在那里定义了该实体,但似乎这个DTD从未被应用。 以下是我正在使用的audio.cdindex:

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE CDInfo SYSTEM "my_private.dtd">
<CDInfo>
   <Title>Audiophile Pearls Vol. 40</Title>
   <NumTracks>18</NumTracks>

   <IdInfo>
      <DiskId>
         <Id>69Zr3Q3U3DVbQpVr2iLBQ.XtFZY-</Id>
      </DiskId>
   </IdInfo>

   <SingleArtistCD>
      <Artist></Artist>
      <Track Num="1">
         <Name>Do Do Do</Name>
      </Track>
      <Track Num="2">
         <Name>I Fell In Love</Name>
      </Track>
      <Track Num="3">
         <Name>Isla Feliz (feat. Antoine Boyer)</Name>
      </Track>
      <Track Num="4">
         <Name>Dig!</Name>
      </Track>
      <Track Num="5">
         <Name>Sulling</Name>
      </Track>
      <Track Num="6">
         <Name>Press Play</Name>
      </Track>
      <Track Num="7">
         <Name>Washington Heights</Name>
      </Track>
      <Track Num="8">
         <Name>Get Down</Name>
      </Track>
      <Track Num="9">
         <Name>Not Only Human</Name>
      </Track>
      <Track Num="10">
         <Name>Money Matters</Name>
      </Track>
      <Track Num="11">
         <Name>Streik</Name>
      </Track>
      <Track Num="12">
         <Name>Chestnut Tree</Name>
      </Track>
      <Track Num="13">
         <Name>Hydra</Name>
      </Track>
      <Track Num="14">
         <Name>Malambe</Name>
      </Track>
      <Track Num="15">
         <Name>Serenade in D Major Op. 41 I. Entrata. Allegro. Hustedt Speidel</Name>
      </Track>
      <Track Num="16">
         <Name>Concerto No 2 f minor. Allegro vivace. Orchestre de Chambre de Lausanne Zacharias</Name>
      </Track>
      <Track Num="17">
         <Name>Sonate f&uuml;r Klavier und Violoncello No. 1 B-Dur, Op. 45  III. Allegro assai. Grigoryan Petersen</Name>
      </Track>
      <Track Num="18">
         <Name>String Quartet Op. 59, No. 2 - IV. Finale. Presto. Chiaroscuro Quartett</Name>
      </Track>
   </SingleArtistCD>
</CDInfo>

我正在使用的Python代码是下面这个:

# Source - https://stackoverflow.com/q/79937078
# Posted by tommiport5, modified by community. See post 'Timeline' for change history
# Retrieved 2026-05-07, License - CC BY-SA 4.0

import xml.etree.ElementTree as ET
import re
import logging

dtype = re.compile(r"<!DOCTYPE.*musicbrainz.*>")
repl = r'<!DOCTYPE CDInfo SYSTEM "my_private.dtd">'
# uncommenting the next line makes the test work
# repl = r'<!DOCTYPE CDInfo [<!ENTITY uuml   "&#252;">]>'

def clean(xtext):
    dt = dtype.search(xtext)
    if dt:
        return xtext[:dt.start()] + repl + xtext[dt.end():]
    else:
        return xtext

def readAudioindex(dest):
    Tracks = []
    try:        
        with open(dest) as fd:
            txt = fd.read()    
        root = ET.fromstring(clean(txt))
        album = root.find('Title').text
        artist = root.find('.//Artist').text
        for track in root.iter('Track'):
            tt = track.find('Name').text
            tp = tt.find('/')
            if tp >= 0:
                ta = tt[:tp]
                ta = ta.strip()
                tt = tt[tp+1:]
                tt = tt.strip()
            else:
                ta = root.find('.//Artist').text
            num = int(track.get('Num'))
            Tracks.append({'num':num, 'tt':tt, 'ta':ta})
        return (artist, album, Tracks)
    except Exception as e:
        logging.getLogger().error('Exception reading audioindex "{}"'.format(str(e)))
#        shutil.copy(dest, 'faulty_audio.cdindex')
        return None

if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("index")
    parser.add_argument("-d", "--dry", action='store_true')
    args = parser.parse_args()
    txt = ""
    if args.dry:
        with open(args.index) as fd:
            txt = fd.read()
        print(clean(txt))
    else:
        cont = readAudioindex(args.index)
        if cont == None:
            print("not valid")
        else:
            print(f"artist: {cont[0]}, album: {cont[1]}")
            for tr in cont[2]:
                print(f'track {tr["num"]} \"{tr["tt"]}\"')

解决方案

确保已移除BOM,并替换实体 [根据 @Furas的注释更新]:

import xml.etree.ElementTree as ET
import html

def clean_xml(xml_string):
    cleaned = html.unescape(xml_string)
    return cleaned.strip()

def parse_cd_xml(xml_string):
    try:
        root = ET.fromstring(clean_xml(xml_string))
        print(f"Album: {root.findtext('Title')}")
        print("-" * 30)
        for track in root.findall(".//Track"):
            print(f"Track {track.get('Num')}: {track.findtext('Name')}")
    except ET.ParseError as e:
        print(f"XML Parsing Error: {e}")
    except Exception as e:
        print(f"Unexpected error: {e}")

def main():
    try:
        with open("cdinfo.xml", "r", encoding="utf-8-sig") as f:
            parse_cd_xml(f.read())
    except FileNotFoundError:
        print("File 'cdinfo.xml' not found.")

if __name__ == "__main__":
    main()

输出:

Album: Audiophile Pearls Vol. 40
------------------------------
Track 1: Do Do Do
Track 2: I Fell In Love
Track 3: Isla Feliz (feat. Antoine Boyer)
Track 4: Dig!
Track 5: Sulling
Track 6: Press Play
Track 7: Washington Heights
Track 8: Get Down
Track 9: Not Only Human
Track 10: Money Matters
Track 11: Streik
Track 12: Chestnut Tree
Track 13: Hydra
Track 14: Malambe
Track 15: Serenade in D Major Op. 41 I. Entrata. Allegro. Hustedt Speidel
Track 16: Concerto No 2 f minor. Allegro vivace. Orchestre de Chambre de Lausanne Zacharias
Track 17: Sonate für Klavier und Violoncello No. 1 B-Dur, Op. 45  III. Allegro assai. Grigoryan Petersen
Track 18: String Quartet Op. 59, No. 2 - IV. Finale. Presto. Chiaroscuro Quartett
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章