Een directory doorzoeken

Deze pagina bevat uitvoerbare code.

Wat staat er allemaal in?

Een directory met subdirectories is een boom, en je weet van tevoren niet hoe diep hij is. In het eerste college drukte je daarom met recursie de namen af van alle bestanden in een directory. In dit werkcollege doorzoek je een directory op meer manieren: hoeveel bestanden staan erin, hoeveel ruimte nemen ze in, en staat er ergens een naam die in de terminal problemen geeft? Een naam met een spatie of een apostrof moet je in de terminal tussen aanhalingstekens zetten, en dat vergeet je makkelijk.

De directory

Een directory is een dictionary met drie sleutels, net als in het eerste college:

Sleutel

Waarde

"naam"

de naam van de directory

"bestanden"

een dictionary van bestandsnaam naar grootte in kilobyte

"directories"

een lijst van subdirectories, elk weer een dictionary in deze vorm

In de cel hieronder staat de directory documents, met drie subdirectories. projecten heeft zelf ook weer een subdirectory:

        flowchart TD
    D["documenten"] --> V(["verslag.docx"])
    D --> R(["rooster.pdf"])
    D --> P["projecten"]
    D --> F["foto's"]
    D --> L["leeg"]
    P --> PL(["plan.docx"])
    P --> LO(["logo.png"])
    P --> A["archief"]
    A --> N(["notulen.docx"])
    A --> S(["setup.exe"])
    F --> ST(["strand.jpg"])
    F --> B(["berg.jpg"])
    

EMPTY is een directory zonder bestanden en zonder subdirectories, en PROJECTS is de subdirectory projecten. Voer de cel eerst uit.

documents = {
    "naam": "documenten",
    "bestanden": {"verslag.docx": 120, "rooster.pdf": 300},
    "directories": [
        {
            "naam": "projecten",
            "bestanden": {"plan.docx": 40, "logo.png": 500},
            "directories": [
                {
                    "naam": "archief",
                    "bestanden": {"notulen.docx": 25, "setup.exe": 900},
                    "directories": [],
                },
            ],
        },
        {
            "naam": "foto's",
            "bestanden": {"strand.jpg": 2000, "berg.jpg": 1800},
            "directories": [],
        },
        {"naam": "leeg", "bestanden": {}, "directories": []},
    ],
}

EMPTY = {"naam": "leeg", "bestanden": {}, "directories": []}
PROJECTS = documents["directories"][0]

Wat je gaat maken

Stap

Functie

Doet

Hoe de recursie loopt

1

count_files

het aantal bestanden

een lus over de subdirectories

2

total_size

de totale grootte

sum([...]) over de subdirectories

3

safe_name

is een naam veilig in de terminal?

over een string, vanaf het eind

4

unsafe_names

alle namen die niet veilig zijn

een lus over de subdirectories, met stap 3

5

depth

hoeveel niveaus diep de directory is

een lus over de subdirectories

Elke functie telt de directory zelf mee én al zijn subdirectories, hoe diep ze ook gaan.

Stap 1: count_files(d)

Geeft het aantal bestanden in d en in al zijn subdirectories.

Aanroep

Resultaat

count_files(documents)

8

count_files(PROJECTS)

4

count_files(EMPTY)

0

Hint

Begin met het aantal bestanden in d zelf: len(d["bestanden"]). Tel daar in een lus over d["directories"] voor elke subdirectory count_files van die subdirectory bij op. Het basisgeval is een directory zonder subdirectories: dan doet de lus niets.

# jouw oplossing
assert count_files(documents) == 8
assert count_files(PROJECTS) == 4
assert count_files(EMPTY) == 0

Stap 2: total_size(d)

Geeft de totale grootte in kilobyte van alle bestanden in d en in al zijn subdirectories.

Aanroep

Resultaat

total_size(documents)

5685

total_size(PROJECTS)

1465

total_size(EMPTY)

0

Schrijf het deze keer zonder lus, met sum.

Hint

d["bestanden"].values() geeft de groottes van de bestanden in d, en sum telt ze op. Voor de subdirectories maak je een list comprehension met voor elke subdirectory zijn total_size, en die tel je ook op met sum([...]), zoals in week 2.

# jouw oplossing
assert total_size(documents) == 5685
assert total_size(PROJECTS) == 1465
assert total_size(EMPTY) == 0

Stap 3: safe_name(name)

Een naam is veilig als hij alleen letters van a tot en met z en A tot en met Z bevat, cijfers, en de tekens ., _ en -. Zo’n naam kun je in de terminal zonder aanhalingstekens typen. Een lege naam telt als veilig: er staat geen verkeerd teken in.

safe_name(name) geeft True als name veilig is, en anders False. Gebruik recursie en bekijk de naam vanaf het eind, teken voor teken:

  • Is de naam leeg, dan geef je True terug.

  • Is het laatste teken geen letter, geen cijfer en niet ., _ of -, dan geef je False terug.

  • Anders roep je de functie opnieuw aan, met de naam zonder het laatste teken, en geef je het resultaat daarvan terug.

Aanroep

Resultaat

safe_name("data_2024-01.csv")

True

safe_name("foto's")

False

safe_name("mijn cv.pdf")

False

Hint

Het laatste teken van s is s[-1], en alles ervóór is s[:-1]. ascii_letters is de string met alle letters van a tot en met z en van A tot en met Z. De cel hieronder zet de goede tekens al in SAFE_CHARS; met in vraag je of een teken daarin staat. Zodra je een verkeerd teken vindt, weet je het antwoord.

from string import ascii_letters, digits

SAFE_CHARS = ascii_letters + digits + "._-"

# jouw oplossing
assert safe_name("verslag.docx") == True
assert safe_name("data_2024-01.csv") == True
assert safe_name("foto's") == False
assert safe_name("mijn cv.pdf") == False
assert safe_name("") == True

Stap 4: unsafe_names(d)

Geeft een lijst met de namen in d en in al zijn subdirectories die niet veilig zijn: namen van bestanden en namen van subdirectories. Eerst de bestanden van d zelf. Daarna, per subdirectory in de volgorde van de lijst, eerst de naam van die subdirectory, als die niet veilig is, en dan wat erin staat.

Aanroep

Resultaat

unsafe_names(documents)

["foto's"]

unsafe_names(EMPTY)

[]

De testcel gebruikt ook een kleine directory WORK met drie onveilige namen.

Hint

De bestanden van d zelf vind je met een list comprehension en safe_name uit stap 3. Loop daarna de subdirectories langs. Is de naam van een subdirectory niet veilig, zet hem dan in je lijst, en plak er daarna het resultaat van unsafe_names voor die subdirectory achter, met +.

# jouw oplossing
WORK = {
    "naam": "mijn werk",
    "bestanden": {"cv.pdf": 10, "brief 1.docx": 5},
    "directories": [
        {"naam": "oude versies", "bestanden": {"a:b.txt": 1}, "directories": []},
    ],
}

assert unsafe_names(documents) == ["foto's"]
assert unsafe_names(WORK) == ["brief 1.docx", "oude versies", "a:b.txt"]
assert unsafe_names(EMPTY) == []

Stap 5: depth(d)

Geeft hoeveel niveaus diep d is. Een directory zonder subdirectories is 1 niveau diep. Een directory met subdirectories is 1 niveau dieper dan zijn diepste subdirectory.

Aanroep

Resultaat

depth(documents)

3

depth(PROJECTS)

2

depth(EMPTY)

1

Hint

Onthoud in een lus over de subdirectories de grootste diepte tot nu toe, zoals je in week 2 het langste woord tot nu toe onthield. Begin bij 0, zodat een directory zonder subdirectories op 1 + 0 uitkomt.

# jouw oplossing
assert depth(documents) == 3
assert depth(PROJECTS) == 2
assert depth(EMPTY) == 1

Tot slot

In documents staan acht bestanden, samen 5685 kilobyte, en één naam is niet veilig in de terminal: de directory foto's, door de apostrof.

Kijk nog eens naar stap 1, 2, 4 en 5. Elke functie liep de subdirectories langs en riep zichzelf aan voor elke subdirectory, in een lus of in een list comprehension. Nergens stond een if voor het basisgeval: een directory zonder subdirectories heeft een lege lijst, en dan volgt er geen recursieve aanroep. Stap 3 werkte anders: daar was het kleinere probleem een kortere string, en dan heb je wel een if nodig om te stoppen.