Kup książkę w wersji drukowanej:

Pliki

Pamięć nieulotna

Do tej pory uczyliśmy się pisać programy i komunikować procesorowi nasze zamiary za pomocą instrukcji warunkowych, funkcji i iteracji. Nauczyliśmy się, jak tworzyć i wykorzystywać struktury danych w pamięci głównej. Procesor i pamięć główna są miejscem, w którym działa i uruchamia się nasze oprogramowanie. To właśnie tam odbywa się całe “myślenie”.

Przypomnij sobie naszą dyskusję na temat architektury sprzętowej. Po wyłączeniu zasilania wszystko, co jest zapisane na procesorze lub w pamięci głównej, zostanie usunięte. Tak więc do tej pory nasze programy były tylko przejściowymi, zabawowymi ćwiczeniami do nauki Pythona.

Pamięć pomocnicza

W tym rozdziale rozpoczynamy pracę z pamięcią pomocniczą (a dokładniej, to z plikami). Pamięć pomocnicza nie jest czyszczona po wyłączeniu zasilania. W przypadku pendrive’a dane, które zapisujemy z naszych programów, mogą zostać usunięte z systemu i przeniesione do innego systemu.

Skupimy się przede wszystkim na odczycie i zapisie plików tekstowych, takich jak te, które tworzymy w edytorze tekstu. Później zobaczymy, jak pracować z plikami bazodanowymi, które są plikami binarnymi, przeznaczonymi specjalnie do odczytu i zapisu przez oprogramowanie bazodanowe.

Otwieranie plików

Kiedy chcemy odczytać lub zapisać plik (np. na dysku twardym), musimy najpierw otworzyć plik. Otwarcie pliku komunikuje się z Twoim systemem operacyjnym, który wie, gdzie przechowywane są dane dla każdego pliku. Gdy otwierasz plik, prosisz system operacyjny o znalezienie go po nazwie i o upewnienie się, że ten plik istnieje. W poniższym przykładzie otwieramy plik mbox.txt, który powinien być przechowywany w tym samym katalogu, w którym znajdujesz się po uruchomieniu Pythona. Możesz pobrać ten plik z https://py4e.pl/code3/mbox.txt.

>>> fhand = open('mbox.txt')
>>> print(fhand)
<_io.TextIOWrapper name='mbox.txt' mode='r' encoding='cp1252'>

Jeśli funkcja open() się powiedzie, system operacyjny zwróci nam tzw. uchwyt pliku (ang. file handle). Uchwyt pliku nie jest rzeczywistymi danymi zawartymi w tym pliku, lecz – trzymając się jednego z możliwych tłumaczeń angielskiego słowa handle – klamką do drzwi, za którymi znajdują się interesujące nas dane. Uchwyt jest swego rodzaju przedmiotem, który możemy użyć do odczytania danych. Podczas próby otwarcia pliku do odczytu otrzymasz uchwyt wtedy, gdy żądany plik istnieje i masz odpowiednie uprawnienia do jego odczytania.

Uchwyt pliku

Jeśli plik nie istnieje, open() się nie powiedzie, a Ty nie będziesz mógł uzyskać dostępu do jego zawartości:

>>> fhand = open('stuff.txt')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'stuff.txt'

Później użyjemy try i except, tak aby zgrabniej poradzić sobie z sytuacją, w której próbujemy otworzyć nieistniejący plik.

Pliki tekstowe i linie

Plik tekstowy może być uważany za sekwencję linii, podobnie jak napis w Pythonie może być uważany za sekwencję liter, liczb i symboli. Na przykład poniżej znajduje się fragment pliku tekstowego, który rejestruje aktywność mailową różnych osób w zespole rozwijającym projekt otwartego oprogramowania:

From stephen.marquard@uct.ac.za Sat Jan  5 09:14:16 2008
Return-Path: <postmaster@collab.sakaiproject.org>
Date: Sat, 5 Jan 2008 09:12:18 -0500
To: source@collab.sakaiproject.org
From: stephen.marquard@uct.ac.za
Subject: [sakai] svn commit: r39772 - content/branches/
Details: http://source.sakaiproject.org/viewsvn/?view=rev&rev=39772
...

Cały plik interakcji mailowych jest dostępny pod adresem:

https://py4e.pl/code3/mbox.txt

a skrócona wersja pliku, pod adresem:

https://py4e.pl/code3/mbox-short.txt

Pliki te są w standardowym formacie pliku tekstowego, który zawiera wiele wiadomości e-mail. Wiersze rozpoczynające się od “From” oddzielają wiadomości, a wiersze rozpoczynające się od “From:” są częścią wiadomości. Więcej informacji na temat formatu Mbox można znaleźć na stronie https://pl.wikipedia.org/wiki/Mbox.

Aby podzielić plik na linie, istnieje specjalny znak, który reprezentuje “koniec linii”, zwany nomen omen znakiem końca linii.

W napisach w języku Python, znak końca linii reprezentujemy jako lewy ukośnik-n. Nawet jeśli wygląda to jak dwa znaki, to w rzeczywistości jest to pojedynczy znak. W poniższym przykładzie, gdy patrzymy na zmienną stuff, po wpisaniu jej w interpreterze, pokazuje nam ona \n w napisie, ale gdy używamy print() do wyświetlenia napisu, widzimy go rozbitego na dwie linie poprzez znak końca linii.

>>> stuff = 'Witaj\nświecie!'
>>> stuff
'Witaj\nświecie!'
>>> print(stuff)
Witaj
świecie!
>>> stuff = 'X\nY'
>>> print(stuff)
X
Y
>>> len(stuff)
3

Widzimy też, że długość napisu X\nY to trzy znaki, ponieważ znak końca linii jest pojedynczym znakiem.

Kiedy więc patrzymy na linie w pliku, musimy sobie wyobrazić, że na końcu każdej linii znajduje się specjalny niewidoczny znak zwany znakiem końca linii.

Tak więc znak końca linii dzieli znaki w pliku na linie.

Czytanie plików

Podczas gdy uchwyt pliku nie zawiera danych pliku, to całkiem łatwo jest skonstruować pętlę for do jego odczytu i zliczyć występujące w nim linie:

fhand = open('mbox-short.txt')
count = 0
for line in fhand:
    count = count + 1
print('Liczba linii:', count)

# Kod źródłowy: https://py4e.pl/code3/open.py

W naszej pętli for możemy użyć uchwytu pliku jako sekwencji. Nasza pętla for po prostu zlicza linie w pliku i wyświetla sumę. Tłumaczenie treści pętli for na polski jest z grubsza następujące: “dla każdej linii w pliku reprezentowanej przez uchwyt pliku, dodaj jeden do zmiennej count”.

Powodem, dla którego funkcja open() nie odczytuje całego pliku jest to, że plik może być dość duży i zawierać wiele gigabajtów danych. Funkcja open() zajmuje zawsze tyle samo czasu, niezależnie od wielkości pliku. Pętla for w rzeczywistości powoduje, że dane są odczytywane z pliku.

Gdy plik jest odczytywany w ten sposób przy użyciu pętli for, Python zajmuje się podziałem danych z pliku na osobne linie przy użyciu znaku końca linii. Python w każdej iteracji pętli for czyta każdą linię od początku aż do wystąpienia znaku końca linii i dołącza go jako ostatni znak w zmiennej line.

Ponieważ pętla for odczytuje z danych jeden wiersz na raz, może efektywnie czytać i zliczać wiersze w bardzo dużych plikach, bez obawy, że w pamięci głównej skończy się miejsce na dane. Powyższy program przy użyciu bardzo małej ilości pamięci może zliczać linie w plikach o dowolnym rozmiarze, ponieważ każda linia jest odczytywana, zliczana, a następnie porzucana.

Jeśli wiesz, że plik jest stosunkowo mały w porównaniu z rozmiarem Twojej pamięci głównej, możesz odczytać cały plik za jednym zamachem, używając metody read() na uchwycie pliku.

>>> fhand = open('mbox-short.txt')
>>> inp = fhand.read()
>>> print(len(inp))
94626
>>> print(inp[:20])
From stephen.marquar

W powyższym przykładzie cała zawartość (wszystkie 94 626 znaków) pliku mbox-short.txt jest wczytywana bezpośrednio do zmiennej inp. Do wyświetlenia pierwszych 20 znaków z napisu przechowywanego w zmiennej inp używamy operacji wycinana podciągu znaków, czyli wycinków.

Gdy plik zostanie odczytany w ten sposób, to wszystkie znaki, łącznie ze wszystkimi liniami i znakami końca linii, są jednym dużym napisem znajdującym się w zmiennej inp. Dobrym pomysłem jest przechowywanie wyniku funkcji read() jako zmiennej, ponieważ kolejne wywołanie read() na tym samym uchwycie nie zwróci nam już danych (podczas pierwszego odczytania został osiągnięty koniec pliku, więc kolejne wywołanie tej metody zwraca pusty napis):

>>> fhand = open('mbox-short.txt')
>>> print(len(fhand.read()))
94626
>>> print(len(fhand.read()))
0

Pamiętaj, że ta forma funkcji open() powinna być używana tylko wtedy, gdy dane pliku zmieszczą się spokojnie w pamięci głównej komputera. Jeśli plik jest zbyt duży, by zmieścić się w pamięci głównej, powinieneś napisać swój program tak, aby odczytywać plik po kawałku, używając pętli for lub while.

Przeszukiwanie pliku

Kiedy przeszukujesz dane w pliku, bardzo powszechnym sposobem jest odczytywanie ich, ignorując przy tym większość linii i przetwarzając tylko te, które spełniają określony warunek – patrząc na to ogólniej, nazywamy to schematem filtrowania. Możemy połączyć przebieg odczytywania pliku z metodami tekstowego typu danych, tak aby zbudować proste mechanizmy wyszukiwania.

Na przykład, jeśli chcielibyśmy odczytać plik i wypisać tylko te linie, które rozpoczęły się od “From:”, moglibyśmy użyć metody tekstowego typu danych startswith(), tak aby wybrać tylko te, które mają pożądany początek:

fhand = open('mbox-short.txt')
for line in fhand:
    if line.startswith('From:'):
        print(line)

# Kod źródłowy: https://py4e.pl/code3/search1.py

Gdy powyższy program zostanie uruchomiony, otrzymamy następujący wynik:

From: stephen.marquard@uct.ac.za

From: louis@media.berkeley.edu

From: zqian@umich.edu

From: rjlowe@iupui.edu
...

Wynik wygląda świetnie, ponieważ jedyne linie, które widzimy, to te, które zaczynają się od “From:”, ale dlaczego widzimy dodatkowe puste linie? Jest to spowodowane wspomnianym wcześniej niewidocznym znakiem końca linii. Każda z linii kończy się wspomnianym znakiem, więc funkcja print() wypisuje tekst zapisany w zmiennej line, który zawiera znak końca linii, a następnie dodaje kolejny znak końca linii, co daje efekt podwójnego odstępu, który widzimy.

Moglibyśmy użyć wycinka linii, aby wypisać wszystkie znaki oprócz ostatniego, ale prostszym podejściem jest użycie metody rstrip(), która usuwa spacje z prawej strony napisu:

fhand = open('mbox-short.txt')
for line in fhand:
    line = line.rstrip()
    if line.startswith('From:'):
        print(line)

# Kod źródłowy: https://py4e.pl/code3/search2.py

Po uruchomieniu tego programu otrzymamy następujący wynik:

From: stephen.marquard@uct.ac.za
From: louis@media.berkeley.edu
From: zqian@umich.edu
From: rjlowe@iupui.edu
From: zqian@umich.edu
From: rjlowe@iupui.edu
From: cwen@iupui.edu
...

W miarę jak Twoje programy przetwarzające pliki będą stawać się coraz bardziej skomplikowane, możesz chcieć uporządkować swoje pętle wyszukiwania za pomocą continue. Podstawową ideą pętli wyszukiwania jest to, że szukasz “interesujących” linii i skutecznie pomijasz “nieciekawe”. A gdy znajdujesz interesującą linię, to coś z nią robisz.

Pętlę możemy uporządkować tak, aby w następujący sposób trzymała się schematu pomijania “nieciekawych” linii:

fhand = open('mbox-short.txt')
for line in fhand:
    line = line.rstrip()
    # Pomiń 'nieciekawe' linie
    if not line.startswith('From:'):
        continue
    # Przetwarzaj 'interesujące' linie
    print(line)

# Kod źródłowy: https://py4e.pl/code3/search3.py

Wynik programu jest taki sam. Mówiąc po ludzku, “nieciekawe” są te linie, które nie rozpoczynają się od “From:”, więc pomijamy je, używając continue. W przypadku “interesujących” linii (tzn. tych, które zaczynają się od “From:”) wykonujemy przetwarzanie danych, czyli wypisujemy ich zawartość na ekran.

Możemy użyć metody tekstowego typu danych find(), tak aby zasymulować wyszukiwanie w edytorze tekstu znajdujące linie, w których poszukiwany tekst jest gdziekolwiek w linii. find() szuka wystąpienia napisu w innym napisie i albo zwraca jego pozycję, albo -1 (jeśli nie zostanie on znaleziony), więc możemy napisać następującą pętlę, tak by pokazać te linie, które zawierają tekst “@uct.ac.za” (tzn. pochodzą z Uniwersytetu w Kapsztadzie w RPA):

fhand = open('mbox-short.txt')
for line in fhand:
    line = line.rstrip()
    if line.find('@uct.ac.za') == -1: continue
    print(line)

# Kod źródłowy: https://py4e.pl/code3/search4.py

Otrzymujemy następujący wynik:

From stephen.marquard@uct.ac.za Sat Jan  5 09:14:16 2008
X-Authentication-Warning: set sender to stephen.marquard@uct.ac.za using -f
From: stephen.marquard@uct.ac.za
Author: stephen.marquard@uct.ac.za
From david.horwitz@uct.ac.za Fri Jan  4 07:02:32 2008
X-Authentication-Warning: set sender to david.horwitz@uct.ac.za using -f
From: david.horwitz@uct.ac.za
Author: david.horwitz@uct.ac.za
...

Używamy tutaj skróconej formy instrukcji if – umieściliśmy continue w tej samej linii co if. Ta skrócona forma if funkcjonuje tak samo, jak gdyby continue znajdowało się w następnej linii i było wcięte.

Pozwolenie użytkownikowi na wybranie nazwy pliku

Naprawdę nie chcemy edytować naszego kodu Pythona za każdym razem, gdy będziemy przetwarzać nowy plik. Bardziej użyteczne byłoby poproszenie użytkownika o wprowadzenie nazwy pliku za każdym razem, gdy program zostanie uruchomiony, tak aby mógł on korzystać z naszego programu na różnych plikach, bez konieczności zmiany kodu Pythona.

Jest to dość proste do zrobienia poprzez odczytanie nazwy pliku od użytkownika za pomocą input() w następujący sposób:

fname = input('Podaj nazwę pliku: ')
fhand = open(fname)
count = 0
for line in fhand:
    if line.startswith('Subject:'):
        count = count + 1
print('Mamy', count, 'linii z tematem wiadomości w pliku', fname)

# Kod źródłowy: https://py4e.pl/code3/search6.py

Odczytujemy nazwę pliku wprowadzoną przez użytkownika, umieszczamy ją w zmiennej o nazwie fname i otwieramy ten plik. Teraz możemy uruchamiać program wielokrotnie na różnych plikach.

Podaj nazwę pliku: mbox.txt
Mamy 1797 linii z tematem wiadomości w pliku mbox.txt
Podaj nazwę pliku: mbox-short.txt
Mamy 27 linii z tematem wiadomości w pliku mbox-short.txt

Zanim przejdziemy do następnej sekcji, spójrzmy na powyższy program i zadajmy sobie pytanie: “Co tu się może nie udać?” lub “Co mógłby zrobić nasz miły użytkownik, żeby nasz mały program zakończył się błędem z komunikatem mechanizmu traceback, co sprawiłoby, że wyjdziemy na kiepskich programistów?”.

Używanie try, except i open()

Mówiłem Ci, żebyś nie podglądał. To Twoja ostatnia szansa.

No dobrze. A co, jeśli nasz użytkownik wpisze coś, co nie jest nazwą pliku?

Podaj nazwę pliku: missing.txt
Traceback (most recent call last):
  File "search6.py", line 2, in <module>
    fhand = open(fname)
FileNotFoundError: [Errno 2] No such file or directory: 'missing.txt'
Podaj nazwę pliku: programista 15k
Traceback (most recent call last):
  File "search6.py", line 2, in <module>
    fhand = open(fname)
FileNotFoundError: [Errno 2] No such file or directory: 'programista 15k'

Użytkownicy czasem robią wszystko, co w ich mocy, by wysypać Twoje programy – albo przypadkiem, albo w złych zamiarach. W rzeczywistości ważną częścią każdego zespołu zajmującego się tworzeniem oprogramowania jest osoba lub grupa o nazwie Quality Assurance (w skrócie QA; zespół zapewniania jakości), której zadaniem jest robienie najbardziej szalonych rzeczy w celu wysypania oprogramowania stworzonego przez programistów.

Zespół QA jest odpowiedzialny za znalezienie wad w programie, zanim dostarczymy go użytkownikom końcowym, którzy mogą go z kolei zakupić lub zapłacić nam za jego napisanie. Tak więc zespół QA jest najlepszym przyjacielem programisty.

Teraz, gdy widzimy wadę w programie, możemy ją elegancko naprawić, używając try/except. Musimy przyjąć, że open() może się nie powieść, więc dodamy kod naprawczy, który zadziała dokładnie w takim momencie:

fname = input('Podaj nazwę pliku: ')
try:
    fhand = open(fname)
except:
    print('Nie można otworzyć pliku:', fname)
    exit()
count = 0
for line in fhand:
    if line.startswith('Subject:'):
        count = count + 1
print('Mamy', count, 'linii z tematem wiadomości w pliku', fname)

# Kod źródłowy: https://py4e.pl/code3/search7.py

Funkcja exit() kończy program. Jest to funkcja, którą wywołujemy, ale która nigdy nie wraca do miejsca wywołania. Teraz, gdy nasz użytkownik (lub zespół QA) wpisze jakieś głupie lub złe nazwy plików, “łapiemy” je i elegancko przywracamy normalne działanie:

Podaj nazwę pliku: mbox.txt
Mamy 1797 linii z tematem wiadomości w pliku mbox.txt
Podaj nazwę pliku: szurum-burum
Nie można otworzyć pliku: szurum-burum

Ochrona wywołania open() jest dobrym przykładem prawidłowego użycia try i except w programie Pythona. W języku angielskim używamy terminu pythonic (“pythonowy”) wtedy, gdy robimy coś “w stylu Pythona”. Możemy powiedzieć, że powyższy przykład jest “pythonowym” sposobem na otwarcie pliku.

Gdy będziesz już bardziej biegły w Pythonie, będziesz mógł brać udział w błyskotliwej wymianie zdań z innymi programistami Pythona, tak aby zdecydować, które z dwóch równoważnych rozwiązań problemu jest “bardziej pythonowe”. Bycie “bardziej pythonowym” wynika z idei, że programowanie jest zarówno inżynierią, jak i sztuką. Nie zawsze jesteśmy zainteresowani tym, aby coś po prostu działało; chcemy również, by nasze rozwiązanie było eleganckie, co zauważą i docenią inni, równi nam programiści.

Pisanie do plików

Aby móc zapisywać dane do pliku, musisz otworzyć go w trybie w (skrót z ang. write), podanym jako drugi argument:

>>> fout = open('output.txt', 'w')
>>> print(fout)
<_io.TextIOWrapper name='output.txt' mode='w' encoding='cp1252'>

Jeśli plik już istnieje, otwarcie go w trybie do zapisu usuwa stare dane i tworzy nowy pusty plik, więc bądź ostrożny! Jeśli plik nie istnieje, tworzony jest nowy.

Metoda write() obiektu uchwytu pliku umieszcza dane w pliku, zwracając liczbę zapisanych znaków. Domyślnym trybem zapisu jest tryb tekstowy, przeznaczony do zapisu (i odczytu) napisów. Dodajmy jedną linię do naszego pliku (nie używaj polskich znaków – wrócimy do tego problemu za chwilę):

>>> line1 = "Oto galaz eukaliptusa,\n"
>>> fout.write(line1)
23

Obiekt pliku śledzi, gdzie się obecnie znajduje, więc jeśli ponownie wywołasz metodę write(), to nowe dane zostaną umieszczone na końcu pliku.

Podczas zapisu do pliku musimy być pewni, że ogarniamy znaki końca linii – tutaj wyraźnie wstawiamy znak końca linii, gdy tę linię chcemy zakończyć. Funkcja print() automatycznie dołącza znak nowej linii, ale metoda write() – nie.

>>> line2 = 'ojczyzny naszej emblemat.\n'
>>> fout.write(line2)
26

Kiedy skończysz pisać, musisz zamknąć plik, aby upewnić się, że ostatni bit danych jest fizycznie zapisany na dysku i aby nie został utracony w przypadku wyłączenia zasilania komputera.

>>> fout.close()

Możemy zamykać również pliki, które otwieramy do odczytu, ale jeśli otwieramy tylko kilka plików, to w zasadzie możemy pozwolić sobie na odrobinę nieuwagi. Python sprawi, że po zakończeniu programu wszystkie otwarte pliki zostaną zamknięte. Natomiast gdy zapisujemy dane do plików, lepiej osobiście zadbać o ich zamknięcie, tak aby niczego nie pozostawić przypadkowi.

Kodowanie plików

Przechowując dane tekstowe w plikach, należy określić, ile bitów potrzeba na zapisanie jednego znaku. Przez ostatnie lata zostało wypracowanych wiele systemów kodowania, np. klasyczny siedmiobitowy ASCII1, obecnie będący standardem UTF-82, popularny w naszej części Europy ISO-8859-23 (znany również jako Latin-2) oraz CP-1250 (znany również jako Windows-12504). Często informacja o kodowaniu pliku nie jest dostępna, a niektóre programy przetwarzające dane z góry zakładają jakieś kodowanie (np. UTF-8). W takich sytuacjach przetwarzanie pliku wejściowego w niepoprawnym kodowaniu da nam złe wyniki. W konsekwencji, pewną trudność może sprawić obsługa znaków diakrytycznych podczas odczytu i zapisu do pliku.

Dla przykładu użyjemy pliku polski.txt, który jest do pobrania z adresu https://py4e.pl/code3/polski.txt.

Standardowo możemy otworzyć plik bez podawania żadnych argumentów:

>>> fhand = open('polski.txt')

W zależności od systemu operacyjnego możemy zauważyć, że wydrukowanie informacji o zmiennej fhand nieco się różni w parametrze encoding. Np. w angielskiej wersji systemu Windows możemy otrzymać informację o domyślnym kodowaniu CP-1252:

>>> print(fhand)
<_io.TextIOWrapper name='polski.txt' mode='r' encoding='cp1252'>

Z kolei w systemie Linux domyślne kodowanie może być ustawione na UTF-8:

>>> print(fhand)
<_io.TextIOWrapper name='polski.txt' mode='r' encoding='UTF-8'>

Jeśli w takiej sytuacji spróbujemy odczytać i wyświetlić zawartość pliku, to w przypadku powyższej wersji Windowsa otrzymamy tzw. krzaki:

>>> print(fhand.read())
Nikt nie spodziewa się Hiszpańskiej Inkwizycji.
Wśród naszych metod są tak różne elementy
jak strach, zaskoczenie... bezwzględna skuteczność
i niemalże fanatyczne oddanie papieżowi...
oraz piękne czerwone mundurki.

W przypadku Linuxa otrzymamy prawidłowy tekst:

>>> print(fhand.read())
Nikt nie spodziewa się Hiszpańskiej Inkwizycji.
Wśród naszych metod są tak różne elementy
jak strach, zaskoczenie... bezwzględna skuteczność
i niemalże fanatyczne oddanie papieżowi...
oraz piękne czerwone mundurki.

Plik polski.txt jest zapisany w kodowaniu UTF-8. To, w jakim kodowaniu Python domyślnie otworzy plik, zależy od systemu operacyjnego. Na szczęście funkcja open() obsługuje parametr o nazwie encoding, w którym możemy podać, w jakim kodowaniu ma zostać otwarty plik. W przypadku Windowsa moglibyśmy otworzyć plik w następujący sposób:

>>> fhand = open('polski.txt', encoding='UTF-8')
>>> print(fhand)
<_io.TextIOWrapper name='polski.txt' mode='r' encoding='UTF-8'>
>>> print(fhand.read())
Nikt nie spodziewa się Hiszpańskiej Inkwizycji.
Wśród naszych metod są tak różne elementy
jak strach, zaskoczenie... bezwzględna skuteczność
i niemalże fanatyczne oddanie papieżowi...
oraz piękne czerwone mundurki.

Próbując zapisać tekst “żółć” pod Windowsem w domyślnym kodowaniu, prawdopodobnie trafimy na błąd kodowania5:

>>> fhand_pl1 = open('proba1.txt', 'w')
>>> print(fhand_pl1)
<_io.TextIOWrapper name='proba1.txt' mode='w' encoding='cp1252'>
>>> fhand_pl1.write("żółć")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Python3\lib\encodings\cp1252.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
UnicodeEncodeError: 'charmap' codec can’t encode character '\u017c'
                    in position 0: character maps to <undefined>
>>> fhand_pl1.close()

Na szczęście możemy użyć parametru encoding:

>>> fhand_pl2 = open('proba2.txt', 'w', encoding='UTF-8')
>>> fhand_pl2.write("żółć")
4
>>> fhand_pl2.close()

Problemy z kodowaniem to szeroki temat i początkującemu programiście może to napsuć wiele krwi. Istnieje wiele sposobów na automatyczne wykrywanie kodowania i konwersji z jednego na drugie. Jednak na potrzeby tego kursu wystarczy, że w razie wątpliwości otworzysz plik np. w programistycznym edytorze tekstu Atom lub edytorze tekstu Notepad++ (informacja o kodowaniu pliku będzie w prawym dolnym rogu ekranu).

Obecnie ogólnie przyjętym standardem kodowania plików jest UTF-8.

Debugowanie

Gdy odczytujesz dane lub zapisujesz dane do plików, możesz mieć problemy z białymi znakami. Tego typu błędy mogą być trudne do debugowania, ponieważ zgrupowania spacji, tabulacje i znaki końca linii są zazwyczaj nieodróżnialne i/lub niewidoczne:

>>> s = '1 2\t 3\n 4'
>>> print(s)
1 2  3
 4

W trybie interaktywnym możemy wyświetlić zawartość takiej zmiennej po prostu poprzez wpisanie jej nazwy:

>>> s
'1 2\t 3\n 4'

W przypadku skryptu możemy skorzystać z wbudowanej funkcji repr(). Przyjmuje ona dowolny obiekt jako argument i zwraca jego reprezentację w postaci napisu. Jeśli obiektem jest właśnie napis, to białe znaki są przedstawiane w postaci z lewym ukośnikiem:

print(repr(s))
'1 2\t 3\n 4'

Innym problemem, na który możesz się natknąć, jest to, że różne systemy używają różnych znaków, by oznaczyć koniec linii. Niektóre systemy (np. Linux i macOS) używają znaku końca linii, reprezentowanego przez \n. Inne używają zamiast niego znaku powrotu karetki, reprezentowanego przez \r. Jeszcze inne (np. Windows) używają obu tych znaków, tj. \r\n. Jeśli przenosisz pliki między różnymi systemami, te niespójności mogą powodować problemy.6

Więcej o tym problemie możesz poczytać na stronie https://pl.wikipedia.org/wiki/Koniec_linii. Dla większości systemów istnieją aplikacje do konwersji z jednego formatu na drugi. Możesz je znaleźć na stronie https://en.wikipedia.org/wiki/Newline w sekcji “Conversion between newline formats”. Oczywiście możesz też samodzielnie napisać taki konwerter.

Słowniczek

krzaki
Niepoprawnie wyświetlane znaki tekstowe.
łapanie wyjątku
Uniemożliwienie wyjątkowi zakończenia programu przy użyciu try i except.
plik tekstowy
Sekwencja znaków zapisana na nośniku do trwałego przechowywania, np. na dysku twardym.
pythonowy
Sposób rozwiązania problemu, który działa elegancko w Pythonie. Np. użycie try i except jest pythonowym sposobem na przywrócenie działania programu w przypadku nieistniejącego pliku.
Quality Assurance
Osoba lub zespół skoncentrowany na zapewnieniu ogólnej jakości oprogramowania. QA są często zaangażowani w testowanie i identyfikowanie problemów, zanim produkt zostanie wydany.
znak końca linii
Specjalny znak używany w plikach i napisach do wskazywania końca linii.

Ćwiczenia

Ćwiczenie 1

Napisz program odczytujący plik i wypisz zawartość pliku (wiersz po wierszu), ale dużymi literami. Uruchomienie programu będzie wyglądało następująco:

Podaj nazwę pliku: mbox-short.txt
FROM STEPHEN.MARQUARD@UCT.AC.ZA SAT JAN  5 09:14:16 2008
RETURN-PATH: <POSTMASTER@COLLAB.SAKAIPROJECT.ORG>
RECEIVED: FROM MURDER (MAIL.UMICH.EDU [141.211.14.90])
     BY FRANKENSTEIN.MAIL.UMICH.EDU (CYRUS V2.3.8) WITH LMTPA;
     SAT, 05 JAN 2008 09:14:16 -0500

Plik do odczytu możesz pobrać z adresu https://py4e.pl/code3/mbox-short.txt

Ćwiczenie 2

Napisz program proszący o podanie nazwy pliku, a następnie przeszukaj ten plik w celu znalezienia linii podobnych do poniższej:

X-DSPAM-Confidence: 0.8475

Gdy trafisz na linię, która zaczyna się od “X-DSPAM-Confidence:”, podziel linię tak, by wyodrębnić z niej liczbę zmiennoprzecinkową. Zliczaj te linie i sumuj wartości oznaczające pewność, że mamy do czynienia ze spamem. Kiedy dotrzesz do końca pliku, wydrukuj średnią wartość pewności spamu.

Podaj nazwę pliku: mbox.txt
Średni poziom pewności spamu: 0.894128046745
Podaj nazwę pliku: mbox-short.txt
Średni poziom pewności spamu: 0.750718518519

Przetestuj swój program na plikach mbox.txt i mbox-short.txt.

Ćwiczenie 3

Czasami, gdy programiści się nudzą lub chcą się trochę zabawić, dodają do swojego programu nieszkodliwy tzw. Easter Egg7 (dosł. z ang. jajko wielkanocne). Zmodyfikuj swój program, który pyta użytkownika o nazwę pliku tak, by wypisał zabawną wiadomość, gdy użytkownik wpisze w nim nazwę pliku “trele morele”. Program powinien zachowywać się normalnie dla wszystkich innych plików, które istnieją lub nie. Oto przykładowe wykonanie programu:

Podaj nazwę pliku: mbox.txt
Mamy 1797 linii z tematem wiadomości w pliku mbox.txt
Podaj nazwę pliku: missing.txt
Nie można otworzyć pliku: missing.txt
Podaj nazwę pliku: trele morele
TRELE MORELE - co za bzdury!

Nie zachęcamy Cię do umieszczania Easter Eggów w Twoich programach; to tylko ćwiczenie.


  1. https://pl.wikipedia.org/wiki/ASCII↩︎

  2. https://pl.wikipedia.org/wiki/UTF-8↩︎

  3. https://pl.wikipedia.org/wiki/ISO_8859-2↩︎

  4. https://pl.wikipedia.org/wiki/CP-1250↩︎

  5. W polskiej wersji systemu Windows domyślnym kodowaniem zapewne będzie CP-1250 i próba zapisu polskiego tekstu zakończy się powodzeniem.↩︎

  6. Jeśli korzystamy np. z edytora Atom, to po otwarciu interesującego nas pliku informację o użytym sposobie zapisu oznaczenia końca linii możemy znaleźć na dolnej belce w prawym dolnym rogu aplikacji. LF (ang. line feed) oznacza \n, a CR (ang. carriage return) oznacza \r.↩︎

  7. https://pl.wikipedia.org/wiki/Easter_egg↩︎


Jeśli znajdziesz błąd w tej książce, wyślij poprawkę za pomocą GitHuba.

Indywidualne wsparcie na utrzymanie i rozwój tej strony można wysłać poprzez GitHub Sponsors.