Suchen nach Dateiinformationen

(Auszug aus "Python & XML" von Christopher A. Jones & Fred L. Drake, Jr.)

In diesem Abschnitt erstellen wir ein Skript für die Indexierung von Dateien, das ein XML-Dokument generieren kann, welches Ihr gesamtes Dateisystem oder einen speziellen Teil davon darstellt. Dateien in XML zu indexieren ist eine mächtige Art, Informationen zu verwalten oder umfangreiche Operationen auf speziellen Dateigruppen auf einer Platte durchzuführen. Mit Python können Sie sehr einfach eine XML-erzeugende Indexierungsroutine erstellen. Das Programm index.py im Beispiel index.py beginnt in einem beliebigen Verzeichnis Ihrer Wahl und erzeugt ein Element für alle Dateien und Verzeichnisse, die unterhalb des Startpunktes vorkommen. Wenn wir den Index mit den Dateiinformationen einmal haben, sehen wir uns an, wie man diese Informationen mit SAX durchsucht, um die Dateiliste nach den Kriterien zu filtern, die uns dann gerade interessieren.

Erzeugen des Indexgenerators

Der Hauptteil dieser Routine funktioniert so, daß er einfach jede Datei in einem Startverzeichnis prüft und sich dann rekursiv für jedes Unterverzeichnis aufruft, das er im Startverzeichnis findet. Durch die Rekursion kann ein gesamtes Dateisystem indexiert werden, wenn Sie das wollen. Unter Unix verrichtet das Programm eine Menge Arbeit, da es für jede Datei eine Inhaltsprüfung mit Hilfe eines popen-Aufrufs des file-Befehls durchführt. (Das könnte man effizienter gestalten, indem man find seltener aufruft und von ihm verlangt, mehr als eine Datei pro Aufruf zu verarbeiten, aber das ist nicht Thema dieses Buches.) Eine der Schlüsselmethoden dieser Klasse ist indexDirectoryFiles:

def indexDirectoryFiles(self, dir):     
     """Indexiert ein Verzeichnis und erzeugt eine XML-Ausgabedatei."""
     # Bereite XML-Ausgabedatei vor
     self.__fd = open(self.outputFile, "w")
     self.__fd.write('<?xml version="1.0" encoding="' + XML_ENC + '"?>\n')
     self.__fd.write("<IndexedFiles>\n")

     # Erledige das eigentliche Indexieren
     self.__indexDir(dir)

     # Beende und schließe XML-Datei
     self.__fd.write("</IndexedFiles>\n")
     self.__fd.close()

Eine XML-Datei wird mit dem in outputFile angegebenen Namen erzeugt, und eine XML-Deklaration und ein Wurzelelement werden hinzugefügt. Die Methode index-DirectoryFiles ruft die interne Methode __indexDir auf – diese erledigt die eigentliche Arbeit. Es ist eine rekursive Methode, die in der Dateihierarchie hinabsteigt, während sie Dateien indexiert.

def __indexDir(self, dir):
    """Rekursive Funktion für das eigentliche Indexieren."""    
    # Erzeuge ein indexFile für jede normale Datei, und
    # rufe die Funktion für jedes Verzeichnis erneut auf
    files = listdir(dir)
    for file in files:
      fullname = os.path.join(dir, file)
      st_values = stat(fullname)
        
      # Prüfe, ob es ein Verzeichnis ist
      if S_ISDIR(st_values[0]):
        print file
        # Erzeuge directory-Element
        self.__fd.write("<Directory ")
        self.__fd.write(' name="' + escape(fullname) + '">\n')
        self.__indexDir(fullname)
        self.__fd.write("</Directory>\n")
        
      else:
        # Erzeuge Eintrag für normale Datei
        print dir + file
        lf = IndexFile(fullname, st_values)
        self.__fd.write(lf.getXML())

Die eigentliche Arbeit besteht darin, zwischen Dateien, die Verzeichnisse sind, und normalen Dateien zu unterscheiden. Währenddessen wird entsprechender XML-Code erzeugt und in die Ausgabedatei geschrieben. Nachdem alle Aufrufe von __indexDir schließlich beendet sind, wird die XML-Datei geschlossen.

Nun ist das Programm praktisch fertig. Eine Hilfsfunktion namens escape wird aus dem Modul xml.sax.saxutils importiert, um eine Entity-Ersetzung bei einigen häufigen Zeichen in XML-Zeichendaten vorzunehmen, damit sicher ist, daß sie nicht als Auszeichnungen im resultierenden XML-Code erscheinen.

Erzeugen der IndexFile-Klasse

Die IndexFile-Klasse wird zur Darstellung der Dateiinformationen in XML verwendet. Diese Informationen leiten sich primär aus dem os.stat-Systemaufruf ab. In ihrer Methode __init__ kopiert die Klasse Informationen aus dem Aufruf von stat in ihre Instanzvariablen, wie hier gezeigt wird:

def __init__(self, filename, st_vals):    
    """Extrahiere Attribute des angegebenen stat-Objekts."""
    self.filename = filename
    self.uid = st_vals[4]
    self.gid = st_vals[5]
    self.size = st_vals[6]
    self.accessed = ctime(st_vals[7])
    self.modified = ctime(st_vals[8])
    self.created = ctime(st_vals[9])


    # Prüfen der Dateinamenserweiterung
    self.extension = os.path.splitext(filename)[1]

In dieser Methode werden wichtige Dateiinformationen aus dem Tupel st_vals extrahiert. Es enthält die vom stat-Aufruf zurückgegebenen Dateiinformationen. Die Methode _ _init _ _ probiert wenn möglich auch eine Dateinamenserweiterung aus, indem auf das Zeichen » . « getestet wird. Wenn Sie Unix verwenden, versucht das Skript, die Funktion os.popen zu benutzen, um den file-Befehl aufzurufen, der eine menschenlesbare Beschreibung des Inhalts sowohl von Text- als auch von binären Dateien zurückgibt. Das kann viel mehr Zeit bei der Erzeugung brauchen, aber wenn man ihn einmal hat, ist der XML-Code sehr wertvoll und muß nicht jedesmal neu erzeugt werden, wenn wir ihn brauchen:

# Prüfe Inhalt mit file-Befehl unter Linux
if os.name == "posix":
  # Öffne einen Prozeß zur Prüfung des Dateiinhalts
  fd = popen("file \"" + filename + "\"")
  self.contents = fd.readline().rstrip( )
  fd.close( )
else:
  # Keine Inhaltsinformation
  self.contents = self.extension

Wenn Sie Unix nicht benutzen, steht Ihnen der file-Befehl nicht zur Verfügung, und dann wird die Information über den Inhalt durch die Dateierweiterung angegeben. Für eine Word-Datei z. B. lautet der XML-Code <contents>.doc</contents>. Unter Unix ergibt der Aufruf von popen jedoch ein Dateiobjekt. Der Ausgabetext des Befehls wird mit der readline-Methode des Dateiobjekts eingelesen. Das Ergebnis wird dann zurechtgestutzt und als Beschreibung des Dateiinhalts benutzt. Die Klasse verfügt über eine einzige Methode, getXML, die die Dateiinformation als ein einzelnes XML-Element im Stringformat zurückgibt:

def getXML(self):         
    """Gibt XML-Version aller Dateneinträge zurück."""
    return ("<file name=\"" + escape(self.filename) + "\">" +
         "\n\t<userID>" + str(self.uid) + "</userID>" +
         "\n\t<groupID>" + str(self.gid) + "</groupID>" +
         "\n\t<size>" + str(self.size) + "</size>" +
         "\n\t<lastAccessed>" + self.accessed +
         "</lastAccessed>" +
         "\n\t<lastModified>" + self.modified +
         "</lastModified>" +
         "\n\t<created>" + self.created + "</created>" +
         "\n\t<extension>" + self.extension +
         "</extension>" +
         "\n\t<contents>" + escape(self.contents) +
         "</contents>" +
         "\n</file>")

Im vorigen Code wird XML als Folge von Strings zusammengebastelt. Ein anderer Weg besteht darin, mit einem DOMImplementation-Objekt individuelle Elemente zu erzeugen und sie in die Dokumentstruktur einzufügen (wird in Python und der Entwurf verteilter Systeme erläutert).

Diese beiden Klassen werden benutzt, um ein längliches XML-Dokument zu entwickeln, das Dateien und Metadaten für jeden Abschnitt Ihres Dateisystems enthält. Das vollständige Listing von index.py wird im folgenden Beispiel gezeigt.

Beispiel: index.py

#!/usr/bin/env python
"""
index.py
Benutzung: python index.py <starting-dir> <output-file>
"""
import os
import sys

from os import stat
from os import listdir
from os import popen
from stat import S_ISDIR
from time import ctime

from xml.sax.saxutils import escape

XML_ENC = "ISO-8859-1"

"""""""""""""""""""""""""""""""""""""""""""""""""""
Klasse: Index(startingDir, outputFile)
"""""""""""""""""""""""""""""""""""""""""""""""""""
class Index:      
      """
      Diese Klasse indexiert Dateien und erstellt ein daraus resultierendes XML-Dokument.
      """
      def __init__(self, startingDir, outputFile):
      """init: setzt Start-Verzeichnis und Ausgabedatei"""
      self.outputFile = outputFile
      self.startingDir = startingDir
      def indexDirectoryFiles(self, dir):
      """Indexiert eine Verzeichnisstruktur und erzeugt eine XML-Ausgabedatei."""
      # Bereite XML-Ausgabedatei vor
      self.__fd = open(self.outputFile, "w")
      self.__fd.write('<?xml version="1.0" encoding="' +
      XML_ENC + '"?>\n')
      self.__fd.write("<IndexedFiles>\n")
      # Führe Indexierung durch
      self.__indexDir(dir)
      # Schließe XML-Datei
      self.__fd.write("</IndexedFiles>\n")
      self.__fd.close( )
      def __indexDir(self, dir):
      """Rekursive Funktion für das eigentliche Indexieren."""
      # Erzeuge ein indexFile für jede normale Datei, und
      # rufe die Funktion erneut bei jedem Verzeichnis auf
      files = listdir(dir)
      for file in files:
      fullname = os.path.join(dir, file)
      st_values = stat(fullname)
      # Prüfe, ob es ein Verzeichnis ist
      if S_ISDIR(st_values[0]):
      print file
      # Erzeuge Verzeichniselement
      self.__fd.write("<Directory ")
      self.__fd.write(' name="' + escape(fullname) + '">\n')
      self.__indexDir(fullname)
      self.__fd.write("</Directory>\n")
      else:
      # Erzeuge Eintrag für normale Datei
      print dir + file
      lf = IndexFile(fullname, st_values)
      self.__fd.write(lf.getXML( ))

"""""""""""""""""""""""""""""""""""""""""""""""""""
Klasse: IndexFile(filename, stat-tuple)
"""""""""""""""""""""""""""""""""""""""""""""""""""
class IndexFile:
"""
      Einfaches Objekt zur Darstellung einer Datei mit XML
      """
      def __init__(self, filename, st_vals):
      """Extrahiere Attribute vom angegebenen stat-Objekt."""
      self.filename = filename
     self.uid = st_vals[4]
      elf.gid = st_vals[5]
      size = st_vals[6]
      v.accessed = ctime(st_vals[7])
      self.modified = ctime(st_vals[8])
      self.created = ctime(st_vals[9])
      # Versuche es mit einer Dateinamenserweiterung
      self.extension = os.path.splitext(filename)[1]
      # Prüfe Inhalt mit file-Befehl unter Linux
      if os.name == "posix":
      # Öffne einen Prozeß für die Prüfung des Dateiinhalts
      fd = popen("file \"" + filename + "\"")
      self.contents = fd.readline().rstrip( )
      fd.close( )
      else:
      # Keine Informationen über den Inhalt
      self.contents = self.extension
      def getXML(self):
      """Gibt XML-Version aller Dateneinträge zurück."""
      return ("<file name=\"" + escape(self.filename) + "\">" +
      "\n<userID>" + str(self.uid) + "</userID>" +
      "\n<groupID>" + str(self.gid) + "</groupID>" +
      "\n<lastAccessed>" + self.accessed +
      "</lastAccessed>" +
      "\n<lastModified>" + self.modified +
      "</lastModified>" +
      "\n\t<created>" + self.created + "</created>" +
      "\n\t<extension>" + self.extension +
      "</extension>" +
      "\n\t<contents>" + escape(self.contents) +
      "</contents>" +
      "\n</file>")

"""""""""""""""""""""""""""""""""""""""""""""""""""
Hauptprogramm
"""""""""""""""""""""""""""""""""""""""""""""""""""
if __name__ == "__main__":
index = Index(sys.argv[1], sys.argv[2])
print "Start-Verzeichnis:", index.startingDir
print "Ausgabedatei:", index.outputFile
index.indexDirectoryFiles(index.startingDir)

Ausführen von index.py

Beim Ausführen von index.py von der Kommandozeile aus muß ein Startverzeichnis und ein XML-Dateiname angegeben werden, der als Ausgabe dient:

 $> python index.py /usr/bin/ usrbin.xml 

Das Skript gibt Verzeichnisnamen ähnlich zum find-Befehl aus, aber nachdem es beendet ist, enthält die Datei usrbin.xml etwas, das ähnlich zu folgendem ist:

<?xml version="1.0" encoding=" ISO-8859-1"?>
<IndexedFiles>
  <Directory name="/usr/bin/X11">
    <file name="/usr/bin/X11/Magick-config">
      <userID>0</userID>
      <groupID>0</groupID>
      <size>1786</size>
      <lastAccessed>Fri Jan 19 22:29:34 2001</lastAccessed>
      <lastModified>Mon Aug 30 20:49:06 1999</lastModified>
      <created>Mon Sep 11 17:22:01 2000</created>
      <extension>None</extension>
      <contents>/usr/bin/X11/Magick-config: Bourne shell script text</contents>
    </file>
    <file name="/usr/bin/X11/animate">
      <userID>0</userID>
      <groupID>0</groupID>
      <size>16720</size>
      <lastAccessed>Fri Jan 19 22:29:34 2001</lastAccessed>
      <lastModified>Mon Aug 30 20:49:09 1999</lastModified>
      <created>Mon Sep 11 17:22:01 2000</created>
      <extension>None</extension>
      <contents>/usr/bin/X11/animate: ELF 32-bit LSB executable, Intel 80386,version 1, dynamically linked (uses shared libs), stripped</contents>
    </file>

Die Größe der XML-Datei hängt vom gewählten Ausgangsverzeichnis ab. Per Voreinstellung folgt das Programm symbolischen Links (unter Unix sind symbolische Links Verzeichnisse oder Dateien, die für andere Verzeichnisse oder Dateien stehen), was jedoch die Möglichkeit von unendlicher Rekursion birgt. Passen Sie also auf! Das beste, was Sie in einem solchen Fall machen können, ist vermutlich, Ihr Home-Verzeichnis oder das Verzeichnis einer Open Source Software zu indexieren, die Sie heruntergeladen haben.

Den Index durchsuchen

Nun da Ihre Dateien als XML abstrahiert worden sind, können Sie einen SAX-Ereignis-Handler schreiben, der nach Einträgen in dieser Dateiliste sucht. SAX ist hier eine gute Wahl, da das Dokument leicht einige Megabytes groß werden könnte. Es dann zu interpretieren, während es gelesen wird, ist der Ressourcen-schonendste Ansatz.

Das Skript saxfinder.py erwartet ein einziges Argument (den Suchtext), parst die angegebene XML-Datei und prüft sie mit seinen SAX-Handler-Schnittstellen, um zu sehen, ob unter den Dateien welche sind, die für Sie von Interesse sind.

Das Skript geht davon aus, XML zu verarbeiten, wie es zuvor mit index.py erzeugt wurde.

Falls das contents-Element Ihrer XML-Datei die Zeichendaten enthält, die Sie in der Kommandozeile angegeben haben, wird die Datei als Treffer betrachtet, und das Skript gibt eine entsprechende Meldung aus. Wenn Sie Windows verwenden, enthalten Ihre contents-Tags nur die Dateierweiterung, wodurch Ihre Suche auf Dateierweiterungen beschränkt ist, es sei denn, Sie ändern den Code ab, um mehr als nur das contents-Element zu berücksichtigen.

Verwenden Sie drei Methoden der SAX-Schnittstelle, um die Suche nach Metadaten zu implementieren. Zuerst wird startElement implementiert, um den Namen des aktuellen file-Elements festzuhalten, wie auch um sich zu merken, wenn Sie die Zeichendaten nach einem contents-Tag betreten:

def startElement(self, name, attrs):
  if name == "file":
    self.filename = attrs.get('name', "")

elif name == "contents":
  self.getCont = 1

Wenn Sie ein contents-Element betreten, wird ein Flag (self.getCont) gesetzt, damit die characters-Methode weiß, wann sie Zeichendaten sammeln und in einer anderen Instanzvariablen speichern muß:

def characters(self, ch):
   if self.getCont:
      self.contents += ch

Wenn es zu einem endElement-Ereignis kommt, untersucht das Skript den Inhalt, der angesammelt worden ist (falls überhaupt), um zu sehen, ob er zum ursprünglichen Kommandozeilen-Parameter paßt. Wenn ja, wird der Dateiname ausgegeben. Wenn nicht, macht SAX fröhlich weiter mit dem nächsten Dateieintrag im XML-Dokument:

def endElement(self, name):
   if name == "contents":
      self.getCont = 0
      if self.contents.find(self.contentType) > -1:
         print self.filename, "hat", self.contentType, "als Inhalt."
      self.contents = ""

Zusätzlich wird das self.getCont-Flag deaktiviert, nachdem ein contents-Element verlassen wird, damit die characters-Methode keine Daten mehr sammelt.

SAX hilft Ihnen dabei, indem es Ihnen ermöglicht, eine XML-Indexdatei zu verarbeiten, die ein gesamtes Dateisystem darstellt und leicht 20 Megabytes auf Ihrer Festplatte beanspruchen kann. Ein solch gigantisches Dokument mit DOM zu parsen kann schwierig und unerträglich langsam sein.

Das folgende Beispiel zeigt das vollständige Listing von saxfinder.py.

Beispiel: saxfinder.py

"""
saxfinder.py - sucht in der Ausgabe von index.py
"""
import sys

from xml.sax import make_parser      
from xml.sax import ContentHandler

class FileHandler(ContentHandler):
  def __init__(self, contentType):
      self.getCont = 0
      self.contents = ""
      self.filename = ""
      self.contentType = contentType

def startElement(self, name, attrs):
  if name == "file":
      self.filename = attrs.get('name', "")

      elif name == "contents":
      self.getCont = 1

def characters(self, ch):
  if self.getCont:
      self.contents += ch

def endElement(self, name):
  if name == "contents":
      self.getCont = 0
      if self.contents.find(self.contentType) > -1:
        print self.filename, "hat", self.contentType, "als Inhalt."
      self.contents = ""

# Hauptprogramm

fh = FileHandler(sys.argv[1])
parser = make_parser( )
parser.setContentHandler(fh)
parser.parse(sys.stdin)

Sie können saxfinder.py unter Unix und Windows von der Kommandozeile aus starten. Sie müssen einen Suchstring als ersten Parameter angeben und ein XML-Dokument (erzeugt mit index.py) auf die Standardeingabe umlenken oder über eine Pipe weitergeben:

 $> python saxfinder.py "C program" < nard.xml 

Das Ergebnis sollte ungefähr so aussehen:

/home/shm00/nard/xd/server.cpp hat C program als Inhalt.
/home/shm00/nard/xd/shmoo.cpp hat C program als Inhalt.
/home/shm00/nard/gl-misc/array.cpp hat C program als Inhalt.
/home/shm00/nard/gl-misc/vertex.cpp hat C program als Inhalt.
/home/shm00/nard/gl-misc/mecogl.cpp hat C program als Inhalt.
/home/shm00/nard/gl-misc/drewgl/smugl.cpp hat C program als Inhalt.
/home/shm00/nard/gl-misc/drewgl/pal.cpp hat C program als Inhalt.
/home/shm00/nard/gl-misc/drewgl/pal.h hat C program als Inhalt.
/home/shm00/nard/gl-misc/drewgl/gl.cpp hat C program als Inhalt.

  

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema Python & XML bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2002 O'Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Python & XML" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O’Reilly Verlag GmbH & Co. KG, Balthasarstraße 81, 50670 Köln, kommentar(at)oreilly.de