Python - wprowadzenie

18 - Ścieżki, katalogi, pliki

W lekcji poświęconych zapisowi i odczytowi danych z plików tekstowych poruszaliśmy się w obrębie bieżącego katalogu, czyli pliki były odczytywane i zapisywane w katalogu, w którym uruchamiany był kod. Często jednak program musi operować na plikach i katalogach, które znajdują się w innym miejscu na komputerze, np. zapisywać wyniki analiz w wybranym podkatalogu, czy analizować dane znajdujące się w katalogu na innym dysku. Poniżej wyjaśnię jak to zrobić, a także pokażę m.in. jak tworzyć, usuwać katalogi i pliki czy sprawdzać ich istnienie.

Bieżący katalog

Zacznijmy od zaimportowania modułu os i sprawdźmy, gdzie właściwie jesteśmy (czyli jaki jest bieżący katalog i ścieżka) w systemie plików. Wynik będzie różny, w zależności od systemu operacyjnego i katalogu, w którym znajduje się uruchamiany plik z kodem:

import os
# Wydruk ścieżki do bieżącego katalogu:
print(os.getcwd())
/home/grzeg/Dokumenty/Python-kurs

Powyższa ścieżka została wyświetlona pod systemem Linux, pod Windows może wyglądać np. tak:

C:\Users\grzeg\Documents\Python-kurs

Ścieżki a systemy operacyjne

Zwróć uwagę na różnice między ścieżkami wyświetlanymi w systemach Windows i Linux. W obu typach systemów mamy inny sposób zapisek ścieżek, ale także różny jest sposób organizacji systemów plików. Nie będziemy tu wchodzić w szczegóły, ale warto wspomnieć o pewnych podstawowych różnicach.

W systemie Linux i innych opartych na Uniksie (np. Mac OS) ścieżka rozpoczyna się od prawego ukośnika / (ang. forward slash) oznaczającego główny katalog systemu (ang. root directory), który, jak nazwa wskazuje, jest katalogiem nadrzędnym wobec wszystkich innych w systemie. Dotyczy to także innych partycji dysków, które są montowane w wybranych katalogach. Przykładowo ścieżka /home/grzeg/DANE może prowadzić do partycji na osobnym dysku, na którym trzymam dane bioinformatyczne, które analizuję. Jak widać, znak / oddziela od siebie także kolejne podkatalogi w ścieżce.

W systemie Windows, sposób zapisu ścieżek wywodzi się od systemu DOS. Rzuca się w oczy stosowanie lewego ukośnika \ (ang. backslash), a także litera oznaczająca partycję znajdująca się na początku ścieżki, o której znajduje się dwukropek (np. C:). Zastosowanie lewego ukośnika ma wpływ na sposób zapisu ścieżek w łańcuchach znaków, przypominam, że \ oznacza tam ,,znak ucieczki'' nadający specjalne znaczenie kolejnemu znakowi. Wrócimy jeszcze do tego tematu.

Zawartość katalogu

Sprawdźmy teraz jakie katalogi i pliki znajdują się w bieżącym katalogu:

import os
# Pobieranie ścieżki do bieżącego katalogu
sciezka = os.getcwd()
# Pobranie listy pliku we wskazanym katalogu
katalogi_i_pliki = os.listdir(sciezka)
# Wydruk listy plików
print(katalogi_i_pliki)
['.ipynb_checkpoints', 'FIGS', 'katalogi_pliki.ipynb', 'tekst.txt']

Oczywiście w Twoim przypadku otrzymany wynik będzie inny. Jeśli nie podamy ścieżki funkcji listdir(), domyślnie będzie pobrana zawartość bieżącego katalogu, można więc powyższy kod skrócić:

katalogi_i_pliki = os.listdir()
print(katalogi_i_pliki)
['.ipynb_checkpoints', 'FIGS', 'katalogi_pliki.ipynb', 'tekst.txt']

Można też oczywiście podać ścieżkę do innego katalogu, np. zawartość podkatalogu FIGS, uzyskamy tak:

katalogi_i_pliki = os.listdir('FIGS')
print(katalogi_i_pliki)

['jupyter-lab-01.png', 'anaconda-01.png', 'anaconda-02.png']

Z kolei zawartość katalogu powyżej bieżącego, możemy pobrać tak (.. - oznacza katalog wyżej w hierarchii):

katalogi_i_pliki = os.listdir('..')
print(katalogi_i_pliki)
['Filogenetyka-local', 'UGENE_Data', 'python_dla_biologow', 'Python-kurs', 'filogenetyka', 'biologia_roslin', 'python-wprowadzenie', 'prezentacja.pdf', 'DANE']

Sprawdźmy teraz, co znajduje się w katalogu DANE. Musimy sprawdzić ścieżkę: ../DANE pod Linuksem lub ..\DANE pod Windows.

Pod systemem Linux:

katalogi_i_pliki = os.listdir('../DANE')
print(katalogi_i_pliki)
['atp1.fasta']

Pod systemem Windows:

katalogi_i_pliki = os.listdir('..\DANE')
print(katalogi_i_pliki)
['atp1.fasta']

Ścieżki względne i bezwzględne

Ścieżki ../DANE lub ..\DANE są przykładami ścieżek względnych, czyli rozpoczynających się w bieżącym katalogu. Ścieżki takie jak /home/grzeg/Dokumenty/DANE, czy C:\Users\grzeg\Documents\DANE to przykłady ścieżek bezwzględnych, które pokazują konkretne miejsca w systemie plików, niezależnie od tego, jaki jest katalog bieżący. Trzeba pamiętać, że stosowanie ścieżek bezwzględnych, jest obarczone pewnym ryzykiem, nie wiemy przecież, w jakim katalogu użytkownik będzie uruchamiał nasz program. Stosujemy, je raczej wtedy, gdy odnoszą się do jakichś stałych, w danym systemie operacyjnym miejsc (np. /bin w systemie Linux) albo takich, których położenie można ustalić, czy podaje je użytkownik.

Analizując powyższe przykłady, zapewne przyszło Ci na myśl, czy nie można napisać uniwersalnego kodu, który dostosowywałby format ścieżek do systemu operacyjnego, na którym uruchamiany jest program. Na szczęście Python posiada dość proste rozwiązanie tego problemu, przyda nam się tu moduł os.path. Zanim napiszesz i uruchomisz poniższy kod, utwórz katalog DANE w katalogu powyżej bieżącego katalogu roboczego i umieć tam plik atp1.fasta, który można pobrać stąd: atp1.fasta.

import os
sciezka = os.path.join('..','DANE')
print(f"Ścieżka: {sciezka}")
katalogi_i_pliki = os.listdir(sciezka)
print(katalogi_i_pliki)

Pod systemem Linux zobaczymy:

Ścieżka: ../DANE
['atp1.fasta']

Pod Windows:

Ścieżka: ..\DANE
['atp1.fasta']

Katalog domowy użytkownika

Czasem potrzebujemy uzyskać nazwę domowego katalogu użytkownika. Można go pobrać w ten sposób (~ - oznacza katalog domowy użytkownika):

katalog_domowy = os.path.expanduser('~')
print(katalog_domowy)
/home/grzeg

Linux:

/home/grzeg

Windows:

C:\Users\grzeg

Tworzenie katalogu

Nowy katalog można utworzyć dzięki funkcji makedirs(). Na przykład w bieżącym katalogu utwórzmy katalog TMP

os.makedirs('TMP')
print(os.listdir())
['.ipynb_checkpoints', 'FIGS', 'katalogi_pliki.ipynb', 'tekst.txt', 'TMP']

Zmiana bieżącego katalogu

Do zmiany bieżącego katalogu służy funkcja chdir(). Można zatem przejść do katalogu użytkownika np. w ten sposób (dalej będę podawał, o ile nie zaznaczę inaczej wyniki dla systemu Linux):

# Bieżący katalog:
print(os.getcwd())
katalog_domowy = os.path.expanduser('~')
# Zmiana katalogu bieżącego
os.chdir(katalog_domowy)
# Bieżący katalog:
print(os.getcwd())
/home/grzeg/Dokumenty/Python-kurs
/home/grzeg

Teraz, znając nasz główny katalog roboczy, można do niego wrócić:

katalog_domowy = os.path.expanduser('~')
katalog_roboczy = os.path.join('Dokumenty','Python-kurs')
sciezka = os.path.join(katalog_domowy, katalog_roboczy)
os.chdir(sciezka)
print(f'Jestem tu: {os.getcwd()}')
Jestem tu: /home/grzeg/Dokumenty/Python-kurs

Sprawdzanie istnienia katalogów i plików

Jeżeli teraz spróbuję ponownie utworzyć katalog TMP, pojawi się błąd:

os.makedirs('TMP')
---------------------------------------------------------------------------
FileExistsError                           Traceback (most recent call last)

<ipython-input-53-df8c00e06bd7> in <module>
----> 1 os.makedirs('TMP')

~/anaconda3/lib/python3.8/os.py in makedirs(name, mode, exist_ok)
    221             return
    222     try:
--> 223         mkdir(name, mode)
    224     except OSError:
    225         # Cannot rely on checking for EEXIST, since the operating system

FileExistsError: [Errno 17] File exists: 'TMP'

Katalog już istnieje. Jak widać, przed utworzeniem go, należałoby najpierw sprawdzić, czy nie ma katalogu o takiej samej nazwie. Posłuży nam do tego funkcja os.path.exists(), która zwraca True jeśli znajdzie plik, lub katalog o podanej nazwie w określonym miejscu:

katalog = 'TMP'
if os.path.exists(katalog):
    print(f'{katalog} istnieje')
else:
    print(f'Tworzę {katalog}')
    os.makedirs(katalog)
Tworzę TMP

Jak wspomniałem, funkcja os.path.exists(), która zwraca True, jeśli znajdzie plik, lub katalog o podanej nazwie w określonym miejscu. Możliwe jednak, że w podanym miejscu istnieje plik o danej nazwie, a chcemy utworzyć katalog (lub odwrotnie). Przyda nam się w takich przypadkach sposób sprawdzania, czy istnieje plik albo katalog o podanej nazwie (i lokalizacji). Służą do tego dwie kolejne funkcje: os.path.isfile() oraz os.path.isdir(). Sprawdzając ich działanie, napiszemy funkcję, która przyda się w dalszej części lekcji.

def sprawdz(nazwa):
    """Sprawdza, czy istnieje plik lub katalog."""
    if os.path.isfile(nazwa):
        print(f'Plik {nazwa} istnieje')
    elif os.path.isdir(nazwa):
        print(f'Katalog {nazwa} istnieje')
    else:
        print(f'W podanym miejscu nie ma ani pliku, ani katalogu: {nazwa}')

# Istniejący katalog w katalogu bieżącym
sprawdz('TMP')
# Istniejący plik w podanej ścieżce
sciezka = os.path.join(os.path.expanduser('~'),'Dokumenty','DANE','atp1.fasta')
sprawdz(sciezka)
# Nieistniejący katalog ani plik o podanej nazwie i ścieżce
nie_ma = os.path.join(os.path.expanduser('~'),
                      'Dokumenty',
                      'Przemowienie_na_wreczeniu_Nobla.txt')
sprawdz(nie_ma)
Katalog TMP istnieje
Plik /home/grzeg/Dokumenty/DANE/atp1.fasta istnieje
W podanym miejscu nie ma ani pliku, ani katalogu: /home/grzeg/Dokumenty/Przemowienie_na_wreczeniu_Nobla.txt

Zmiana nazwy katalogów i plików

Skoro wiemy jak stworzyć katalog i sprawdzić, czy plik lub katalog istnieje, warto by dowiedzieć się jak zmieniać ich nazwy i usuwać. Utwórzmy plik (patrz lekcja 16 - Dane w plikach tekstowych - zapis i odczyt) w katalogu TMP a następnie zmieńmy mu nazwę.

nazwa = os.path.join('TMP', 'dane.txt')
plik_zapis = open(nazwa, 'wt')
plik_zapis.write('Próba pliku')
plik_zapis.close()
sprawdz(nazwa)
print('Zmieniam nazwę.')
nowa_nazwa = os.path.join('TMP', 'dane-bk.txt')
os.rename(nazwa, nowa_nazwa)
sprawdz(nazwa)
sprawdz(nowa_nazwa)
Plik TMP/dane.txt istnieje
Zmieniam nazwę.
W podanym miejscu nie ma ani pliku, ani katalogu: TMP/dane.txt
Plik TMP/dane-bk.txt istnieje

Usuwanie plików

Teraz usuńmy plik:

sprawdz(nowa_nazwa)
print(f"Usuwam {nowa_nazwa}")
os.remove(nowa_nazwa)
sprawdz(nowa_nazwa)
Plik TMP/dane-bk.txt istnieje
Usuwam TMP/dane-bk.txt
W podanym miejscu nie ma ani pliku, ani katalogu: TMP/dane-bk.txt

Usuwanie katalogów

Usuwanie katalogów jest odrobinę bardziej skomplikowane. Funkcja os.rmdir() usuwa katalog, ale tylko wtedy, jeśli jest on pusty:

sprawdz('TMP')
#Usuwanie pustego
print('Usuwanie katalogu')
os.rmdir('TMP')
sprawdz('TMP')
Katalog TMP istnieje
Usuwanie katalogu
W podanym miejscu nie ma ani pliku, ani katalogu: TMP

Zatem przed usuwaniem katalogu tym sposobem, warto sprawdzić, czy jest on pusty. Najpierw jednak odtwórzmy katalog i plik:

# Tworzymy katalog
katalog = 'TMP'
if os.path.exists(katalog):
    print(f'{katalog} istnieje')
else:
    print(f'Tworzę {katalog}')
    os.makedirs(katalog)
sprawdz('TMP')

# Tworzymy plik
nazwa = os.path.join('TMP', 'dane.txt')
plik_zapis = open(nazwa, 'wt')
plik_zapis.write('Próba pliku')
plik_zapis.close()
sprawdz(nazwa)
Tworzę TMP
Katalog TMP istnieje
Plik TMP/dane.txt istnieje

Teraz napiszmy kod, który sprawdzi, czy katalog jest pusty, jeśli tak, to go usunie, jeśli nie, to wypisze odpowiedni komunikat:

katalog = 'TMP'
# Pobieramy listę plików z katalogu
pliki = os.listdir(katalog)
# Jeśli liczba plików w katalogu jest == 0
if len(pliki) == 0:
    print(f"Usuwam katalog: {katalog}")
    os.rmdir(katalog)
else:
    print(f"W katalogu: '{katalog}' znajduje się {len(pliki)} plików/katalogów.")
    
W katalogu: 'TMP' znajduje się 1 plików/katalogów.

Można też uzupełnić powyższy kod o funkcję, która usuwa zawartość katalogu z plików, ale tylko plików.

def usuwam_pliki(katalog):
    # Jeśli nie będzie katalogu, to zwrócona będzie wartość True
    wynik = True
    pliki_i_katalogi = os.listdir(katalog)
    print(f"Pliki: {pliki_i_katalogi}")
    for n in pliki_i_katalogi:
        nazwa = os.path.join(katalog, n)
        if os.path.isfile(nazwa):
            print(f"Usuwam plik: {nazwa}")
            os.remove(nazwa)
        elif os.path.isdir(nazwa):
            print(f"{nazwa} to katalog! Nie usuwam go!.")
            # Jest katalog, zwrócona będzie wartość False
            wynik = False
    return wynik   
    
katalog = 'TMP'
# Pobieramy listę plików z katalogu
pliki = os.listdir(katalog)
# Jeśli liczba plików w katalogu jest == 0
if len(pliki) == 0:
    print(f"Usuwam katalog: {katalog}")    
else:
    print(f"W katalogu: '{katalog}' znajuje się {len(pliki)} plików/katalogów.")
    print("Próbuję je usunąć:")
    # Jeśli funkcja usuwam_pliki() zwróci True (brak podkatalogów)
    if usuwam_pliki(katalog):
        os.rmdir(katalog)
sprawdz(katalog)                                
W katalogu: 'TMP' znajuje się 1 plików/katalogów.
Próbuję je usunąć:
Pliki: ['dane.txt']
Usuwam plik: TMP/dane.txt
W podanym miejscu nie ma ani pliku, ani katalogu: TMP

Tworzenie katalogów z podkatalogami

Co zrobić jeśli jeśli w katalogu ‘TMP’ znajdują się inne katalogi? Najpierw poznajmy sposób tworzenia katalogu, wraz z podkatalogami:

def drukuj_zawartosc(katalog):
    """Drukuje zawartość katalogu, z podkatalogami i plikami."""
    for sciezka, katalogi, pliki in os.walk(katalog):
        print(f"Ścieżka: {sciezka}")
        for plik in pliki:
            print(f"plik: {plik}")

# Lista katalogów do utworzenia, zapisana w sposób "Linuksowy"
katalogi_src = ['TMP/data/raw',
            'TMP/data/analysed',
            'TMP/results',
            'TMP/docs']
katalogi = []
# Tworzymy ścieżki niezależne od systemu operacyjnego
for katalog in katalogi_src:
    # Lista (pod)katalogów
    lista = katalog.split('/')
    # Łączymy z powrotem ścieżkę - uwaga na znak '*'!
    katalogi.append(os.path.join(*lista))
# Tworzymy kolejne katalogi i pliki
i = 1
for katalog in katalogi:
    # Tworzenie katalogów z podkatalogami
    os.makedirs(katalog)
    nazwa_pliku = os.path.join(katalog,f'data-{i}.txt')
    plik = open(nazwa_pliku, 'wt')
    plik.write(f"Plik: {nazwa_pliku}")
    plik.close()
    i += 1

drukuj_zawartosc('TMP')
Ścieżka: TMP
Ścieżka: TMP/data
Ścieżka: TMP/data/raw
plik: data-1.txt
Ścieżka: TMP/data/analysed
plik: data-2.txt
Ścieżka: TMP/docs
plik: data-4.txt
Ścieżka: TMP/results
plik: data-3.txt

Przeanalizuj kod, zwróć uwagę na znak * w poleceniu os.path.join(*lista). Polecenie os.path.join() nie przyjmuje listy jako argumentu ale zestaw argumentów, ,,gwiazdka'' powoduje, że elementy w liście ,,rozpakowują się'' tak, jakby każdy element listy był kolejnym argumentem przekazywanym liście.

Usuwanie katalogów z zawartością

Wiemy już, jak stworzyć dość złożony system katalogów, podkatalogów i plików, dowiedzmy się zatem, jak go wyczyścić. Służy do tego funkcja rmtree() z modułu shutil.

import shutil
# Usuwanie katalogu wraz całą zawartością.
shutil.rmtree('TMP')
drukuj_zawartosc('TMP')

Kopiowanie katalogów i plików

Skoro już mamy do czynienia z modułem shutil, to warto zapoznać się z funkcjami służącymi do kopiowania. Najpierw odtwórzmy katalog TMP z plikiem.

# Tworzymy katalog
katalog = 'TMP'
if os.path.exists(katalog):
    print(f'{katalog} istnieje')
else:
    print(f'Tworzę {katalog}')
    os.makedirs(katalog)
sprawdz('TMP')

# Tworzymy plik
nazwa = os.path.join('TMP', 'dane.txt')
plik_zapis = open(nazwa, 'wt')
plik_zapis.write('Próba pliku')
plik_zapis.close()
sprawdz(nazwa)

Tworzę TMP
Katalog TMP istnieje
Plik TMP/dane.txt istnieje

Teraz kopiowanie pliku:

drukuj_zawartosc('TMP')
# Kopiowanie pliku
print("Kopiuję")
kopia_pliku = os.path.join('TMP', 'kopia_danych.txt')
shutil.copy(nazwa, kopia_pliku)
drukuj_zawartosc('TMP')
Ścieżka: TMP
plik: dane.txt
Kopiuję
Ścieżka: TMP
plik: kopia_danych.txt
plik: dane.txt

Kopiowanie katalogu:

shutil.copytree('TMP', 'TMP-BK')
drukuj_zawartosc('TMP-BK')
Ścieżka: TMP-BK
plik: kopia_danych.txt
plik: dane.txt

Zadania

  1. Napisz program, który zapisze, w formacie FASTA zawartość pliku atp1.fasta w osobnych plikach zawierających poszczególne sekwencje - nazwy plików powinny zawierać opis sekwencji, znajdujący się w linii rozpoczynającej się od znaku >, utworzone pliki powinny się znaleźć w osobnym, utworzonym przez program katalogu o nazwie SEKWENCJE.

  2. Napisz program, który wykona czynność odwrotną do poprzedniego programu: połączy zawartość poszczególnych plików zawierających sekwencje, w jednym pliku o nazwie podanej przez użytkownika.

Uwaga: W rozwiązaniu zadania może pomóc lekcja 16 - Dane w plikach tekstowych - zapis i odczyt

Last updated on 12 Jan 2021
Published on 12 Jan 2021