17 - Ś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 plików i katalogów we wskazanym katalogu
katalogi_i_pliki = os.listdir(sciezka)
# Wydruk listy plików i katalogó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']
Teraz 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.
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
.
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
-
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 nazwieSEKWENCJE
. -
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