untergeordnete Dateien/Verzeichnisse aussortieren

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Holger Chapman
User
Beiträge: 35
Registriert: Samstag 12. Juli 2014, 01:59

Hallo,

ich schreibe gerade ein kleines Programm, das für alle angegebenen Verzeichnisse (inkl. Unterverzeichnisse usw.) bestimmte Aktionen durchführt.
Jetzt will ich aus der Liste der Verzeichnisse, die das Programm durchgeht, diejenigen aussortieren, die schon in einem anderen Verzeichnis enthalten sind.

Also: aus

Code: Alles auswählen

['/d1/sd1', '/d1/sd1/file1', '/d2', '/d1']
soll werden:

Code: Alles auswählen

['/d2', '/d1']
. ('/d1/sd1' und '/d1/sd1/file1' sind beide in '/d1' enthalten und können daher aussortiert werden.)

Ich bastele jetzt schon 'ne ganze Weile mit for-Schleifen herum ... (

Code: Alles auswählen

if len(liste) > 1:
    neue_liste.append(liste[0])
    for i in range(len(liste) - 1):
        i_ist_neu = True
        for j in range(i + 1, len(liste)):
            print
            print "compare " + liste[i] + " and " + liste[j]
            print "compare '" + liste[i][:len(liste[j])] + "' and '" + liste[j][:len(liste[j])] + "'"
            if liste[i][:len(liste[j])] == liste[j][:len(liste[j])]:
) ... aber irgendwie habe ich das Gefühl, das mein ganzer Ansatz falsch ist und es eine viel elegantere Möglichkeit geben muss. - Vermutlich irgendwas mit list comprehensions.

Kann jemand von euch mir einen Tipp geben?

Vielen Dank!
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

basierend auf dem Beispiel wäre das einfacher:

[codebox=pycon file=Unbenannt.txt]>>> my_list = ['/d1/sd1', '/d1/sd1/file1', '/d2', '/d1']
>>> result =[x for x in my_list if x.count('/')==1]
>>> result
['/d2', '/d1']
>>> [/code]

Wie gesagt, basierend auf dem Beispiel von dir.
Wenn die Ausgangsliste z.B. ` ['/d1/sd1', '/d1/sd1/file1', '/d2', '/d1', '/d3/foo']` wäre, dann würde dann das so nicht funktionieren.

Die andere Frage wäre, ob du die Ausgangsliste nicht schon so generieren kannst, dass Unterverzeichnisse nicht enthalten sind.

Gruß, noisefloor
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

Nachtrag: dem 2. Fall kannst du mit einem regulären Ausdruck und einer Set-Comprehension zu Leibe rücken:

[codebox=pycon file=Unbenannt.txt]>>> import re
>>> my_list2 = ['/d1/sd1', '/d1/sd1/file1', '/d2', '/d1', '/d3/foo']
>>> pattern = r'^(\/\w+).*$'
>>> compiled = re.compile(pattern)
>>> result2 = list({compiled.match(x).group(1) for x in my_list2})
>>> result2
['/d2', '/d3', '/d1']
>>>[/code]

Gruß, noisefloor
Holger Chapman
User
Beiträge: 35
Registriert: Samstag 12. Juli 2014, 01:59

Hallo noisefloor,

vielen Dank für Deine Antworten! - Den zweiten Code mit der Regex habe ich noch nicht ganz verstanden, schaue ich mir aber nochmal in Ruhe an.
Leider tut der Code nicht ganz das, was ich will. Für ['/d1/sd1', '/d1/sd1/file1', '/d2', '/d1', '/d3/foo'] würde ich als Ergebnis erwarten:
['/d2', '/d1', '/d3/foo']. (Für '/d3/foo' gibt es kein übergeordnetes Verzeichnis in der Liste.)

Schönen Gruß


Holger
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

stimmt, Denkfehler meinerseits. So bekommst du alle gesuchten Verzeichnisse:

[codebox=pycon file=Unbenannt.txt]>>> import re
>>> my_list2 = ['/d1/sd1', '/d1/sd1/file1', '/d2', '/d1', '/d3/foo']
>>> pattern = r'^(\/\w+).*$'
>>> compiled = re.compile(pattern)
>>> top_level_dirs = [x for x in my_list2 if x.count('/')==1]
>>> result = [x for x in my_list2 if compiled.match(x).group(1) not in top_level_dirs]
>>> result
['/d3/foo']
>>> result + top_level_dirs
['/d3/foo', '/d2', '/d1'][/code]

Die Regex sucht einfach nur nach einen Ausdruck, der mit dem Slash beginnt, gefolgt von beliebig vielen Buchstaben und Zahlen, gefolgt von beliebigen Zeichen. Der Slash plus Zahlen- / Zeichenfolge ist die 1. Capture-Group und die enthält dann den ersten Teil der Verzeichnisses.

Gruß, noisefloor
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@noisefloor: mit regulären Ausdrücken auf Verzeichnisse einzuprügeln ist der falsche Weg. Dafür gibt es Funktionen in os.path.

Code: Alles auswählen

directories = ['/d1', '/d1/sd1', '/d1/sd1/file1', '/d2', '/d3/foo']
directories.sort()
base_directories = []
for directory in directories:
    if not any(os.path.commonprefix([d, directory]) == d for d in base_directories):
        base_directories.append(directory)
Holger Chapman
User
Beiträge: 35
Registriert: Samstag 12. Juli 2014, 01:59

@Sirius3:
Danke, das macht genau das, was ich gesucht habe! "os.path.commonprefix" kannte ich noch nicht ...

Schönen Gruß


Holger
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

`commonprefix` aus `os.path` kannte ich auch bis dato nicht.

Aber: es ist IMHO keine universelle Lösung für das gegebene Problem:

Code: Alles auswählen

import os.path

directories = ['/d1', '/d1/sd1', '/d1/sd1/file1', '/d2', '/d3/foo', '/d3/fool']
directories.sort()
base_directories = []
for directory in directories:
    if not any(os.path.commonprefix([d, directory]) == d for d in base_directories):
        base_directories.append(directory)
print(base_directories)
liefert

Code: Alles auswählen

['/d1', '/d2', '/d3/foo']
korrekt wäre `['/d1', '/d2', '/d3/foo', '/d3/fool']

BTW: ich verstehe den Sinn des `directories.sort()` an der Stelle nicht...

Gruß, noisefloor
Holger Chapman
User
Beiträge: 35
Registriert: Samstag 12. Juli 2014, 01:59

Hallo!

Danke für den Hinweis, noisefloor. Ich hatte den Code von Sirius3 schon eingebaut, aber der funktioniert tatsächlich noch nicht richtig.

Mit den Anregungen von euch beiden, noisefloor und Sirius3, habe ich jetzt Code geschrieben, der in meinen Tests immer gemacht hat, was ich will. - Vielleicht findet ihr ja doch noch Ausnahmesituationen, die ich nicht berücksichtigt habe, oder Dinge, die ich eleganter programmieren kann?

Hier jedenfalls der funktionierende Code:

Code: Alles auswählen

#!/usr/bin/env python2

import os

def ohne_dateien_aus_unterordnern(liste):
    liste = [os.path.abspath(i) for i in liste] # rel. Dateinamen -> absolut
    liste.sort() # damit uebergeordnete Ordner vor Unter-Elementen stehen
    if len(liste) > 1:
        tmp_liste = []
        for i in liste:
            is_new = True
            if len(tmp_liste) > 0:
                for j in tmp_liste:
                    if os.path.commonprefix([i + os.sep, j + os.sep]) in tmp_liste:
                        is_new = False
            if is_new == True:
                tmp_liste.append(i + os.sep)
        tmp_liste = [i[:-1] for i in tmp_liste]
        return tmp_liste
    else:
        return liste

# main
liste = ['/d1/sd1', '/d1', '/d1/sd1/file1', '/d2', '/d3/foo', '/d3/foo2']
print ' vorher: ' + str(liste)
liste = ohne_dateien_aus_unterordnern(liste)
print 'nachher: ' + str(liste)
Das Ergebnis sieht so aus:

Code: Alles auswählen

$ ./parms.py 
 vorher: ['/d1/sd1', '/d1', '/d1/sd1/file1', '/d2', '/d3/foo', '/d3/foo2']
nachher: ['/d1', '/d2', '/d3/foo', '/d3/foo2']
Viele Grüße!


Holger
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

was man auf jeden Fall kürzer / eleganter schreiben kann:

`if tmp_liste:` statt `if len(tmp_liste) > 0:` und `if is_new:` statt`if is_new == True:`

Gruß, noisefloor
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@Holger Chapman: Variabelnnamen sollten aussagekräftig sein. liste und tmp_liste sind zu generisch, i, j sind total falsch, weil man da eine Zahl erwartet. Normalerweise versucht man Flags zu vermeiden. Aus

Code: Alles auswählen

            is_new = True
            if len(tmp_liste) > 0:
                for j in tmp_liste:
                    if os.path.commonprefix([i + os.sep, j + os.sep]) in tmp_liste:
                        is_new = False
            if is_new == True:
                tmp_liste.append(i + os.sep)
wird

Code: Alles auswählen

            for j in tmp_liste:
                if os.path.commonprefix([i + os.sep, j + os.sep]) in tmp_liste:
                    break
            else:
                tmp_liste.append(i + os.sep)
Die if-Abfrage ist unnötig, weil bei einer leeren Liste die Schleife einfach 0-mal durchlaufen wird. Der else-Zweig bei for wird genau dann durchlaufen, wenn die Schleife nicht durch break verlassen wurde, also genau dann, wenn is_new nicht auf False gesetzt wurde.

Noch ein bißchen vereinfacht, sieht das dann so aus:

Code: Alles auswählen

def is_subdirectory(parent, path):
    return path.startswith(parent + os.sep)

def filter_common_paths(paths):
    paths = sorted(os.path.realpath(os.path.normpath(path)) for path in paths)
    result = []
    for path in paths:
        if not result or not is_subdirectory(result[-1], path):
            result.append(path)
    return result
Holger Chapman
User
Beiträge: 35
Registriert: Samstag 12. Juli 2014, 01:59

Hallo Sirius3,

vielen Dank, Dein Vorschlag hat den Code noch ein ganzes Stück verbessert.

Was ich nicht verstanden habe: Ist es von Vorteil, die Funktion "is_subdirectory" zu benutzen? Wäre es von Nachteil, an Stelle von

Code: Alles auswählen

def is_subdirectory(parent, path):
    return path.startswith(parent + os.sep)

if not result or not is_subdirectory(result[-1], path):
direkt die Bedingung

Code: Alles auswählen

if not result or not path.startswith(result[-1] + os.sep):
zu benutzen und die Funktion "is_subdirectory" wegzulassen? - Das erscheint mir übersichtlicher, als eine Funktion zu definieren, die nur an einer Stelle verwendet wird.

Und was ich anders machen möchte: Aus Deinem

Code: Alles auswählen

paths = sorted(os.path.realpath(os.path.normpath(path)) for path in paths)
möchte ich

Code: Alles auswählen

paths = sorted(os.path.abspath(path) for path in paths)
machen, weil symbolische Links in meinem Anwendungsfall nicht aufgelöst werden sollen.

Außerdem verwende ich bei den Variablenbezeichnungen lieber "file" als "dir" oder "path", weil der Code unabhängig davon funktionieren sollte, ob die Liste Dateien, Verzeichnisse oder Symlinks (oder eine Kombination davon) enthält.

(Wie nennt man eigentlich ein Element, das in einem Verzeichnis eines Dateisystems gelistet wird, wenn man noch nicht weiß, was genau es ist ("echte" Datei, Unterverzeichnis, Symlink, ...)? - "fso" (file system object)? "directory_entry"? Oder einfach nur "file" (gemäß dem Unix-Prinzip "Everything is a file")? - Ich habe mich für letzteres entschieden.)

Jedenfalls: So sieht mein kleines Programm jetzt aus:

Code: Alles auswählen

#!/usr/bin/env python2

import os

def simplify_file_list(file_list):
    # - remove elements contained in higher-level directories, if element + dir are in file_list
    # - convert relative file names to absolute file names (don't follow symlinks)
    # - sort elements alphabetically
    file_list = sorted(os.path.abspath(file_name) for file_name in file_list)
    simplified_file_list = []
    for file_name in file_list:
        if not simplified_file_list or not file_name.startswith(simplified_file_list[-1] + os.sep):
            simplified_file_list.append(file_name)
    return simplified_file_list

# main
file_list = ['/d3/foo2', '/d1/sd1', '/d1', '/d1/sd1/file1', '/d2', '/d3/foo']
print 'before: ' + str(file_list)
file_list = simplify_file_list(file_list)
print ' after: ' + str(file_list)
Wie gesagt, vielen Dank für Deine Hilfe! - Aus nicht funktionierendem Code ist erst funktionierender Code und dann schlanker und übersichtlicher funktionierender Code geworden. - Du hast mir sehr geholfen!

Viele Grüße


Holger
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@Holger Chapman: Funktionsnamen helfen auch, den Code zu verstehen, gerade bei kryptischen Stringoparationen, wo man nicht sofort sieht, was der Grund dafür ist.
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

Holger Chapman hat geschrieben: (Wie nennt man eigentlich ein Element, das in einem Verzeichnis eines Dateisystems gelistet wird, wenn man noch nicht weiß, was genau es ist ("echte" Datei, Unterverzeichnis, Symlink, ...)? - "fso" (file system object)? "directory_entry"? Oder einfach nur "file" (gemäß dem Unix-Prinzip "Everything is a file")? - Ich habe mich für letzteres entschieden.)
Ich würde auch bei 'file' bleiben. 'File system object' klingt nach den Einheiten, mit denen auf Dateisystemebene operiert wird (also Inodes unter Unix-Systemen); das ist aber hier eher nicht gemeint.
BlackJack

Wobei `file` a) der Name des Dateityps in Python ist, und b) hier gar keine Dateien sondern Dateinamen gemeint sind, was semantisch einen grossen Unterschied macht. Und wenn es nicht nur um Datei*namen* geht, sondern auch Pfade dranhängen ist `path` IMHO der passendere Name für so etwas. Wenn es nur der Pfad ist also der Pfad zu einem Verzeichnis, dann `dir_name` oder `folder_name`.
Antworten