4. Práce se souborovým systémem a regexy

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.

Modul pathlib

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.

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).

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 v aktuálním adresáři
pathlib.Path.cwd().glob("*/*.py")

Práce se soubory

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

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).

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.

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

Regulární výrazy

Regulární výrazy (také známé jako regex nebo RegExp) jsou sekvence znaků, které definují vzory pro vyhledávání a manipulaci s textovými řetězci. Tyto vzory jsou používány pro vyhledávání a nahrazování textu v textových řetězcích nebo pro kontrolu formátu textu.

Regulární výrazy jsou v pythonu přístupné přes knihovnu re, více zde. Přehled základních speciální znaků a sekvecní najdete v tabulce (převzato z w3schools):

Znak Popis Příklad
[] Sada znaků "[a-m]"
\ Označuje speciální sekvenci (lze použít i k úniku speciálních znaků) "\d"
. Libovolný znak (kromě znaku nového řádku) "he..o"
^ Začíná na "^hello"
$ Končí na "planet$"
* Nula nebo více výskytů "he.*o"
+ Jeden nebo více výskytů "he.+o"
? Nula nebo jediný výskyt "he.?o"
{} Přesný počet výskytů "he.{2}o"
| Buď nebo "falls|stays"
() Zachytit a seskupit
\A Vrátí shodu, pokud jsou určené znaky na začátku řetězce "\AThe"
\b Vrátí shodu, pokud jsou určené znaky na začátku nebo na konci slova r"\bain"
r"ain\b"
\B Vrátí shodu, pokud jsou určené znaky přítomny, ale NE na začátku (nebo na konci) slova r"\Bain"
r"ain\B"
\d Vrátí shodu, pokud řetězec obsahuje číslice (čísla od 0-9) "\d"
\D Vrátí shodu, pokud řetězec NEobsahuje číslice "\D"
\s Vrátí shodu, pokud řetězec obsahuje bílý znak "\s"
\S Vrátí shodu, pokud řetězec NEobsahuje bílý znak "\S"
\w Vrátí shodu, pokud řetězec obsahuje jakékoli znaky slov (znaky od a do Z, číslice od 0-9 a podtržítko _) "\w"
\W Vrátí shodu, pokud řetězec NEobsahuje žádné znaky slov "\W"
\Z Vrátí shodu, pokud jsou určené znaky na konci řetězce "Spain\Z"

V Pythonu předpona "r" před řetězcem označuje, že se jedná o nezpracovaný řetězcový literál. To znamená, že zpětná lomítka v řetězci nejsou považována za znaky escape.

Funkce re.match() hledá shodu s regulárním výrazem od začátku řetězce.

import re


txt1 = "Hello world!"

re.match(r"He..o", txt1) # match hledá od začátku řetězce
# <re.Match object; span=(0, 5), match='Hello'>

re.match(r"He\w\wo", txt1)  # \w je císlo 0-9 nebo písmeno a-Z a _ (podtržítko)
# <re.Match object; span=(0, 5), match='Hello'> 

re.match(r"wo..d", txt1) is None  # . je libovolný znak, pro tečku používáme \.
# True

Funkce re.search() hledá shodu s regulárním výrazem na libovolném místě v řetězci.

import re


txt1 = "Hello world!"

re.search(r"wo..d", txt1) # search hledá kdekoliv v řetězci
# <re.Match object; span=(6, 11), match='world'>

re.search(r"^He..o", txt1) # ^ začátek řetězce
# <re.Match object; span=(0, 5), match='Hello'>

re.search(r"^wo..d", txt1) is None
# True

re.search(r"wo..d!$", txt1) # $ konec řerězce
# <re.Match object; span=(6, 12), match='world!'>

re.search(r"He.*o", txt1) # * libovolný počet výskytů
# <re.Match object; span=(0, 8), match='Hello wo'>

re.search(r"world!( Hello!)?",txt1) # ? nula výskytů nebo jeden výskyt
# <re.Match object; span=(6, 12), match='world!'>

re.search(r"He.*?o", txt1) # .*? vypne greedy přístup pro *
# <re.Match object; span=(0, 5), match='Hello'>

Funkce re.findall() vrací seznam všech podřetězců, které mají shodu s regulárním výrazem.

import re


txt2 = "Hello world! Halloween 2024!"

# findall najde všechny výskyty daného řetězce
re.findall(r"[aiueo]", txt2) #[] označuje množinu 
# ['e', 'o', 'o', 'a', 'o', 'e', 'e']

re.findall(r"\d", txt2) # \d označuje jeden znak
# ['2', '0', '2', '4']

re.findall(r"[aiueo]{2}", txt2) #{} počet výskytů
# ['ee']

re.findall(r"H[aiueo]{1}ll", txt2)
# ['Hell', 'Hall']

re.findall(r"l[^aiueo].", txt2)
#['llo', 'ld!', 'llo']

Všechny funkce modulu RE přijímají volitelný argument flags, který umožňuje různé unikátní funkce a varianty syntaxe.

Například přidáním vlajky re.IGNORECASE jako argumentu metody search povolíte vyhledávání bez ohledu na velká a malá písmena, nebo re.MULTILINE, která mění sémantiku ^ a & na začátek a konec řádku.

import re
  

text = "Contact us at info@example.com or Support@Example.org."
pattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b" #\b znaky, které bývají na začátku a konci slova
re.findall(pattern, text, re.IGNORECASE)
# ['info@example.com', 'Support@Example.org']

Funkce re.sub() narahzuje všechny shody jiným podřetězcem, a re.split() rozdělí řetězec podle každé shody.

Capture groups v regulárních výrazech jsou části výrazu, které jsou uzavřeny v závorkách (). Slouží k dvěma hlavním účelům:

  1. Seskupení: Umožňují vám zachycovat a označovat skupiny znaků jako jednotnou jednotku. To může být užitečné například pro použití kvantifikátorů (jako je *, +, ?, nebo {}).
  2. Zachycení a uchování textu: Regulární výrazy mohou obsahovat více zachycených skupin. Když se shoda nalezená pomocí regulárního výrazu najde, text, který odpovídá každé zachycené skupině, je uložen pro pozdější použití.
import re


html = """<h3>Seznam přednášek</h3>
<ol>
    <li>Úvod, abeceda, řetězece, jazyk, konečný deterministický automat</li>
    <li>Vlastnosti regulárních jazyků a nedeterminismus</li>
    <li>Determinizace a regulární výrazy</li>
    <li>Další vlastnoti regulárních jazyků a pumping lemma</li>
    <li>Minimalizace</li>
    <li>Zápočtový test</li>
</ol>"""

content = re.search(r"<ol>\s*(.*?)\s*</ol>", html, re.DOTALL) # flag DOTALL způsobí, že . rozpoznává i \n
content
# <re.Match object; span=(26, 338), match='<ol>\n    <li>Úvod, abeceda, řetězece, jazyk, kon>

# () v RE tvoří takzvanou capture group - daný podřetězec se uloží jako do proměnné
content.group(1)
# '<li>Úvod, abeceda, řetězece, jazyk, konečný deterministický automat</li>\n    <li>Vlastnosti regulárních jazyků a nedeterminismus</li> ... <li>Zápočtový test</li>'

# sub nahradí všechny vyskyty výrazu jiným řetězcem
content_mod = re.sub(r"^<li>|</li>$", "", content.group(1))
content_mod
# 'Úvod, abeceda, řetězece, jazyk, konečný deterministický automat</li> ... <li>Zápočtový test'

# split dělí řetězec podle regulárního výrazu
re.split(r"</li>\s*<li>", content_mod)
# ['Úvod, abeceda, řetězece, jazyk, konečný deterministický automat',
#  'Vlastnosti regulárních jazyků a nedeterminismus',
#  'Determinizace a regulární výrazy',
#  'Další vlastnoti regulárních jazyků a pumping lemma',
#  'Minimalizace',
#  'Zápočtový test']

# alternativně lze stejný proces udělat takto pomocí capture grup
re.findall("<li>(.*?)</li>", html)
# ['Úvod, abeceda, řetězece, jazyk, konečný deterministický automat',
#  'Vlastnosti regulárních jazyků a nedeterminismus',
#  'Determinizace a regulární výrazy',
#  'Další vlastnoti regulárních jazyků a pumping lemma',
#  'Minimalizace',
#  'Zápočtový test']

Při nahrazování můžeme použít zpěnté referencování capture group pomocí \g<i>.

import re


html2 = """<h3>Seznam přednášek</h3>
<ol>
    <li>Úvod, abeceda, řetězece, jazyk, konečný deterministický automat (<a href="<?php echo BASE_DIR; ?>/slides/fj01.pdf"><b>pdf</b></a>)</li>
    <li>Vlastnosti regulárních jazyků a nedeterminismus (<a href="<?php echo BASE_DIR; ?>/slides/fj02.pdf"><b>pdf</b></a>)</li>
    <li>Determinizace a regulární výrazy (<a href="<?php echo BASE_DIR; ?>/slides/fj03.pdf"><b>pdf</b></a>)</li>
    <li>Další vlastnoti regulárních jazyků a pumping lemma (<a href="<?php echo BASE_DIR; ?>/slides/fj04.pdf"><b>pdf</b></a>)</li>
    <li>Minimalizace (<a href="<?php echo BASE_DIR; ?>/slides/fj05.pdf"><b>pdf</b></a>)</li></li>
    <li>Zápočtový test</li>
</ol>"""

re.findall(r"<a href=\"(.*?)\">", html2)
# ['<?php echo BASE_DIR; ?>/slides/fj01.pdf',
# '<?php echo BASE_DIR; ?>/slides/fj02.pdf',
# '<?php echo BASE_DIR; ?>/slides/fj03.pdf',
# '<?php echo BASE_DIR; ?>/slides/fj04.pdf',
# '<?php echo BASE_DIR; ?>/slides/fj05.pdf']

# capture grupy jsou přístupné i při nahrazování pomocí \g<i> 
re.sub(r"(<a href=\".*?)(fj.*?\.pdf)\">.*?</a>", "\g<1>\g<2>\">\g<2></a>", html2)
# '<h3>Seznam přednášek</h3>\n<ol>\n    <li>Úvod, abeceda, řetězece, jazyk, konečný deterministický automat (<a href="<?php echo BASE_DIR; ?>/slides/fj01.pdf">fj01.pdf</a>)</li>\n    <li>Vlastnosti regulárních jazyků a nedeterminismus (<a href="<?php echo BASE_DIR; ?>/slides/fj02.pdf">fj02.pdf</a>)</li> ...<li>Zápočtový test</li>\n</ol>'

Všechny capture groups jsou přístupné pomocí .groups().

import re


# část výstupu z příkazu 'top' na mac os
top = """Processes: 631 total, 2 running, 629 sleeping, 3400 threads          00:50:23
Load Avg: 1.35, 1.60, 2.52  CPU usage: 8.87% user, 5.52% sys, 85.59% idle
SharedLibs: 610M resident, 93M data, 26M linkedit.
...
"""

top_re = re.match("^Processes: (\d+) total, (\d+) running, (\d+) sleeping, (\d+) threads", top)
top_re.group(1)
# '631'

# všechny grupy získáme pomocí .groups()
top_re.groups()
# ('631', '2', '629', '3400')

Zápočtové úkoly

Nevíte si rady? Přečtěte si "Jak pracovat s Github Classroom?".

Dobrovolné Úkoly