7. Pliki tekstowe

7.1. Skrypt [fic_01]: odczyt/zapis pliku tekstowego
Poniższy skrypt przedstawia przykład wykorzystania plików tekstowych:
# importy
import sys
# tworzenie, a następnie sekwencyjne przetwarzanie pliku tekstowego
# plik ten stanowi zbiór wierszy o postaci login:pwd:uid:gid:infos:dir:shell
# każdy wiersz jest umieszczany w słowniku w postaci login => uid:gid:infos:dir:shell
# --------------------------------------------------------------------------
def affiche_infos(dico: dict, clé: str):
# wyświetla wartość powiązaną z kluczem w słowniku „dico”, jeśli istnieje
if clé in dico.keys():
# wyświetla wartość powiązaną z kluczem
print(f"{clé} : {dico[clé]}")
else:
# klucz nie występuje w słowniku „dico”
print(f"la clé [{clé}] n'existe pas")
# main -----------------------------------------------
# ustala się nazwę pliku
FILE_NAME = "./data/infos.txt"
# tworzenie i wypełnianie pliku tekstowego
fic = None
try:
# otwarcie pliku do zapisu (w=write)
fic = open(FILE_NAME, "w")
# generowanie dowolnej treści
for i in range(1, 101):
# jedna linia
ligne = f"login{i}:pwd{i}:uid{i}:gid{i}:infos{i}:dir{i}:shell{i}"
# jest zapisywana w pliku tekstowym
fic.write(f"{ligne}\n")
except IOError as erreur:
print(f"Erreur d'exploitation du fichier {FILE_NAME} : {erreur}")
sys.exit()
finally:
# zamknięcie pliku, jeśli został otwarty
if fic:
fic.close()
# otwiera się go w trybie odczytu
fic = None
try:
# otwarcie pliku w trybie odczytu
fic = open(FILE_NAME, "r")
# słownik jest początkowo pusty
dico = {}
# każdy wiersz jest umieszczany w słowniku [dico] w postaci login => uid:gid:infos:dir:shell
# odczyt pierwszego wiersza z usunięciem spacji na początku i na końcu wiersza
ligne = fic.readline().strip()
# o ile wiersz nie jest pusty
while ligne != '':
# wpisujemy wiersz do tablicy
infos = ligne.split(":")
# pobieramy login
login = infos[0]
# pomijamy hasło
infos[0:2] = []
# tworzymy wpis w słowniku
dico[login] = infos
# odczyt następnego wiersza
ligne = fic.readline().strip()
except IOError as erreur:
print(f"Erreur d'exploitation du fichier {FILE_NAME} : {erreur}")
sys.exit()
finally:
# zamykamy plik, jeśli był otwarty
if fic:
fic.close()
# wykorzystanie słownika „dico”
affiche_infos(dico, "login10")
affiche_infos(dico, "X")
Uwagi:
- wiersz 28: otwarcie pliku do zapisu (w=write). Jeśli plik już istnieje, zostanie nadpisany;
- wiersze 30–34: generowanie 100 wierszy w pliku tekstowym;
- wiersz 34: zapisywanie wiersza w pliku tekstowym. Metoda [write] nie dodaje znaku końca wiersza. Należy zatem uwzględnić ten znak w zapisywanym tekście;
- wiersze 35–37: obsługa ewentualnego wyjątku;
- wiersz 37: przerwanie wykonywania skryptu (jednak po wykonaniu klauzuli finally);
- wiersze 38–41: w każdym przypadku, niezależnie od tego, czy wystąpił błąd, czy nie, zamyka się plik, jeśli jest otwarty;
- wiersz 47: otwarcie pliku do odczytu (r=read);
- wiersz 49: zdefiniowanie pustego słownika;
- wiersz 52: metoda [readline] odczytuje wiersz tekstu, łącznie ze znakiem końca wiersza. Metoda [strip] usuwa „spacje” na początku i na końcu ciągu znaków. Przez „spację” należy rozumieć znak białego znaku, znak końca linii, znak końca strony, tabulator oraz kilka innych. W związku z tym metoda [ligne] nie będzie zawierała znaków końca linii, w przeciwieństwie do metod [\r\n] (Windows) lub [\n] (Unix);
- wiersz 54: przetwarzamy plik, dopóki nie napotkamy pustego wiersza;
- wiersze 54–64: plik tekstowy jest przenoszony do słownika [dico]. Kluczem jest pole [login], a wartością – pola [uid:gid:infos:dir:shell];
- wiersze 65–67: obsługa ewentualnego wyjątku;
- wiersze 68–71: zamknięcie pliku w każdym przypadku, niezależnie od tego, czy wystąpił błąd, czy nie;
- wiersze 74–75: wykorzystanie słownika [dico];
Plik [data/infos.txt]:
Wyniki wyświetlane na ekranie:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fichiers/fic_01.py
login10 : ['uid10', 'gid10', 'infos10', 'dir10', 'shell10']
la clé [X] n'existe pas
Process finished with exit code 0
7.2. Skrypt [fic_02]: obsługa plików tekstowych zakodowanych w formacie UTF-8
W dalszej części dokumentu będziemy zajmować się wyłącznie plikami tekstowymi zakodowanymi w formacie UTF-8. Najpierw skonfigurujemy PyCharm:

- w [5-6]: należy wybrać kodowanie UTF-8 dla plików projektu;
Aby utworzyć plik zakodowany w formacie UTF-8, można postępować w następujący sposób (fic-02):
# importy
import codecs
# zapis w formacie UTF-8 do pliku tekstowego
# nie obsługuje się wyjątków
file=codecs.open("./data/utf8.txt","w","utf8")
file.write("Hélène est partie à Bâle pendant l'été chez sa grand-mère")
file.close()
Uwagi
- wiersz 2: w celu zarządzania kodowaniem plików należy zaimportować moduł [codecs];
- wiersz 6: metoda [codecs.open] działa tak samo jak klasyczna funkcja [open]. Można jednak określić żądane kodowanie (tworzenie) lub istniejące (odczyt). Po otwarciu obiekt [file] uzyskany w wierszu 6 używa się jak zwykły plik;
- wiersz 7: użyto znaków z akcentami, które w większości przypadków są wyświetlane różnie w zależności od zastosowanego zestawu znaków;
Wyniki
Po otwarciu uzyskanego pliku [data/utf8.txt] (patrz wiersz 6) otrzymujemy następujący wynik:

7.3. Skrypt [fic_03]: obsługa plików tekstowych zakodowanych w formacie ISO-8859-1
Skrypt [fic_03] działa tak samo jak skrypt [fic_02], ale koduje plik tekstowy w formacie ISO-8859-1. Chcemy pokazać różnicę między uzyskanymi plikami:
# importy
import codecs
# zapis w kodowaniu iso-8859-1 do pliku tekstowego
# brak obsługi wyjątków
file=codecs.open("./data/iso-8859-1.txt","w","iso-8859-1")
file.write("Hélène est partie à Bâle pendant l'été chez sa grand-mère")
file.close()
Po otwarciu pliku [data/iso-8859-1] utworzonego w wierszu 6 otrzymujemy następujący wynik:

Ponieważ skonfigurowaliśmy projekt do pracy z plikami o nazwach UTF-8, program PyCharm próbował otworzyć plik [iso-8859-1.txt] w formacie UTF-8. Program rozpoznaje, że plik [1] nie jest plikiem UTF-8. Następnie proponuje, aby plik [2] został ponownie załadowany w innym kodowaniu:

- w [3-5]: plik jest ponownie ładowany przy użyciu kodowania ISO-8859-1;

- na [6] – ten sam plik, ale wyświetlany w innym kodowaniu;
Jeśli wrócimy do ustawień projektu:

- widać, że w przypadku pliku [6-7] program PyCharm odnotował, że plik [iso-8859-1.txt] powinien być otwarty z kodowaniem ISO-8859-1. Jest to zatem wyjątek od reguły [5];
7.4. Skrypt [json_01]: obsługa pliku jSON
JSON oznacza notację obiektową JavaScript. Jak sama nazwa wskazuje, jest to sposób tekstowego przedstawiania obiektów języka JavaScript. W tym przypadku wykorzystamy ją w odniesieniu do obiektów języka Python.
Plik jSON, obsługiwany przez [data/in.json], będzie wyglądał następująco:

- W pliku [2] widać, że treść tekstowa pliku [in.json] mogłaby reprezentować słownik w języku Python. Plik PyCharm sformatował ten tekst (Ctrl-Alt-L), ale nawet gdyby znajdował się on w jednym wierszu, nie zmieniłoby to niczego. Forma tekstu nie ma żadnego znaczenia, o ile pod względem składniowym stanowi on obiekt języka Python;
Skrypt [json-01] pokazuje, jak wykorzystać ten plik:
# importy
import codecs
import json
import sys
# odczyt / zapis pliku jSON
inFile=None
outFile=None
try:
# otwarcie pliku jSON w trybie odczytu
inFile = codecs.open("./data/in.json", "r", "utf8")
# przeniesienie zawartości do słownika
data = json.load(inFile)
# wyświetlanie odczytanych danych
print(f"data={data}, type(data)={type(data)}")
limites = data['limites']
print(f"limites={limites}, type(limites)={type(limites)}")
print(f"limites[1]={limites[1]}, type(limites[1])={type(limites[1])}")
# przeniesienie słownika [data] do pliku JSON
outFile = codecs.open("./data/out.json", "w", "utf8")
json.dump(data, outFile)
except BaseException as erreur:
# wyświetla się komunikat o błędzie i program się zamyka
print(f"L'erreur suivante s'est produite : {erreur}")
sys.exit()
finally:
# zamknięcie otwartych plików
if inFile:
inFile.close()
if outFile:
outFile.close()
Uwagi
- wiersz 3: aby obsługiwać plik JSON, importujemy moduł [json];
- wiersz 11: będziemy obsługiwać pliki jSON zakodowane w formacie UTF-8. Tutaj otwieramy plik [data/in.json] za pomocą modułu [codecs];
- wiersz 13: metoda [json.load] odczytuje zawartość pliku jSON i umieszcza ją w zmiennej [data]. Typ tej zmiennej będzie tutaj słownikiem;
- wiersze 15–18: aby pokazać, że rzeczywiście uzyskaliśmy słownik w języku Python, wyświetlamy kilka jego elementów;
- wiersze 20–21: wykonujemy operację odwrotną: słownik [data] jest zapisywany w pliku o nazwie UTF-8 za pomocą metody [json.dump];
- wiersze 22–25: obsługa ewentualnego wyjątku;
- wiersze 26–31: w każdym przypadku, niezależnie od tego, czy wystąpił błąd, czy nie, zamykamy pliki, które mogły zostać otwarte;
Wyniki
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fichiers/json_01.py
data={'limites': [9964, 27519, 73779, 156244, 0], 'coeffR': [0, 0.14, 0.3, 0.41, 0.45], 'coeffN': [0, 1394.96, 5798, 13913.69, 20163.45], 'PLAFOND_QF_DEMI_PART': 1551, 'PLAFOND_REVENUS_CELIBATAIRE_POUR_REDUCTION': 21037, 'PLAFOND_REVENUS_COUPLE_POUR_REDUCTION': 42074, 'VALEUR_REDUC_DEMI_PART': 3797, 'PLAFOND_DECOTE_CELIBATAIRE': 1196, 'PLAFOND_DECOTE_COUPLE': 1970, 'PLAFOND_IMPOT_COUPLE_POUR_DECOTE': 2627, 'PLAFOND_IMPOT_CELIBATAIRE_POUR_DECOTE': 1595, 'ABATTEMENT_DIXPOURCENT_MAX': 12502, 'ABATTEMENT_DIXPOURCENT_MIN': 437}, type(data)=<class 'dict'>
limites=[9964, 27519, 73779, 156244, 0], type(limites)=<class 'list'>
limites[1]=27519, type(limites[1])=<class 'int'>
Process finished with exit code 0
- wiersze 2–4 pokazują, że poprawnie odzyskano słownik zawarty w pliku jSON;
Teraz przyjrzyjmy się zawartości pliku [data/out.json]:

Tekst pliku znajduje się w jednym wierszu. Jednak program PyCharm rozpoznaje pliki jSON i można je sformatować, podobnie jak pliki w języku Python i inne, za pomocą skrótu klawiszowego Ctrl-Alt-L. Otrzymujemy wówczas następujący wynik:

7.5. Skrypt [json_02]: obsługa plików jSON zakodowanych w formacie UTF-8
Plik jSON zakodowany w formacie UTF-8 może występować w dwóch postaciach:
# importy
import codecs
import json
import sys
# słownik
data = {'marié': 'oui', 'impôt': 1340}
# zapis pliku jSON
out_file1 = None
out_file2 = None
try:
# przeniesienie słownika [data] do pliku JSON
out_file1 = codecs.open("./data/out1.json", "w", "utf8")
json.dump(data, out_file1, ensure_ascii=True)
# przeniesienie słownika [data] do pliku JSON
out_file2 = codecs.open("./data/out2.json", "w", "utf8")
json.dump(data, out_file2, ensure_ascii=False)
except BaseException as erreur:
# wyświetlanie komunikatu o błędzie i zakończenie działania
print(f"L'erreur suivante s'est produite : {erreur}")
sys.exit()
finally:
# zamknięcie otwartych plików
if out_file1:
out_file1.close()
if out_file2:
out_file2.close()
…
- w tym skrypcie zapisujemy słownik [data] (wiersz 7) w dwóch plikach jSON (wiersze 14, 17);
- wiersze 14, 17: w obu przypadkach tworzy się plik tekstowy o nazwie UTF-8;
- wiersze 15: podczas zapisywania słownika używany jest parametr o nazwie [ensure_ascii=True];
- wiersze 18: podczas zapisywania słownika używa się parametru o nazwie [ensure_ascii=False];
Oto dwa uzyskane pliki:

- W pliku [out1.json] znaki z akcentami zostały zastąpione ciągiem znaków reprezentującym ich kod UTF-8. Czasami mówi się, że zostały one „eskapowane”. Z technicznego punktu widzenia w pliku binarnym [out1.json] dla znaku „é” z pliku [marié] występują kolejno kody binarne UTF-8 sześciu znaków [\u00e9];
- w pliku [out2.json] znaki z akcentami pozostawiono bez zmian. Oznacza to, że w pliku binarnym [out2.json] znaki te są reprezentowane przez ich kod binarny UTF-8 (tylko jeden kod UTF-8 zamiast 6 dla out1). W przypadku znaku „é” z pliku [marié] znajdziemy zatem kod binarny [00e9] na 4 bajtach;
- to wartość parametru [ensure_ascii] metody [json.dump] decyduje o zastosowanym formacie;
Niektóre aplikacje wykorzystują „escapowany” UTF-8 dla swoich plików jSON. W takim przypadku należy zastosować wartość [ensure_ascii=True]. Jest to w rzeczywistości wartość domyślna. Jeśli więc nie używa się parametru [ensure_ascii], będzie się pracować z plikami jSON i UTF-8 z „escapowaniem”.
Skrypt przebiega dalej w następujący sposób:
# importy
import codecs
import json
import sys
# słownik
data = {'marié': 'oui', 'impôt': 1340}
…
# ponowne odczytanie plików jSON
in_file1 = None
in_file2 = None
try:
# przeniesienie pliku jSON 1 do słownika
in_file1 = codecs.open("./data/out1.json", "r", "utf8")
dico1 = json.load(in_file1)
# wyświetlanie
print(f"dico1={dico1}")
# przeniesienie pliku jSON 2 do słownika
in_file2 = codecs.open("./data/out2.json", "r", "utf8")
dico2 = json.load(in_file2)
# wyświetlenie
print(f"dico2={dico2}")
except BaseException as erreur:
# wyświetlanie komunikatu o błędzie i zamknięcie
print(f"L'erreur suivante s'est produite : {erreur}")
sys.exit()
finally:
# zamknięcie plików, jeśli są otwarte
if in_file1:
in_file1.close()
if in_file2:
in_file2.close()
Uwagi
- wiersze 11–34: odczytanie dwóch plików [out1.json, out2.json] i wyświetlenie słownika odczytanego w każdym z przypadków;
Wyniki
Co zaskakujące, okazuje się, że nie było potrzeby określania w funkcji [json.load] (wiersze 17, 22) typu kodowania (z znakami escape lub bez) ciągu jSON, który ma zostać odczytany. W obu przypadkach pobierany jest właściwy słownik.