PPy3/WejścieWyjście

Z Brain-wiki

Obsługa wejścia i wyjścia

Większość sensownych programów służy do przetworzenia jakichś informacji uzyskanych z zewnątrz programu: w najprostszym przypadku, z pliku na dysku, lub z danych wprowadzonych przez użytkownika za pomocą klawiatury. W przypadku programów wywoływanych z linii poleceń, istnieje też poręczny i łatwy do wykorzystania mechanizm by wskazać programowi, co ma robić - za pomocą tzw. opcji i/lub wartości parametrów wpisanych na linii poleceń, po nazwie wywoływanego programu. Często też chcemy, by wyniki działania programu, np. przetworzone dane lub wyniki obliczeń, znalazły się w pliku na dysku - choć czasami możemy woleć, aby również (lub zamiast tego) zostały one wyświetlone na ekranie. Omówimy teraz krótko, jak to osiągnąć.

Pliki tekstowe vs. binarne

Zawartość każdego pliku na dysku to tak naprawdę strumień bajtów - bajt, czyli grupa ośmiu bitów, może być traktowany jako reprezentacja (w zapisie dwójkowym), jakiejś liczby całkowitej dodatniej z zakresu 0-255. Często jednak zawartość ta ma być traktowana jako reprezentacja tekstu, czyli ciągu znaków stosowanych w zapisie jakiegoś języka - może to być język naturalny (polski, angielski, chiński, ...) lub np. język programowania (Python, Java, C++, ...), język tworzenia stron WWW (HTML), itd. W tym celu stworzono tzw. kodowania, czyli standardy reprezentowania znaków stosowanych w systemach zapisu języków naturalnych za pomocą bajtów lub grup bajtów (języki programowania itp. zasadniczo posługują się tymi samymi zestawami znaków, co języki naturalne - a zwłaszcza angielski).

Nie wchodząc za bardzo w szczegóły, obecnie stosowane reprezentacje cyfrowe tekstu oparte są na standardzie Unicode, który m. in. definiuje tzw. uniwersalny zestaw znaków (UCS) - tablicę, zawierającą wszystkie znaki (alfanumeryczne, przestankowe, ideogramy - właściwe dla języków dalekowschodnich, i szeregu innych kategorii) stosowane w systemach pisma wszystkich żywych języków świata (i wielu języków martwych). Wewnętrzna reprezentacja danych napisowych w Pythonie oparta jest na standardzie Unicode - zatem pythonowy napis może zawierać znaki z wszelkiego rodzaju systemów pisma, w dowolnej kombinacji. Zapis tych danych w pliku dyskowym - i odwrotne, zinterpretowanie strumienia bajtów odczytanego z pliku dyskowego jako reprezentacji pewnego napisu - wymaga przyjęcia jakiegoś konkretnego kodowania. Unicode nie stanowi sam w sobie kodowania - określa on repertuar znaków oraz pozycję każdego z nich w tablicy UCS, nie zaś bajt lub grupę bajtów go reprezentującą.

W Pythonie problem ten rozwiązany jest w sposób następujący: domyślnie, zawartość wczytywana z plików interpretowana jest jako dane tekstowe (chyba, że zażyczymy sobie inaczej), zgodnie z pewnym domyślnym kodowaniem, właściwym dla systemu operacyjnego (chyba, że wskażemy że ma być stosowane inne) - w przypadku Linuxa będzie to prawie zawsze kodowanie o nazwie UTF-8, które się dobrze nadaje do zastosowania dla języków zachodnich, posługujących się systemem pisma opartym na alfabecie łacińskim. Inaczej może być w środowiskach języków posługujących się np. cyrylicą, czy języków azjatyckich (pismo arabskie lub ideograficzne - chińskie, japońskie, koreańskie, itd.). W środowiskach Windows może być inaczej...

Z powyższego widać, że rozróżnienie - plik tekstowy czy binarny - jest dość umowne, i dotyczy raczej interpretacji zawartości pliku (plik zapisany zgodnie z nieznanym nam i/lub nieoczekiwanym kodowaniem jest nie do odróżnienia od pliku binarnego). Co więcej, pliki zapisywane przez programy takie, jak MS Word, lub pliki PDF, nawet zawierające wyłącznie tekst, nie są w tym rozumieniu plikami tekstowymi, tylko binarnymi. W przykładach będziemy mieli do czynienia prawie wyłącznie z plikami tekstowymi - warto jednak wiedzieć, że w Pythonie jest osobny typ danych - ciągi bajtów (bytes), o własnościach nieco podobnych do napisów, ale o elementach będących bajtami, a nie znakami pisma.

Czytanie tekstu z pliku

f = open('nazwa_pliku.txt')
for linia in f:
    przetworz(linia)
f.close()

# albo może lepiej:

with open('nazwa_pliku.txt') as f:
    for linia in f:
        przetworz(linia)

# jeszcze inaczej

tresc = open('nazwa_pliku.txt').read() # wczytujemy od razu całą treść pliku
  • Otwarty do odczytu plik tekstowy dopuszcza iterację, w której kolejnymi elementami są linie tekstu - wraz z kończącym je kodem przejścia do nowej linii; w ostatniej linijce pliku kodu tego może brakować (lub nie);
  • Wartość zmiennej linia to w każdym obiegu pętli, treść kolejnej linii tekstu jako napis; zawartość (bajtowa) pliku jest interpretowana jako napis zgodnie z domyślnym kodowaniem systemowym (można to zmienić poprzez dodatkowy parametr wywołania open);
  • Jeżeli zawartość pliku nie jest zgodna z założeniem, że można go interpretować jako tekst w przyjętym kodowaniu, to w trakcie przetwarzania może wystąpić błąd (wyjątek);
  • Po zakończeniu przetwarzania plik należy zamknąć, wywołując metodę close; ewentualnie można to pominąć, jeśli wiemy na pewno że wraz z końcem przetwarzania pliku kończy się cały program - wraz z zakończeniem działania programu otwarte pliki zostaną zamknięte;
  • Druga postać wprowadza nową instrukcję złożoną - blok with; to, jak on dokładnie działa, zależy od typu obiektu do jakiego odwołujemy się po słowie with - jeżeli jest nim otwarty plik, to zostanie on automatycznie zamknięty wraz z końcem bloku;
  • Odwoływanie się do pliku, który już został zamknięty, będzie nieskuteczne; żadne operacje się nie powiodą.
  • Ostatnia wersja jest odrobinę ryzykowna - jeśli plik jest bardzo duży, to na jego treść może nie wystarczyć pamięci RAM, i program się wywróci. W dzisiejszych czasach oznacza to jednak, że rozmiar pliku sięga wielu gigabajtów.

Czytanie dowolnych danych z pliku

f = open('nazwa_pliku', mode='b')
dane = f.read(1024*1024) # wczytujemy 1 MB danych (lub mniej, jeśli tylu już nie ma)
(...)
f.seek(0) # "przewinąć" do początku pliku (lub innej pozycji, względem początku)
(...)
pos = f.tell() # uzyskać aktualną pozycję
(...)
f.close()
  • To, co odczytamy - czyli dane, to będzie łańcuch bajtów;
  • Można odpowiednio wykorzystać blok with, aby uniknąć ręcznego zamykania pliku;
  • Wywołanie read() bez argumentu oznacza: wczytaj wszystko;
  • Jeśli wywołanie read z dodatnim argumentem zwróci łańcuch o długości zero - to dotarliśmy do końca pliku.

Zapis danych do pliku

Aby był możliwy zapis danych do pliku dyskowego, należy go otworzyć w trybie do zapisu:

f = open('nazwa_pliku.txt', 'w')
(...)
f.write(dane)
(...)
f.close()

UWAGA: otwarcie pliku w trybie do zapisu ('w') spowoduje usunięcie poprzedniej zawartości pliku - o ile dotyczy pliku już istniejącego. Jeżeli nazwa odnosi się do pliku jeszcze nieistniejącego, to zostanie on utworzony. Jeżeli chcemy zachować poprzednie dane, dopisując nowe do końca pliku, należy plik otworzyć w trybie 'a' (append).

Domyślnie plik jest otwierany w trybie zapisu tekstu, zgodnie z domyślnym kodowaniem. A więc dane powinny być napisem. Jeżeli chcemy zapisywać surowe bajty, należy użyć trybu 'wb'. Jeżeli chcemy użyć kodowania innego niż domyślne, możemy w funkcji open użyć parametru encoding, w postaci:

f = open('nazwa_pliku.txt', 'w', encoding='cp1250')

w tym przykładzie użyto nazwy kodowania stosowanego do języka polskiego w Windows.

Standardowe strumienie i przekierowania

Wprowadzenie

System operacyjny z rodziny unixów (np. Linux lub MacOS) udostępnia każdemu procesowi (uruchomionemu programowi) trzy tzw. standardowe strumienie, mogące być źródłem danych (tzw. standardowy strumień wejściowy, stdin) lub kanałem przekazywania wyników, komunikatów itp. (standardowy strumień wyjściowy, stdout i standardowy strumień błędów, stderr).

W systemach z rodziny Windows jest podobnie, w odniesieniu do programów uruchamianych z linii poleceń, czyli okna programu cmd.exe.

Co to konkretnie znaczy? Domyślnie, zawartością stdin są dane wprowadzane z klawiatury, natomiast dane wypisywane na stdout i stderr pojawiają się na ekranie - w oknie terminalowym, z którego uruchomiono dany program. Przyjęto umowę, że na stdout wypisuje się dane, stanowiące wynik zgodnej z przewidywaniami pracy programu (oczywiście o ile nie przewidziano zapisu tych wyników bezpośrednio do pliku na dysku), natomiast na stderr - jedynie komunikaty dotyczące sytuacji nieprzewidzianych, a więc błędów przetwarzania, lub ostrzeżenia - pojawiające się w sytuacjach, gdy wprawdzie kontynuacja pracy programu jest możliwa, ale okoliczności wskazują, że jej wyniki mogą być pod jakimś względem wątpliwe.

Powyższe ma sens, ponieważ domyślne powiązania - stdin z klawiaturą, a stdout i stderr z ekranem można zmienić - poprzez mechanizmy tzw. przekierowania. W szczególności, strumienie wyjścia i błędu można rozdzielić, np. przekierowując treść stdout do pliku, a pozostawiając stderr jako związany z ekranem.

W środowisku Python standardowe strumienie dostępne są jako elementy modułu sys: sys.stdin, sys.stdout i sys.stderr. Mają one właściwości plików tekstowych, choć bez możliwości przewijania (seek).

Funkcja print w Pythonie domyślnie wypisuje dane do strumienia stdout; można to zmienić, używając parametru wywołania file, którego wartością powinien być plik otwarty do zapisu w trybie tekstowym.

Sposoby przekierowania

Przekierowanie strumieni dokonuje się na poziomie systemu operacyjnego, całkowicie poza środowiskiem Pythona - program nie ma możliwości wykrycia, czy strumienie standardowe na jakich operuje zostały przekierowane. Przekierowania określa się na linii poleceń w momencie uruchomienia programu.

Przekierowanie stdin z pliku:

$ program.py < plik_wejsciowy.txt
$ < plik_wejsciowy.txt program.py

Oba powyższe polecenia są równoważne i skutkują tym, że dane czytane przez program z sys.stdin będą tak na prawdę pochodzić z pliku o podanej nazwie (a treść wprowadzana z klawiatury będzie w programie niedostępna).

Przekierowanie stdout do pliku:

$ program.py > plik_wyjscia.txt
$ > plik_wyjscia.txt program.py

Dane pisane do sys.stdout nie pojawią się w oknie terminala, tylko znajdą się w pliku. Dane pisane do sys.stderr dalej będą trafiać na ekran.

Przekierowania równoczesne:

$ program.py < plik_wejsciowy.txt > plik_wyjscia.txt

Oba poprzednio opisane przekierowania można również wykonać równocześnie. Elementy polecenia również mogą wystąpić w innym porządku.

Przekierowanie stderr:

$ program.py 2> plik_bledow.txt

Komunikaty o błędach i ostrzeżenia znajdą się w pliku o podanej nazwie, zamiast na ekranie. Analogicznie do 2>, strumieniom stdout i stdin przypisane są numerki 1 i 0 - można więc pisać odpowiednio 1> i 0<, ale tu te wartości numerów strumieni przekierowanych (naprawdę - deskryptorów plików) są domyślne i niemal zawsze się je pomija.

Potoki

Oprócz przekierowania do i z plików, istnieje jeszcze analogiczny mechanizm, gdzie np. strumień stdout jednego procesu łączy się z strumieniem stdin innego procesu - skutkiem czego dane wyjściowe pierwszego są wprowadzane do drugiego jako dane wejściowe, tworząc to co się nazywa potokiem. Najprostszy przykład:

$ program1.py | program2.py

tu operatorem potoku w systemowej linii poleceń jest znak kreski pionowej (|), a program1.py przekazuje wyniki swojej pracy do dalszego przetwarzania kolejnemu, program2.py. Taki potok może mieć więcej niż dwa etapy i może łączyć się z zastosowaniem przekierowań do plików, np.

$ < plik_wejsciowy.txt program1.py 2>bledy.txt | program2.py > plik_wynikow.txt

Pożytecznym narzędziem do stosowania w potokach jest polecenie (program) o nazwie tee:

$ program.py < dane.txt | tee wynik.txt

w tym przypadku program wczyta zawartość pliku dane.txt, a wynik wypisany do sys.stdout zarówno pojawi się na ekranie (w oknie terminala, jak i zostanie zapisany w pliku wynik.txt.

Odczyt linii poleceń

Zawartość linii poleceń dostępna jest w obiekcie sys.argv - czyli moduł (standardowy) sys, element argv. Jest to lista argumentów - ,,słów" jakie wywołujący program umieścił w linii poleceń. Inaczej mówiąc, treść linii poleceń ulega wstępnemu rozbiorowi - na słowa, według reguł zależnych od systemu operacyjnego. Najczęściej poszczególne słowa oddzielają spacje (jedna lub więcej), jeśli chcemy, by ciąg zawierający spacje był potraktowany jako pojedyncze słowo (np. nazwa pliku zawierająca spacje), należy ciąg ten np. umieścić w cudzysłowach (które zostaną usunięte z treści argumentu). Treść linii poleceń na ogół ulega jeszcze innym formom obróbki przez system operacyjny (np. rozwijanie rozmaitych skrótów), ale dzieje się to poza kontrolą Pythona.

from sys import argv
for arg in argv:
    print(arg)

Korzystając z powyższego kodu możemy dowiedzieć się, jak wygląda lista argumentów już po jej obróbce przez system operacyjny.

Na początku listy argv czyli w pozycji argv[0] znajduje się nazwa uruchomionego programu (tj. nazwa pliku z kodem uruchomionego jako program główny).

Przykłady

1.

Jeśli przewidujemy, że jedynymi argumentami wywołania naszego programu będą nazwy plików, na każdym z których należy wykonać jakąś czynność, to można to zrealizować tak:

#! /usr/bin/python3

from sys import argv

def przetwarzaj(plik):
    # tu określamy na czym polega "przetworzenie" pliku

for plik in argv[1:]:
    przetwarzaj(plik)
  • Wzięcie wycinku z argv służy pominięciu pliku zawierającego kod programu;
  • Elementami argvnazwy plików - napisy.

2.

Nieraz jest pożądane, aby program przetwarzający strumień danych mógł działać na dwa sposoby: albo wywołany z argumentami będącymi nazwami plików z danymi wejściowymi wczytuje kolejno zawartość tych plików; albo wywołany bez takich argumentów, czyta dane wejściowe ze standardowego strumienia wejściowego, w którym one znajdują się zazwyczaj w wyniku poprzedniego kroku przetwarzania potokowego. Nota bene w ten sposób działa wiele tradycyjnych narzędzi systemowych w Linuxie.

W standardowej bibliotece Pythona jest moduł fileinput, który ułatwia realizację takiego podejścia. W najprostszym przypadku:

#! /usr/bin/python3

import fileinput

def przetwarzaj(linia):
    # tu określamy na czym polega przetworzenie linii treści

for linia in fileinput.input():
    przetwarzaj(linia)

W pętli w powyższym przykładzie wystąpią kolejno wszystkie linie w treści plików wymienionych na linii poleceń (z pominięciem pliku samego programu), lub jeśli linia poleceń jest pusta — linie w treści sys.stdin. Ewentualnie można wywołać funkcję fileinput.input(pliki), tj. zamiast bez argumentów to z argumentem będącym sekwencją nazw plików zawierających dane wejściowe. Może to być np. lista argumentów programu, ale po usunięciu z niej pozycji będących opcjami, a nie nazwami plików. Oczywiście w dokumentacji znajdziemy jeszcze dalsze możliwości.

Ćwiczenia

1. Napisać program, który wczyta dany plik linia po linii i znajdzie wśród nich podzbiór linii o maksymalnej długości (w znakach) oraz wartość tej maksymalnej długości. Niech wynikiem działania programu będzie wypisanie liczby oznaczającej maksymalną długość linii występujących w badanym pliku, a następnie - treść tych linii, w kolejności w jakiej pojawiały się w pliku. Wypróbować ten program na pliku /usr/share/dict/words. W końcowej wersji program powinien operować na pliku, którego nazwę podamy jako argument na linii poleceń.

Wskazówka: długość linii nie powinna uwzględniać kodu przejścia do nowej linii, zapisywanego symbolicznie jako '\n' kończącego (prawie) każdą linię.

2. Napisać program, który wczyta dany plik linia po linii i znajdzie wśród nich podzbiór linii o danej długości w znakach. Niech wynikiem działania programu będzie wypisanie linii o żądanej długości występujących w treści pliku w takiej kolejności, w jakiej pojawiały się w pliku. Długość szukanych linii podajemy jako pierwszy, a nazwę badanego pliku jako drugi argument na linii poleceń.

3. Napisać program, który wyznaczy rozkład długości linii w treści danego pliku - nazwę pliku podajemy na linii poleceń. Wynikiem działania programu jest wypisanie ciągu linijek postaci długość : ile, gdzie liczba ile opisuje, ile linii o danej długości wystąpiło w treści pliku. Pozycje, w których ile wynosiłoby 0 nie pojawiają się.

4. Napisać program, który wyznacza częstości występowania w treści pliku określonych znaków. Zestaw znaków, których wystąpienia zamierzamy zliczać podajemy jako pierwszy argument na linii poleceń, nazwę badanego pliku jako drugi. Wynikiem działania programu jest wypisanie ciągu linijek postaci znak : częstość.

5. Napisać program, który znajduje w pliku /usr/share/dict/words wszystkie słowa, składające się z tych samych liter co słowo, podane jako argument na linii poleceń - choć niekoniecznie występujących tę samą liczbę razy. Inaczej mówiąc, każda z liter występujących w podanym słowie występuje również w słowach szukanych, a nie występują w nich żadne inne znaki. Program wypisuje na ekran znalezione słowa, po jednym na linijkę.

6. Niech program znajduje wszystkie słowa, jakie można utworzyć korzystając z danego zestawu liter (podanego jako pierwszy argument na linii poleceń); tzn. tak jak w Scrabble dysponujemy takimi literami i w takiej liczbie, jakie składają się na to słowo (lub ,,słowo"). Źródłem słów jakie można utworzyć jest plik podany jako drugi argument (domyślnie - /usr/share/dict/words).
Wskazówka: może się tu przydać operacja L.remove(x), gdzie L jest listą; usuwa ona z listy pierwsze z kolei znalezione w niej wystąpienie elementu x.
Uwaga: operacja ta zwróci błąd, jeśli element x nie występuje w L.


poprzednie | strona główna | dalej

RobertJB (dyskusja) 13:26, 4 sie 2016 (CEST)