7. Práce se souborovým systémem


Obsah


Argumenty z příkazové řádky

Použití modulu sys

Modul sys poskytuje atribut argv, což je seznam, který obsahuje argumenty zadané v příkazové řádce. Prvním prvkem je vždy název skriptu.

# soubor script.py
import sys

# sys.argv obsahuje všechny argumenty z příkazové řádky
print("Název skriptu:", sys.argv[0])

# pokud existují další argumenty
if len(sys.argv) > 1:
    print("Argumenty:", sys.argv[1:])
else:
    print("Žádné argumenty nebyly zadány.")

Po spuštení tohoto skriptu se dvěma argumenty hello a world dostaneme následující výsledek.

$ python3 script.py hello world 
Název skriptu: script.py
Argumenty: ['hello', 'world']

Modul argparse

Pro složitější zpracování parametrů, jako je pojmenování argumentů, výchozí hodnoty nebo validace, je lepší použít modul argpase.

import argparse

parser = argparse.ArgumentParser(description="Ukázka hodnoty args.")

# povinný argument name
parser.add_argument("name", help="Jméno uživatele.")
# nepovinný argument s výchozí hodnotou, který se pokusí přetypovat hodnotu na int
parser.add_argument("--age", type=int, default=18, help="Věk uživatele.")
# nepovinný argument pro příznak, tj. pokud jej uvedeme bude mít hodnotu True
parser.add_argument("--verbose", action="store_true", help="Zobrazit podrobné informace.")
# nepovinný více hodnot pro jeden argument
parser.add_argument("--hobbies", nargs="+", help="Seznam koníčků.")  

# v args jsou hodnoty uloženy v Namespace objektu
args = parser.parse_args()
args.name # přístup k hodnotě name
args.hobbies # přístup k hodnotě hobbies atd.

# příkaz: python script.py Bob --age 25 --verbose
# obsah args: Namespace(name='Bob', age=25, verbose=True, hobbies=None)

# příkaz: python script.py Charlie --hobbies čtení běhání programování
# obsah args: Namespace(name='Charlie', age=18, verbose=False, hobbies=['čtení', 'běhání', 'programování'])

Modul pathlib

Programátoři běžně pracovali s cestami jako s řetězci a aplikovali na ně běžné metody pro řetězce. To v praxi přináší mnohé problémy (například rozdíly mezi platformami).

Modul pro manipulaci s cestami a soubory. Poskytuje objektový přístup. Dostupný od verze 3.4, dříve se používala kombinace modulů os, glob a shutil, která s cestami pracovala jako s běžnými řetězci. Modul pathlib nabízí objektový přístup.

Jednonuchý příklad získání aktuálního pracovního adresáře:

import pathlib

# získáme instanci třídy Path reprezentující aktuální pracovní adresář
pathlib.Path.cwd()

Instanci třídy Path je rovněž možné vytvořit z klasického řetězce reprezentující cestu v souborovém systému:

import pathlib

# Windows - využití r pro ignoraci \ jako escape znaku
pathlib.Path(r"C:\Users\user\python\file.txt")

# Unix
pathlib.Path("/home/user/python/file.txt")

Zde je nutné si uvědomit, že se jedná o reprezentaci libovolné (i fiktivní) cesty v souborovém systému. Soubory ani složky nemusí existovat. Důležité je, že se se s cestami pracuje jako s datovou strukturou umožňující abstrakce. Formát reprezentace je automaticky vybrát na základě operačního systému.

Jedna z abstrakcí je jednoduché získání domovského adresáře uživatele:

import pathlib

# domovský adresář
home = pathlib.Path.home()

# můžeme spojit několik složek
python_scripts = home / "python" / "scripts"

# druhý způsob spojení pomoci .joinpath
python_scripts = home.joinpath("python", "scripts")

V případě pathlib.Path.home() získáme cestu absolutní, obsahuje tedy kompletní cestu ke složce uživatele. Je však možné pracovat i s cestou relativní.

import pathlib

# relativní cesta
python_scripts = pathlib.Path("scripts", "python")
# absolutní cesta
home = pathlib.Path.cwd()

# jejich spojení
pathlib.Path.joinpath(home, python_scripts)

V případě relativní cesty můžeme použít speciální metodu Path.resolve() pro získání cesty absolutní. Dosáhneme pak stejného výsledku jako výše.

import pathlib

# relativní cesta
python_scripts = pathlib.Path("scripts", "python")

python_scripts.resolve()

Přístup k jednotlivým částem cesty

Třída Path obsahuje vlastnosti k přístupu k užitečným částem cesty. Pro jednoduchý přehled je možné použít následující "tahák".

Rodičovská složka/složka ve které je soubor uložen

Pomoci vlastnosti .parent můžeme jednoduše získat rodičovský adresář.

import pathlib

# rodičovská složka
pathlib.Path.cwd().parent

# řetězení je možné
pathlib.Path.cwd().parent.parent.parent

# složka obsahující soubor file.py
pathlib.Path.cwd().joinpath("file.py").parent
Název souboru/složky

Pomoci vlastnosti .name můžeme získat název souboru nebo složky.

import pathlib

# název složky
pathlib.Path.cwd().name

# název souboru
pathlib.Path.cwd().joinpath("file.py").name
Název souboru bez přípony

Pomoci vlastnosti .stem můžeme získat název souboru bez jeho přípony (suffixu).

import pathlib

# název souboru bez suffixu
pathlib.Path.cwd().joinpath("file.py").stem
Přípona souboru

Pomoci vlastnosti .suffix můžeme získat příponu souboru (suffix).

import pathlib

# název souboru bez suffixu
pathlib.Path.cwd().joinpath("file.py").suffix

# v případě vícero přípon lze použít
pathlib.Path.cwd().joinpath("file.tar.gz").suffixes
Rozdělení cesty na jednotlivé části

Objekt třídy Path lze jednoduše rozdělit na jednotlivé části.

import pathlib

pathlib.Path.cwd().parts

Metody na testovaní existence

Test zda cesta existuje

Pomoci metody exists() můžeme jednoduše ověřit zda zadaná cesta existuje.

import pathlib

# složka
pathlib.Path.cwd().exists()

# soubor
pathlib.Path.cwd().joinpath("file.py").exists()
Test zda se jedná soubor/složku

U objektu třídy Path můžeme testovat zda se jedná o složku nebo soubor. Pozor pokud soubor/složka neexistuje je vráceno False.

import pathlib

# složka
pathlib.Path.cwd().is_dir()

# soubor
pathlib.Path.cwd().joinpath("file.py").is_file()

Selekce souborů a složek na základě vzoru

Modul pathlib integruje funkcionalitu modulu glob, který umožňuje jednoduchým způsobem selektovat soubory a složky.

import pathlib

# všechny soubory v aktuálním adresáři
pathlib.Path.cwd().glob("*")

# všechny soubory s příponou .py v aktuálním adresáři
pathlib.Path.cwd().glob("*.py")

# všechny soubory s příponou .py v aktuálním adresáři začínající písmenem a
pathlib.Path.cwd().glob("a*.py")

# všechny soubory s příponou .py v aktuálním adresáři
# začínající písmenem a, končící písmenem b
pathlib.Path.cwd().glob("a*b.py")

# všechny soubory s příponou .py v libovolném podadresáři aktuálního adresáře
pathlib.Path.cwd().glob("*/*.py")

Pro rekurzivní prohledávání adresářové struktury lze použít funkci rglob().

import pathlib

# všechny soubory s příponou .py v libovolně zanořeném podadresáři dostupný z aktuálního adresáře
pathlib.Path.cwd().rglob("*/*.py")

# rekuzrivně prohledá složku files v aktuálním adresáři
pathlib.Path.cwd().rglob("files/*/*.py")

Práce se soubory

Pozor, při práci se soubory může dojít k nechtěnému poškození uložených dat. Ověřujte jaké zdrojové kódy na svém PC spouštíte.

Na chvíli odbočíme od modulu pathlib a podíváme se na práci se soubory (čtení a zápis).

Hlavní problematikou práce se soubory (ale i paralelní programování případně síťová komunikace) je správa prostředků. Situace kdy jeden program použije sdílený prostředek a již jej neodevzdá zpět nazýváme memory leak.

V kontextu souborů nám jde především o důsledné dodržování uzavírání otevřených souborů (ať už po čtení nebo zápisu). Zápis do souboru je totiž provádět přes takzvaný buffer. Data jsou napřed zapsána do bufferu a až po volání .close() jsou z bufferu zapsána na disk. V opačném případě mohou být tato data ztracena.

Dalším případem může být otevření souboru a následné vyvolání vyjimky (ukončení programu, nikoli však uzavření souboru).

Následující kód negarantuje, že soubor bude uzavřen v případě vyjimky.

file = open("hello.txt", "w")
file.write("Hello, World!")
file.close()

U funkce open() pouze zdůrazníme, že prvním argumentem je cesta k souboru, druhým pak mód otevření souboru:

Běžně jsou soubory otevřeny v textovém módu – k souborům je přistupováno tak, že obsahují text (v určitém kódování). Všechny módy však existují ve variantě b (binární, např rb).

Situaci můžeme ošetři již známým příkazem try ... finally.

# bezpečné otevření souboru - pokud zde nastane chyba soubor se ani neotevře
file = open("hello.txt", "w")

try:
    file.write("Hello, World!")
finally:
    # bezpečné zavření souboru v případě chyby u zápisu
    file.close()

Nejjednodším způsobem je však použítí příkazu with, který zabezpečuje bezpečné odbavení kontextu (například otevření a zavření souboru, připojení na server a další).

with open("hello.txt", "w") as file:
    file.write("Hello, World!")

Příkaz with se postará o volání speciálních dunder metod __enter__() a __exit__(). V případě souborů se tedy jedná o otevření a zavření souboru.

Příkaz with lze použít i v komplikovanější podobě.

with open("input.txt") as in_file, open("output.txt", "w") as out_file:
    pass

Objekt souboru

Po otevření souboru funkci open() získáme objekt reprezentující otevřený soubor. K dispozici máme následující metody.

with open("hello.txt", "r") as file:
    # přečtení jednoho řádku
    file.readline()

with open("hello.txt", "r") as file:
    # získání seznamu všech řádků
    file.readlines()

with open("hello.txt", "w") as file:
    # zápis řetězce
    file.write("test")

with open("hello.txt", "w") as file:
    # zápis seznamu řetězců jako jednotlivých řádků
    file.writelines(["test\n", "test2\n"])

Objekt souboru poskytuje iterátor proto je možné následující.

with open("hello.txt", "r") as file:
    for line in file:
        print(line)

with open("hello.txt", "r") as file:
    line = file.readline()
    while line:
        print(line)
        line = file.readline()

Použití pathlib pro práci se souborem

V rámci modulu pathlib můžeme se soubory jednoduše pracovat. K dispozici jsou čtyři vysokoůrovňové metody.

import pathlib

path = pathlib.Path.cwd() / "test.md"

# načtení textu
path.read_text()

# načtení bajtů
path.read_bytes()

# zápis textu
path.write_bytes("Test")

# zápis bajtů
path.write_bytes(b"Test")

Pro větši kontrolu je však možné použít příkaz with a metodu .open().

import pathlib

path = pathlib.Path.cwd() / "test.md"

with path.open(mode="r") as file:
    # přečteme jeden řádek, můžeme použít vše jako u objektu souboru
    file.readline()

Formát souboru csv

S formátem csv (comma separated value) jsme se setkali již několikrát. V jazyce Python je k dispozici modul csv umožňující jednoduchou práci s tímto formátem (čtení/zápis). Více zde.

import pathlib
import csv

# vytvoření a zápis
path = pathlib.Path("data.csv")

data = [[str(value) for value in range(5)] for _ in range(10)]

with path.open(mode="w") as file:
    # volitelným parametrem separator nastavujeme oddělovač
    # čárka je výchozí hodnota
    csv_writer = csv.writer(file, delimiter=",")
    
    for row in data:
        csv_writer.writerow(row)

# čtení
path = pathlib.Path("data.csv")

with path.open(mode="r") as file:
    csv_reader = csv.reader(file)
    
    input_data = [row for row in csv_reader]

assert data == input_data

Formát souboru json

Formát JSON (JavaScript Object Notation) je inspirován JavaScript syntaxí pro zápis objektů. V jazyce Python je nejblíže slovníku (případně zanořenému slovníku).

{
   "glossary":{
      "title":"example glossary",
      "GlossDiv":{
         "title":"S",
         "GlossList":{
            "GlossEntry":{
               "ID":"SGML",
               "SortAs":"SGML",
               "GlossTerm":"Standard Generalized Markup Language",
               "Acronym":"SGML",
               "Abbrev":"ISO 8879:1986",
               "GlossDef":{
                  "para":"A meta-markup language, used to create markup languages such as DocBook.",
                  "GlossSeeAlso":[
                     "GML",
                     "XML"
                  ]
               },
               "GlossSee":"markup"
            }
         }
      }
   }
}

Modul json jazyka Python můžeme použít pro jednoduché načítání a ukládání dat ve formatu JSON. Více zde.

import pathlib
import json

# vytvoření a zápis
path = pathlib.Path("data.json")

data = {
    "username": "pepa",
    "name": {
        "firstname": "Josef",
        "lastname": "Novak"
        },
    "titles": ["Mgr.", "Bc."],
    "salary": "30000"
}

with path.open(mode="w") as file:
    file.write(json.dumps(data))

# čtení
path = pathlib.Path("data.json")

with path.open(mode="r") as file:
    input_data = json.loads(file.read())

assert data == input_data

Formát souboru XML

Formát XML (Extensible Markup Language) je značkovací jazyk pro ukládání dat a vytváření dokumentů.

<knihovna>
    <kniha id="1">
        <nazev>Python Basics</nazev>
        <autor>Jan Novak</autor>
        <rok>2020</rok>
    </kniha>
    <kniha id="2">
        <nazev>Advanced Python</nazev>
        <autor>Petra Svobodova</autor>
        <rok>2021</rok>
    </kniha>
</knihovna>

Modul xml jazyka Python můžeme použít pro jednoduché načítání a ukládání dat ve formatu XML. Více zde.

import xml.etree.ElementTree as ET

# Načtení XML souboru
strom = ET.parse('knihy.xml')
koren = strom.getroot()

# Průchod elementy knihy
for kniha in koren.findall('kniha'):
    id_knihy = kniha.get('id')  # Získání atributu id
    nazev = kniha.find('nazev').text  # Získání textu z elementu nazev
    autor = kniha.find('autor').text  # Získání textu z elementu autor
    rok = kniha.find('rok').text  # Získání textu z elementu rok

    # Výpis informací o knize
    print(f'Kniha ID: {id_knihy}')
    print(f'Název: {nazev}')
    print(f'Autor: {autor}')
    print(f'Rok vydání: {rok}')
    print('-------------------------')

# Kniha ID: 1
# Název: Python Basics
# Autor: Jan Novak
# Rok vydání: 2020
# -------------------------
# Kniha ID: 2
# Název: Advanced Python
# Autor: Petra Svobodova
# Rok vydání: 2021
# -------------------------