Textové soubory

Výstup textových dat do souboru

Doposud jsme používali příkaz print za účelem výstupu na konzoli. Často potřebujeme výstup programu uložit do nějakého souboru. Můžeme

  • Výstup programu zkopírovat z konzole myší do textového editoru.

  • Přesměrovat výstup programu do souboru na příkazové řádce pomocí symbolu >

    python program.py > data.txt
    

    Uvidíme, že potom vše, co původně program vypisoval, skončí v souboru data.txt.

  • Modifikovat program tak, aby výstup posílal do souboru. Za tím účelem využijeme argument příkazu print pojmenovaný file. Ten ale musí mít podobu souboru otevřeného pro zápis, nikoli jen jména souboru. Proto se používá konstrukce

[4]:
with open("data.txt","w") as out_file:
  print(f"{1+1=}",file=out_file)
  print(f"{1+2=}",file=out_file)

Zde jsme použili nový příkaz with. Ten je určen k automatickému úklidu důsledků akce, kterou má ve svém (prvním) argumentu. V případě práce se soubory jde o to, že příkaz

my_file = open('data.txt','w')

soubor otevře (např. pro zápis nebo čtení) a ponechá jej za tím účelem otevřený až do okamžiku, než zavoláme operaci

my_file.close()

(v důsledku objektové nátury Pythonu je close metoda třídy soubor a nikoli globální funkce.)

V prostředí jupyter/google colab/ipython pak zůstane soubor otevřen i po vyhodnocení dané buňky. To může být nežádoucí a proto se doporučuje použít příkaz with, který dobu otevření souboru ohlídá.

O tom, že daný soubor exsituje se můžeme přesvědčit příkazem shellu ls (pod linuxem) nebo dir (pod windows). Jaká data soubor obsahuje zjistíme vyspáním jeho obsahu na konsoli příkazem cat (pod linuxem) resp. type (pod windows).

[8]:
!ls  -l data.txt
!echo -------------
!cat data.txt
-rwxr-xr-x 1 ledvinka ledvinka 12 říj 16 14:32 data.txt
-------------
1+1=2
1+2=3

Pod windows by příkazy shellu musely vypadat takto

!dir data.txt
!echo -------------
!type data.txt

a výstup by pak byl např.

 Volume in drive C has no label.
 Volume Serial Number is 8C21-A167

 Directory of c:\Users\student\Documents\programovani

10/16/2023  12:52 PM                14 data.txt
               1 File(s)             14 bytes
               0 Dir(s)  18,584,666,112 bytes free
------------
1+1=2
1+2=3

Pokud se divíte, proč pod linuxem a windows mají soubory různou velikost (12 resp. 14 byte), pak za tím je různá podoba konce řádek na obou platformách ("\n" resp. "\n\r").

Úlohy

  1. Vytvořte programem soubor triangl.txt obsahující text v podobě deseti řádek

                             10
                        8  9 10
                  6  7  8  9 10
            4  5  6  7  8  9 10
      2  3  4  5  6  7  8  9 10
0  1  2  3  4  5  6  7  8  9 10
      2  3  4  5  6  7  8  9 10
            4  5  6  7  8  9 10
                  6  7  8  9 10
                        8  9 10
                             10

Pozn. Nejprve odlaďte program aby vypisoval požadovaný text "na obrazovku", teprve poté přidejde file=soubor.

  1. Alternativně vypište do souboru tabulku dělitenlostí

 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20
 2  .     *     *     *     *     *     *     *     *     *
 3     .        *        *        *        *        *
 4  *     .           *           *           *           *
 5           .              *              *              *
 6  *  *        .                 *                 *
 7                 .                    *
 8  *     *           .                       *
 9     *                 .                          *
10  *        *              .                             *
11                             .
12  *  *  *     *                 .
13                                   .
14  *              *                    .
15     *     *                             .
16  *     *           *                       .
17                                               .
18  *  *        *        *                          .
19                                                     .
20  *     *  *              *                             .

Vstup textových dat ze souboru

Protože naše znalosti nedovolují zatím zpracovat složitější podobu vstupních dat, budeme uvažovat úplně nejjednodušší variantu, kdy na každém řádku vtupu bude jediné číslo. Program pak vypíše rodíl hodnot na posledních dvou řádcích.

V prvním kroku ale musíme nějaká vstupní data pořídit. Abychom se dozvěděli i něco zajímavého z matematiky, data budou mít podobu posloupnosti \(a_k = k(k-1)(k-2)/6\) pro \(k=0,1,2,...n\).

[8]:
n = 13
fn_out = 'posloupnost.txt'

with open(fn_out,'w') as f:
  for k in range(n+1):
    print(k*(k-1)*(k-2)//6, file=f)

Nyní jsme vytvořili soubor posloupnost.txt, který obsahuje čísla \(0, 0, 0, 6, 24, 60, 120, 210, 336, 504, 720, 990, 1320, 1716\) na každém řádku jedno. V interaktivním režimu se o tom můžeme přesvědčit např. příkazem ! head -6 posloupnost.txt, který vypíše prvních 6 řádků tohoto souboru.

Protože se ale učíme číst ze souboru, tuto drobnost si napíšeme

[18]:
fn_in = 'posloupnost.txt'
print_lines = 6

with open(fn_in) as fin:
  for line in fin:
    if print_lines <= 0:
      break

    current_a = int(line)
    print(current_a)
    print_lines = print_lines - 1
0
0
0
1
4
10

Zde otevíráme soubor voláním open(fn_in), přičemž se neobtěžujeme uvést druhý parametr, protože má výchozí hodnotu 'r', tedy otevři soubor pro čtení. Snadno se o tom přesvědčíme zadáním ?open (zkuse si to).

Proměnná print_lines se stará o to, abychom vytiskli nanejvýš požadovaný počet řádek (a kdo to preferuje, může ve stylu C poslední řádek pozměnit na print_lines -= 1).

Protože to použijeme za chvíli, řádek, který v rámci příkazu for načteme do řetězce line, před vypsáním konvertujeme do podoby celého čísla a uložíme do proměnné current_a.

Poznámka: Protože by soubor nemusel zamýšlený počet řádek obsahovat, nezvolili jsme variantu obsahující cyklus for k in range(print_lines). Protože by nyní šlo o celočíselný cyklus, místo příkazů for line in fin
bychom museli použít neporobíranou variantu čtení ze souboru v podobě line = fin.readline().

Nyní můžeme tento krátký kód rozšířit a přidat výpočet rozdílu sousedních řádků. Musíme přidat název výstupního souboru a proměnnou, která si bude pamatovat hodnotu posledního řádku. Tu před započetím cyklu vhodně inicializujeme.

[10]:
fn_in = 'posloupnost.txt'
fn_out = 'diference1.txt'

previous_a = None

with open(fn_in) as fin:
  with open(fn_out,'w') as fout:
    for line in fin:
      current_a = int(line)
      if previous_a is not None:
        print(current_a-previous_a, file=fout)
      previous_a = current_a


Provedením toho kódu jsme vytvořili soubor diference1.txt, který obsahuje čísla \(0, 0, 6, 18, 36, 60, 90, 126, 168, 216, 270, 330, 396\), jak bychom zjistili provedením příkazu shellu !cat diference1.txt.

Nás bude zajímat, co by stalo, kdybychom postup opakovali. Máme více možností. Jedna spočívá v tom, že program výše spustíme s fn_in = 'diference1.txt' a vhodně pozměněným fn_out.

Další možností je přímo napsat program, který počítá a vypisuje tyto druhé diference. To je čtenáři ponecháno jako cvičení.

Konečně je tu možnost, která představuje zajímavou metodu práce s programy na příkazové rádce a kterou si nyní vyzkoušíme.

Přesměrování vstupu a výstupu programu

Když programy místo z buněk Jupyterového sešitu spouštíme z příkazové řádky, jsme zvyklí, že se dočkáme textového výstupu, který se následně objeví na konsoli. Například

tail -2 posloupnost.txt
220
286

zachyujce situaci, kdy na příkazové řádce spustíme program tail a požádáme jej, aby nám vypsal poslední dva řádky souboru posloupnost.txt.

Pokud příkaz pozměníme na

tail -2 posloupnost.txt > poslednidva.txt

místo na obrazovce skončí tyto dvě čísla v souboru poslednidva.txt.

Podobně se chová na příkazové řádce znak <, který podstrčí obsah souboru spouštěnému programu, jako by to byl vztup z klávesnice. V našem případě budeme chtít provést následující příkazy

python3 dif.py < posloupnost.txt > diference1.txt
python3 dif.py < diference1.txt

První příkaz spustí z pomoci interpretu jazyka python program obsažený v souboru dif.py, který spočte to, co jsme již dokázali: načte soubor posloupnost.txt, spočte první diference a pošle je do souboru diference1.txt. Druhý příkaz pak vezme diference1.txt a vypíše první dirence dat obsažených v tomto souboru.

Za tím účelem musíme vytvořit soubor dif.py obsahující mírně pozměněnou verzi kódu výše:

import os

previous_a = None

for line in os.stdin:
  current_a = int(line)
  if previous_a is not None:
    print(current_a-previous_a)
  previous_a = current_a

Nejvýraznější změnou je absence obou příkazů open. Je to tím, že program vypisuje ppomocí základní varianty příkazu print bez zpecifikace výstupního souboru a čte z tzv. standarního vstupu. Tím by za normálních okolností, kdybychom nepoužívali přesměrování pomocí < byla klávesnice.

Je vhodné zde zmínit základní fakt související se spouštěním programů v příkazové řádce. Každý program dostane při svém spuštění tři otevřené textové soubory - stdout, který je otevřen pro zápis. To, co do něj program zapíše se objeví na obrazovce na řádcích následujcících spuštěné programu. V pythonu tam končí výtup příkazu print s výchozí hodnotou nepovinného parametru file=....

Pokud jsme použili přesměrování > vyst_soubor.txt, je před spuštěním programu vytvořen prázdný soubor vyst_soubor.txt a výtup programu končí v něm. Po dobu běhu programu obsahuje vyst_soubor.txt částečnou podobu výstupu spušťeného programu, po skončení běhu programu je vyst_soubor.txt uzavřen a obsahuje vše, co program poslal na std_out. - stdin, který je otevřen pro čtení a skončí v něm to, co píšeme na klávesnici. Pokud používáme přesměrování < vst_soubor.txt, přicházejí místo klávesnice data z tohoto souboru.

Vstupní soubor mívá konečnou délku. Pokud probíhá vstup z klávesnice, existuje speciální kombinace kláves (ctrl-Z pod windows, jinde ctrl-D), která má význam konce vstupu. - stderr, který je otevřen pro zápis a to, co do tohotou souboru program pošle se také objeví na obrazovce. Důvod k existenci tohoto kanálu je, že i pokud přesměrujeme stdout do souboru pomocí >, chybová hlášení se stále objevují na obrzovce.

Tolik teorie. Abychom si to mohli vyzkoušet potřebujeme mít zdrojvý text v souboru dif.py. Protože pracujeme ve virtuálních sezeních prostředí Jupyter, začínáme s prázným adreářem a všechny soubory si musíme vytvořit. Soubor posloupnost.txt jsme vytvořili spuštěním kódu výše. Sobor dif.py vyvoříme přesměrováním příkazu shellu echo -e "text1\ntext2", který vypíše dva řádky, na prvním text1 a na druhém text2.

[15]:
!echo -e 'import sys\n\nprevious_a = None\n\nfor line in sys.stdin:\n  current_a = int(line)\n  if previous_a is not None:\n    print(current_a-previous_a)\n  previous_a = current_a' > dif.py
!cat dif.py
import sys

previous_a = None

for line in sys.stdin:
  current_a = int(line)
  if previous_a is not None:
    print(current_a-previous_a)
  previous_a = current_a

Příkazem cat dif.py jsme ověřili, že soubor obsahuje zamýšleý kód. Znak vykřičník, jak víme, zařídí, že tyto příkazy nejsou podtrčeny interpertu Pythonu, ale shellu systému, pod kterým běží Jupyter.

Nyní již můžeme spočíst druhé diference:

[12]:
!python3 dif.py < posloupnost.txt > diference1.txt
!python3 dif.py < diference1.txt
0
1
2
3
4
5
6
7
8
9
10
11

Cvičení: V úvodním programu jsme použili celočíselné dělení, což vůbec nevadilo, protože výraz \(k(k-1)(k-2)\) je pro celé \(k\) dělitelný šesti.

Změňte // na /. Přesvědčte se, že nyní dojde při konverzi int(line) k chybě. Prohlédněte si soubor posloupnost.txt a odhalte rozdíl oproti použití operace /. Vyzkoušejte, že konverze na typ float, tedy výraz ve tvaru float(line) problém řeší.

Cvičení: zkuste postup zopakovat pro jiný polynom třetího stupně, než je \(k(k-1)(k-2)/6\) a přesvědčte se, že opět tvoří druhé diference aritmetickou řadu.

Cvičení: Přesvědčte se, že čtvrté diference polynomu třetího stupně jsou nulové. Za tím účelem prověďte čtyři příkazy shellu, které vytvořet kromě souboru diference1.txt také diference2.txt a diference3.txt aby nakonec vypsaly čtvré diference.

Cvičení: Program dif.py lze spusti místo v shellu Jupyteru na příkazové řádce vašeho počítače. Vyzkoušejte, že počítá diference čísel zadaných z klávesnice místo z přesměrovaného souboru a že když zadáte poslední řádek vstupu, lze o tom program informovat klávesami ctrl-Z pod windows nebo jinde ctrl-D.

Zřetězení programů v příkazové řádce (pipe)

V posledním cvičení se provede několik příkazů, které postupně vytvářejí pomocné soubory (konkrétně diference1.txt také diference2.txt a diference3.txt). Protože nás tyhle mezivýsledky nezajímají, můžeme využít vlastností shellu, ketrý dovoluje zřetezit programy tak, že výstup jednoho se tává vstupem druhého. Nejprve standardní ukázka:

[13]:
! head -4 posloupnost.txt | tail -2
0
1

Protože soubor posloupnost.txt obsahuje řádky \(0, 0, 0, 1, 4, 10, ...\), když příkaz head -4 vypíše jeho první čtyři řádky, jsou to tyto \(0, 0, 0, 1\). Použitím přesměrovnání, které se způsobí spojením příkazů symbolem |, se tyto čtyři řádky stanou vstupem příkazu tail -2. Jeho je výstup, tedy poslední dva řádky jeho vstupu, jsou pochopiteně \(0, 1\).

Nyní je zřejmé, že druhé diference dat v souboru posloupnost.txt můžeme spočíst i bez použití mezisouboru diference1.txt takto:

[ ]:
!python3 dif.py < posloupnost.txt | python3 dif.py

Cvičení: Vyzkoušejte takto spočíst i třetí a čtvrté diference.

Shrnutí: Vstup a výstup dat z programu

Data, která program produkuje mohou podle potřeby skončit na obrazovce nebo v souboru. Do souboru je můžeme poslat přesměrováním výstupu na příkazové řádce nebo přímo v programu siubor vytvořit a psát do něj variantou příkazu print(..., file=...).

Čtení dat může také probíhat přímo ze souboru, z konzole nebo z přesměrovaného vstupu. Je však komplikováno tím, že načtená data mají podobu řetězců. Je na řádku více jak jeden číselný údaj, musíme se naučit takový řetězec rozebrat na jednotlivé údaje a ty pak konvertovat na čísla. To se naučíme až za nějaký čas.

Více programů můžeme spustit tak, že výstup jednoho se stane vstupem druhého.

[ ]:

[19]:
!tail -2 posloupnost.txt

220
286
[ ]: