Python - wprowadzenie

16 - Dane w plikach tekstowych - zapis i odczyt

Zapis i odczyt danych jest niezwykle istotnym aspektem funkcjonalności wielu programów, zwłaszcza w analizie danych czy bioinformatyce. Dane można zapisać na różne sposoby, często jednak stosuje się zapis w plikach tekstowych. Do największych zalet takiego rozwiązania należy uniwersalność. Można je otwierać, przeglądać, modyfikować i zapisywać w niezliczonych edytorach tekstu, a także w narzędziach dostosowanych do pracy z nimi. Wiele z nich (jak np. grep, sed, awk, wc, more) jest ,,na dzień dobry’’ zainstalowanych na systemach Uniksowych i pochodnych (Unix, GNU Linux, Mac OS itd.). Wiele popularnych formatów zapisu danych bioinformatycznych, jest opartych na plikach tekstowych, np. FASTA, FASTQ, Newick czy Nexus. Można też zapisywać dane w formacie binarnym, jednak skupimy się na tekstowym.

Niezależnie od tego, czy chcemy zapisać, odczytać, czy dopisać dane, pierwszą rzeczą, jaką musimy zrobić jest otwarcie pliku. Służy do tego polecenie open(nazwa_pliku, [tryb]), zwracająca obiekt reprezentujący plik (file object). Zaraz wyjaśnię, co oznacza argument tryb.

Po wykonaniu operacji na pliku należy go zamknąć.

Zapis w pliku

Zacznijmy od prostego przykładu:

# Otwarcie pliku do zapisu.
plik_zapis = open('przyklad_1.txt', 'wt')
# Zapisanie w pliku tekstu
print('Tekst 1.', file=plik_zapis)
print('Tekst 2.', file=plik_zapis)
# Inny sposób zapisu
plik_zapis.write('Tekst 3.')
plik_zapis.write('Tekst 4.')
# Zamknięcie pliku
plik_zapis.close()

W katalogu, w którym został uruchomiony program z powyższym kodem, powinien pojawić się plik o nazwie przyklad_1.txt. Otwórz go w dowolnym edytorze tekstu, będzie mieć następującą zawartość:

Tekst 1.
Tekst 2.
Tekst 3.Tekst 4.

Jak widać, udało nam się zapisać tekst do pliku. W powyższym przykładzie użyłem dwu różnych sposobów zapisu do pliku. Pierwszy z użyciem funkcji print(), której należało podać argument file, drugi polegający na wywołaniu metody write() na obiekcie. Zauważ, że w pierwszym przypadku (print()) każdy tekst został zapisany w osobnej linii, ponieważ przy wywołaniu tej funkcji, na końcu tekstu wprowadzony jest znak przejścia do nowej linii, podobnie jak w terminalu (można to zmienić podając parametr end=''). Zastosowanie metody write() nie powoduje przejścia do nowej linii, zatem umożliwia dopisywanie tekstu w jednej linii (to też można zmienić, dodając do łańcucha znaków: \n).

Tryby otwarcia pliku

Warto teraz przyjrzeć się drugiemu argumentowi polecenia open(). Jak widać, w powyższym przykładzie zawiera on dwie litery oznaczające tryby otwarcia pliku. Możliwe są tryby:

tryb znaczenie
r Odczyt (ang. read)
r+ Odczyt i zapis
w Zapis (ang. write). Jeśli plik istnieje, zostaje nadpisany.
x Zapis, ale tylko jeśli plik nie istnieje.
a Dopisanie na końcu pliku (ang. append).

Drugi argument jest opcjonalny, jeśli go nie podamy, plik otwierany jest w trybie do odczytu (r). Druga litera w powyższym przykładzie, t oznacza plik tekstowy, jest to zresztą wartość domyślna, więc nie musimy jej podawać, jeśli korzystamy z trybu tekstowego. Gdybyśmy chcieli skorzystać z trybu binarnego, należałoby podać b.

Odczyt z pliku

Skoro wiemy już, jak zapisać tekst w pliku, czas się dowiedzieć, jak taki plik odczytać.

# Otwarcie pliku do odczytu.
plik_odczyt = open('przyklad_1.txt', 'rt')
# Odczyt zawartości pliku
caly_tekst = plik_odczyt.read()
plik_odczyt.close()
print(caly_tekst)
Tekst 1.
Tekst 2.
Tekst 3.Tekst 4.

Metoda read() odczytuje całą zawartość pliku na raz. Nie zawsze jest to optymalne rozwiązanie. Czasem zawartość pliku jest bardzo duża, np. w przypadku danych genomowych. W takim wypadku można dane odczytywać fragmentami albo linia po linii:

# Otwarcie pliku do odczytu.
plik_odczyt = open('przyklad_1.txt', 'rt')
l = 1
for linia in plik_odczyt:
    print(f"linia {l}: {linia}")
    l += 1
plik_odczyt.close()
linia 1: Tekst 1.

linia 2: Tekst 2.

linia 3: Tekst 3.Tekst 4.

Zauważ, że pomiędzy liniami pojawiła się dodatkowa pusta linia. Stało się tak dlatego, że oprócz znaku nowego wiersza znajdującego się na końcu każdej linii w pliku tekstowym, swój dodatkowy, nowy znak dodaje też funkcja print(). Można temu zaradzić używając metody rstrip() (usuwającej białe znaki z końca łańcucha) lub ustawiając argument end='' w funkcji print():

# Otwarcie pliku do odczytu.
plik_odczyt = open('przyklad_1.txt', 'rt')
l = 1
for linia in plik_odczyt:
    print(f"linia {l}: {linia}", end='')
    l += 1
plik_odczyt.close()
linia 1: Tekst 1.
linia 2: Tekst 2.
linia 3: Tekst 3.Tekst 4.

Poszczególne linie z pliku tekstowego można także odczytać metodą readline(). Można, ją wykorzystać w pętli, ale także poza nią.

plik_odczyt = open('przyklad_1.txt', 'rt')
linia = plik_odczyt.readline()
print(f"Pierwsza linia: {linia}", end = '')
print("Następne linie:")
l = 2
while True:
    linia = plik_odczyt.readline()
    # Jeśli nie ma kolejnej linii:
    if not linia:
        break
    print(f"Linia {l}: {linia}", end = '')
    l += 1
plik_odczyt.close()
Pierwsza linia: Tekst 1.
Następne linie:
Linia 2: Tekst 2.
Linia 3: Tekst 3.Tekst 4.

Z pliku do listy i z powrotem

Wszystkie linie tekstu w pliku, możemy przekazać bezpośrednio do listy, używając metody readlines(). Jest o użyteczne np. kiedy chcemy oddzielić proces odczytu pliku od późniejszej obróbki odczytanych danych.

# Otwarcie pliku do odczytu.
plik_odczyt = open('przyklad_1.txt', 'rt')
linie = plik_odczyt.readlines()
plik_odczyt.close()
print(linie)
['Tekst 1.\n', 'Tekst 2.\n', 'Tekst 3.Tekst 4.']

Zauważ znaki nowej linii.

Analogicznie, użycie metody writelines() pozwala zapisać elementy listy, do kolejnych linii pliku tekstowego:

# Lista
lista = ['AGCCCATAC\n', 'GGTCAATCGT\n', 'CGGGTACCA\n', 'ACACTTTG']
plik_sekw = open('sekw.txt', 'wt')
# Zapis listy do pliku
plik_sekw.writelines(lista)
plik_sekw.close()

# Sprawdźmy zawartość pliku
plik_odczyt = open('sekw.txt', 'rt')
# Odczyt całości pliku i od razu wydruk 
print(plik_odczyt.read())
plik_odczyt.close()
AGCCCATAC
GGTCAATCGT
CGGGTACCA
ACACTTTG

Zauważ, że ciągi znaków w liście (oprócz ostatniego) są zakończone znakiem nowej linii \n. Bez nich elementy listy zostałyby zapisane ciągiem w jednej linii. Oczywiście w takim przypadku można użyć iteracji, zapisując kolejne elementy listy do pliku:

# Lista
lista = ['AGCCCATAC', 'GGTCAATCGT', 'CGGGTACCA', 'ACACTTTG']
plik_sekw = open('sekw2.txt', 'wt')
for sekw in lista:
    plik_sekw.write(sekw + "\n")
plik_sekw.close()

# Sprawdźmy zawartość pliku
plik_odczyt = open('sekw2.txt', 'rt')
print(plik_odczyt.read())
plik_odczyt.close()
AGCCCATAC
GGTCAATCGT
CGGGTACCA
ACACTTTG

Dopisanie tekstu na końcu pliku

Sprawdźmy jeszcze tryb dopisywania na końcu pliku:

# Otwarcie pliku do dopisywania.
plik_dopisanie = open('przyklad_1.txt', 'at')
# Dopisanie,w nowej linii na końcu pliku
plik_dopisanie.write("\nTekst dopisany")
plik_dopisanie.close()
# Sprawdzamy wynik dopisania:
plik_odczyt = open('przyklad_1.txt', 'rt')
caly_tekst = plik_odczyt.read()
plik_odczyt.close()
print(caly_tekst)
Tekst 1.
Tekst 2.
Tekst 3.Tekst 4.
Tekst dopisany

Konstrukcja with open(...) as ....

Jak wspomniałem powyżej, po otwarciu pliku, należy go zamknąć. Dotychczas stosowaliśmy pary funkcji open() oraz metody close(). Można jednak wykorzystać inną konstrukcję, pomijającą metodę close(). W tym przypadku kiedy wykona się ,,wcięty’’ blok kodu pod wyrażeniem with open(...) as ...., plik zostanie automatycznie zamknięty:

import random
# Zapis 10 liczb pseudolosowych do pliku
with open('liczby.txt', 'wt') as plik:
    for i in range(1,11):
        plik.write(f"{i} {random.random()}\n")
l = 1        
with open('liczby.txt', 'rt') as plik:
    for linia in plik:
        print(f"Liczba {l}: {linia}", end='')
        l += 1
Liczba 1: 1 0.853261452522613
Liczba 2: 2 0.980256230911471
Liczba 3: 3 0.953439426587273
Liczba 4: 4 0.15793851957961835
Liczba 5: 5 0.4668890626061927
Liczba 6: 6 0.7785696121224095
Liczba 7: 7 0.4508544479417528
Liczba 8: 8 0.4283972150851564
Liczba 9: 9 0.1562176822689083
Liczba 10: 10 0.8041240006053397

Parę przykładów

Odczyt pliku w formacie FASTA.

Pobierz plik atp6-sequences.fasta. Sprawdź jego zawartość:

>KU180473.1 Orobanche coerulescens clone G18 ATPase subunit 6 (atp6) gene, partial cds; mitochondrial
CTGCTAACTCTCAGTTTGGTCCTACTTCTGATTCATTTTGTTACTAAAAAGGGAGGAGGAAACTCAGTAC
CAAATGCTTGGCAATCCGTGGTAGAGTTTATTTATGATTTCGTGCTGAACCTGGTAAACGAACAAATAGG
GGGTCTTTCCGGAAATGTTAAACAAAAGTTTTTCCCTTGCATCTTGGTCACTTTTACTTTTTTGTTATTT
TGTAATCTTCAGGGTATGATACCTTATAGCTTCACAGTTACAAGTCATTTTCTCATTACTTTAGGTCTCT
CATTTTCTCTTTTTATTGGCATTACTATAGTGGGATTTCAAAGAAATGGGCTTCATTTTTTAAGCTTCTT
ATTACCCGCAGGAGTCCCACTGCCATTAGCACCTTTTTTAGTACTCCTTGAGCTAATTTCTTATTGTTTT
CGCGCATTAAGCTTAGGAATACGTTTATTTGCTAATATGATGGCCGGTCATAGTTTAGTAAAGATTTTAA
GTGGGTTCGCTTGGACTATGCTATGTATGAATGATCTTTTGTATTTTATAGGGGATCTTGGTCCTTTATT
TATAGTTCTTGCATTAACCGGTCTTGAATTAGGTGTAGCTATATCACAAGCTCATGTTTCTACGATCTCA
ATCTGTATTTAC

>KU180472.1 Orobanche cernua clone G15 ATPase subunit 6 (atp6) gene, partial cds; mitochondrial
CTACTCACTCTCAGTTTGGTCCTACTTTTTGTTCATTTTGTTACTAAAAAGGGAGGAGGAAACTCAGTAC
CAAATGCTTTTCAATCCGTGTTAGAGCTTATTTATGATTTTGTGCCGAACCTGGTAAACGAACAAATAGG
TGGTCTTTCCGGAAATGTTAAACAAAAGTTTTTCCCTTGCATCTCGGTTACTTTTACTTTTTCGTTATTT
CGTAATCTGCAGGGTATGATACCTTATAGCTTCACAGTAACAAGTCATTTTATCGTTACTTTGGGTCTCT
CATTTTCTCTTTTTATTGGCATTACTATAGTGGGATTTCAAAAAAATGGGCTTCATTTTTTAAGCTTCTC
ATTACCCGCAGGAGTCCCACTGCCGTTAGCACCTTTTTTAGTACTCCTTGAGCTAATCCCTCATTGTTTT
CGCGCATTAAGCTTAGGAATACGTTTATTTGCTAATATGATGGCCGGTCATAGTTTAGTAAAGATTTTAA
GTGGGTTCGCTTGGACTATGCTATGTATGAATGATCTTTTCTATTTCATAGGGGATCCTGGTCCTTTATT
TATAGTTCTTGCATTAACCGGTCTTGAATTAGGTGTAGCTATATCACAAGCTCATGTTTCTACGATCTCA
ATCTGTATTTAC
...

Plik zawiera zestaw sekwencji genu atp6 pobranych z bazy GenBank i zapisanych w formacie FASTA. Jak widać, zgodnie z formatem FASTA dla każdej sekwencji linia z opisem rozpoczyna się od znaku >. Następnie następuje seria linii z sekwencją zasad. Po nich następuje pusta linia, a po niej zaczynają się dane dla kolejnej sekwencji.

Napiszemy teraz program, który odczyta z pliku kolejne sekwencje i zapisze je w słowniku, gdzie opis będzie kluczem a cała sekwencja będzie wartością. Zauważ, że linię z opisem, po usunięciu znaku > w całości zapisujemy jako klucz, wartość słownika musimy ,,skleić’’ z kolejnych linii zawierających litery oznaczające zasady, a puste linie zignorować.

Zaczniemy od napisania kodu, który odczyta plik, linia po linii:

with open('atp6-sequences.fasta', 'rt') as plik:
    for linia in plik:
        print(f"{linia}", end='')
>KU180473.1 Orobanche coerulescens clone G18 ATPase subunit 6 (atp6) gene, partial cds; mitochondrial
CTGCTAACTCTCAGTTTGGTCCTACTTCTGATTCATTTTGTTACTAAAAAGGGAGGAGGAAACTCAGTAC
CAAATGCTTGGCAATCCGTGGTAGAGTTTATTTATGATTTCGTGCTGAACCTGGTAAACGAACAAATAGG
GGGTCTTTCCGGAAATGTTAAACAAAAGTTTTTCCCTTGCATCTTGGTCACTTTTACTTTTTTGTTATTT
TGTAATCTTCAGGGTATGATACCTTATAGCTTCACAGTTACAAGTCATTTTCTCATTACTTTAGGTCTCT
CATTTTCTCTTTTTATTGGCATTACTATAGTGGGATTTCAAAGAAATGGGCTTCATTTTTTAAGCTTCTT
ATTACCCGCAGGAGTCCCACTGCCATTAGCACCTTTTTTAGTACTCCTTGAGCTAATTTCTTATTGTTTT
CGCGCATTAAGCTTAGGAATACGTTTATTTGCTAATATGATGGCCGGTCATAGTTTAGTAAAGATTTTAA
GTGGGTTCGCTTGGACTATGCTATGTATGAATGATCTTTTGTATTTTATAGGGGATCTTGGTCCTTTATT
TATAGTTCTTGCATTAACCGGTCTTGAATTAGGTGTAGCTATATCACAAGCTCATGTTTCTACGATCTCA
ATCTGTATTTAC

>KU180472.1 Orobanche cernua clone G15 ATPase subunit 6 (atp6) gene, partial cds; mitochondrial
CTACTCACTCTCAGTTTGGTCCTACTTTTTGTTCATTTTGTTACTAAAAAGGGAGGAGGAAACTCAGTAC
CAAATGCTTTTCAATCCGTGTTAGAGCTTATTTATGATTTTGTGCCGAACCTGGTAAACGAACAAATAGG
TGGTCTTTCCGGAAATGTTAAACAAAAGTTTTTCCCTTGCATCTCGGTTACTTTTACTTTTTCGTTATTT
...    

Teraz dopiszmy kod, który ,,rozpoznaje’’ czy linia jest opisem sekwencji, fragmentem ciągu zasad, czy pustą linią:

with open('atp6-sequences.fasta', 'rt') as plik:
    for linia in plik:
        # Linia z opisem sekwencji
        if linia.startswith('>'):
            print("opis: ", end='')
        # Linia z sekwencją
        elif len(linia) > 1:
            print("sekwencja: ", end='')
        # Pusta linia
        else:
            print("pusta: ", end='')
        print(f"{linia}", end='')
opis: >KU180473.1 Orobanche coerulescens clone G18 ATPase subunit 6 (atp6) gene, partial cds; mitochondrial
sekwencja: CTGCTAACTCTCAGTTTGGTCCTACTTCTGATTCATTTTGTTACTAAAAAGGGAGGAGGAAACTCAGTAC
sekwencja: CAAATGCTTGGCAATCCGTGGTAGAGTTTATTTATGATTTCGTGCTGAACCTGGTAAACGAACAAATAGG
sekwencja: GGGTCTTTCCGGAAATGTTAAACAAAAGTTTTTCCCTTGCATCTTGGTCACTTTTACTTTTTTGTTATTT
...
pusta: 

Jak widać, nasz kod prawidłowo identyfikuje linie, teraz możemy dopisać kod, który usuwa znak ‘>’ z opisu sekwencji, łączy fragmenty sekwencji i usuwa znaki końca linii:

sekwencja = ''
with open('atp6-sequences.fasta', 'rt') as plik:
    for linia in plik:
        # Linia z opisem sekwencji
        if linia.startswith('>'):
            opis = linia.replace('>','').rstrip()
        # Linia z sekwencją
        elif len(linia) > 1:
            sekwencja += linia.rstrip()
        # Pusta linia
        else:
            # Sprawdzamy, czy wszystko OK
            print(f"opis: {opis} ")
            print(f"sekwencja: {sekwencja}")
            sekwencja = ''
opis: KU180473.1 Orobanche coerulescens clone G18 ATPase subunit 6 (atp6) gene, partial cds; mitochondrial 
sekwencja: CTACTCACTCTCAGTTTGGTCCTACTTTTTGTTCATTTTGTTACTAAAAAGGGAGGAGGAAACTCAGTACCAAATGCTTGGCAATCCTTGGTAGAGCTTATTTATGATTTCGTGCCGAACCTGGTAAACGAACAAATAGGTGGTCTTTCCGGAAATGTGAAACAAAAGTTTTTCCCTTGCATCTCGGTTACTTTTACTTTTTCGTTATTTCGTAATCTCCAGGGTATGATACCTTATAGCTTCACAGTGACAAGTCATTTTCTCATTACTTTGGGTCTCTCATTTGCTCTTTTTATTGGCATTACTATAGTGGGATTTGCACAAAATGGGCTTCATTTTTTAAGCTTCTCATTACCCGCAGGAGTCCCACTGCCGTTAGCACCTTTTTTAGTACTCCTTGAGCTAATCCCTCATTGTTTTCGCGCATTAAGCTTAGGAATACGTTTATTTGCTAATATGATGGCCGGGCATAGTTTAGTCAAGATTTTAAGTGGGTTCGCTTGGACTATGCTATGTATGAATGATCTTTTTTATTTCATAGGAGATCTTGGTCCTTTATTTATAGTTCTTGCATTAACTGGTCTGGAATTAGGCGTAGCTATATTACAAGCTTATGTTTTTACTATATTAATCTGTATTTACCTGCTAACTCTCAGTTTGGTCCTACTTCTGATTCATTTTGTTACTAAAAAGGGAGGAGGAAACTCAGTACCAAATGCTTGGCAATCCGTGGTAGAGTTTATTTATGATTTCGTGCTGAACCTGGTAAACGAACAAATAGGGGGTCTTTCCGGAAATGTTAAACAAAAGTTTTTCCCTTGCATCTTGGTCACTTTTACTTTTTTGTTATTTTGTAATCTTCAGGGTATGATACCTTATAGCTTCACAGTTACAAGTCATTTTCTCATTACTTTAGGTCTCTCATTTTCTCTTTTTATTGGCATTACTATAGTGGGATTTCAAAGAAATGGGCTTCATTTTTTAAGCTTCTTATTACCCGCAGGAGTCCCACTGCCATTAGCACCTTTTTTAGTACTCCTTGAGCTAATTTCTTATTGTTTTCGCGCATTAAGCTTAGGAATACGTTTATTTGCTAATATGATGGCCGGTCATAGTTTAGTAAAGATTTTAAGTGGGTTCGCTTGGACTATGCTATGTATGAATGATCTTTTGTATTTTATAGGGGATCTTGGTCCTTTATTTATAGTTCTTGCATTAACCGGTCTTGAATTAGGTGTAGCTATATCACAAGCTCATGTTTCTACGATCTCAATCTGTATTTAC
opis: KU180472.1 Orobanche cernua clone G15 ATPase subunit 6 (atp6) gene, partial cds; mitochondrial 
sekwencja: CTACTCACTCTCAGTTTGGTCCTACTTTTTGTTCATTTTGTTACTAAAAAGGGAGGAGGAAACTCAGTACCAAATGCTTTTCAATCCGTGTTAGAGCTTATTTATGATTTTGTGCCGAACCTGGTAAACGAACAAATAGGTGGTCTTTCCGGAAATGTTAAACAAAAGTTTTTCCCTTGCATCTCGGTTACTTTTACTTTTTCGTTATTTCGTAATCTGCAGGGTATGATACCTTATAGCTTCACA
...

Wygląda na to, że kod działa dobrze. Zatem pozostaje umieścić wszystko w słowniku:

sekwencje = {}
sekwencja = ''
with open('atp6-sequences.fasta', 'rt') as plik:
    for linia in plik:
        # Linia z opisem sekwencji
        if linia.startswith('>'):
            opis = linia.replace('>','').rstrip()
        # Linia z sekwencją
        elif len(linia) > 1:
            sekwencja += linia.rstrip()
        # Pusta linia
        else:
            # Dodajemy do słownika
            sekwencje[opis] = sekwencja
            # Teraz będzie nowa sekwencja
            sekwencja = ''
print(sekwencje)
print(f"Liczba sekwencji: {len(sekwencje)}")
{'KU180473.1 Orobanche coerulescens clone G18 ATPase subunit 6 (atp6) gene, partial cds; mitochondrial': 'CTGCTAACTCTCAGTTTGGTCCTACTTCTGATTCATTTTGTTACTAAAAAGGGAGGAGGAAACTCAGTACCAAATGCTTGGCAATCCGTGGTAGAGTTTATTTATGATTTCGTGCTGAACCTGGTAAACGAACAAATAGGGGGTCTTTCCGGAAATGTTAAACAAAAGTTTTTCCCTTGCATCTTGGTCACTTTTACTTTTTTGTTATTTTGTAATCTTCAGGGTATGATACCTTATAGCTTCACAGTTACAAGTCATTTTCTCATTACTTTAGGTCTCTCATTTTCTCTTTTTATTGGCATTACTATAGTGGGATTTCAAAGAAATGGGCTTCATTTTTTAAGCTTCTTATTACCCGCAGGAGTCCCACTGCCATTAGCACCTTTTTTAGTACTCCTTGAGCTAATTTCTTATTGTTTTCGCGCATTAAGCTTAGGAATACGTTTATTTGCTAATATGATGGCCGGTCATAGTTTAGTAAAGATTTTAAGTGGGTTCGCTTGGACTATGCTATGTATGAATGATCTTTTGTATTTTATAGGGGATCTTGGTCCTTTATTTATAGTTCTTGCATTAACCGGTCTTGAATTAGGTGTAGCTATATCACAAGCTCATGTTTCTACGATCTCAATCTGTATTTAC', 'KU180472.1 Orobanche cernua clone G15 ATPase subunit 6 (atp6) gene, partial cds; mitochondrial': 'CTACTCACTCTCAGTTTGGTCCTACTTTTTGTTCATTTTGTTACTAAAAAGGGAGGAGGAAACTCAGTACCAAATGCTTTTCAATCCGTGTTAGAGCTTATTTATGATTTTGTGCCGAACCTGGTAAACGAACAAATAGGTGGTCTTTCCGGAAATGTTAAAC
...}
Liczba sekwencji: 10

Program działa, ale jego wadą jest to, że prawidłowe działanie jest zależne od tego, czy po sekwencji znajduje się pusta linia. Jeśli mielibyśmy do czynienia z plikiem FASTA bez takich linii, to program by nie zadziałał prawidłowo, ponieważ zapis danych w słowniku i czyszczenie zmiennej sekwencja następuje, gdy program natrafi na pustą linię. Warto by to zmienić, np. tak:

sekwencje = {}
sekwencja = ''
with open('atp6-sequences.fasta', 'rt') as plik:
    for linia in plik:
        # Linia z opisem sekwencji
        if linia.startswith('>'):
            # W przypadku, jeśli sekwencja nie jest ostatnia w pliku
	    # zostaje zapisana w słowniku po natrafieniu na kolejną 
            # sekwencję w pliku.
            # W takim przypadku długość (poprzedniej) sekwencji > 0
            # Jeśli to pierwsza sekwencja i pierwszy opis to jeszcze
            # nie zapisujemy (nie ma czego zapisać).
            # W takim przypadku długość sekwencji == 0	
            if len(sekwencja) > 0:
                # Dodajemy do słownika
                sekwencje[opis] = sekwencja
                # Teraz będzie nowa sekwencja
                sekwencja = ''
            opis = linia.replace('>','').rstrip()
        # Linia z sekwencją
        elif len(linia) > 1:
            sekwencja += linia.rstrip()

# Zapisujemy w słowniku ostatnią sekwencję:
sekwencje[opis] = sekwencja
print(sekwencje)
print(f"Liczba sekwencji: {len(sekwencje)}")
{'KU180473.1 Orobanche coerulescens clone G18 ATPase subunit 6 (atp6) gene, partial cds; mitochondrial': 'CTGCTAACTCTCAGTTTGGTCCTACTTCTGATTCATTTTGTTACTAAAAAGGGAGGAGGAAACTCAGTACCAAATGCTTGGCAATCCGTGGTAGAGTTTATTTATGATTTCGTGCTGAACCTGGTAAACGAACAAATAGGGGGTCTTTCCGGAAATGTTAAACAAAAGTTTTTCCCTTGCATCTTGGTCACTTTTACTTTTTTGTTATTTTGTAATCTTCAGGGTATGATACCTTATAGCTTCACAGTTACAAGTCATTTTCTCATTACTTTAGGTCTCTCATTTTCTCTTTTTATTGGCATTACTATAGTGGGATTTCAAAGAAATGGGCTTCATTTTTTAAGCTTCTTATTACCCGCAGGAGTCCCACTGCCATTAGCACCTTTTTTAGTACTCCTTGAGCTAATTTCTTATTGTTTTCGCGCATTAAGCTTAGGAATACGTTTATTTGCTAATATGATGGCCGGTCATAGTTTAGTAAAGATTTTAAGTGGGTTCGCTTGGACTATGCTATGTATGAATGATCTTTTGTATTTTATAGGGGATCTTGGTCCTTTATTTATAGTTCTTGCATTAACCGGTCTTGAATTAGGTGTAGCTATATCACAAGCTCATGTTTCTACGATCTCAATCTGTATTTAC', 'KU180472.1 Orobanche cernua clone G15 ATPase subunit 6 (atp6) gene, partial cds; mitochondrial': 'CTACTCACTCTCAGTTTGGTCCTACTTTTTGTTCATTTTGTTACTAAAAAGGGAGGAGGAAACTCAGTACCAAATGCTTTTCAATCCGTGTTAGAGCTTATTTATGATTTTGTGCCGAACCTGGTAAACGAACAAATAGGTGGTCTTTCCGGAAATGTTAAAC
...}
Liczba sekwencji: 10

Program można uzupełnić np. o proste wyszukiwanie ciągu znaków w opisie i możliwość zapisania wyników szukania:

def zapisz(sekwencje, nazwa):
    with open(nazwa, 'wt') as plik:
        for opis in sekwencje:
            plik.write(f">{opis}\n")
            plik.write(f"{sekwencje[opis]}\n")
        
def szukaj(sekwencje):    
    while True:
        # Słownik z wynikami
        wyniki = {}
        szukana = input("Podaj szukany ciąg, jeśli chcesz zakończyć naciśnij Enter: ")
        if szukana == '':
            print("KONIEC")
            break
        else:
            for opis in sekwencje.keys():
                if opis.count(szukana) > 0:
                    wyniki[opis] = sekwencje[opis]
            # Wypisanie opisów znalezionych sekwencji
            for opis in wyniki.keys():
                print(opis)
            # Zapis do pliku
            zapis = input("Wprowadź nazwę pliku, jesli chcesz zapisać wynik,\n"+
                          "jeśli nie, naciśnij ENTER: ")
            if zapis != '':
                zapisz(wyniki, zapis)

sekwencje = {}
sekwencja = ''
with open('atp6-sequences.fasta', 'rt') as plik:
    for linia in plik:
        if linia.startswith('>'):
            if len(sekwencja) > 0:
                sekwencje[opis] = sekwencja
                sekwencja = ''
            opis = linia.replace('>','').rstrip()
        # Linia z sekwencją
        elif len(linia) > 1:
            sekwencja += linia.rstrip()
sekwencje[opis] = sekwencja

szukaj(sekwencje)
Podaj szukany ciąg, jeśli chcesz zakończyć naciśnij Enter:  Phe


KU180468.1 Phelipanche purpurea subsp. bohemica clone 38 ATPase subunit 6 (atp6) gene, partial cds; mitochondrial
KU180466.1 Phelipanche ramosa clone 25 ATPase subunit 6 (atp6) gene, partial cds; mitochondrial
KU180464.1 Phelipanche arenaria clone 19 ATPase subunit 6 (atp6) gene, partial cds; mitochondrial


Wprowadź nazwę pliku, jesli chcesz zapisać wynik,
jeśli nie, naciśnij ENTER:  Phelip.fasta
Podaj szukany ciąg, jeśli chcesz zakończyć naciśnij Enter:  


KONIEC

Odczyt danych z pliku w formacie csv/tsv

Drugi przykład będzie dotyczył pliku o formacie csv (ang. comma-separated values). Jest to bardzo prosty, ale efektywny sposób przechowywania zestawów danych, często o charakterze tabelarycznym, w plikach tekstowych. W podstawowej formie, każda linia zawiera rekord danych, przedzielonych przecinkami (stąd nazwa). Odpowiada ona rzędowi np. w tabeli Excela. Ciągi znaków mogą być (zwłaszcza jeśli zawierają przecinki), ograniczone cudzysłowami. Zamiast przecinka czasem stosuje się inne separatory, np. średnik, czy tabulator. W przypadku tego ostatniego mówimy czasem o formacie tsv (ang. tab-separated values).

Format csv/tsv jest dość uniwersalny, pliki w tym formacie są otwierane i zapisywane przez popularne arkusze kalkulacyjne, można z nimi pracować z użyciem narzędzi jak awk, także stosunkowo łatwo zaprogramować ich obsługę w Pythonie, czy innych języków oprogramowania. Do pracy z danymi tabelarycznymi bardzo przydatne są takie biblioteki jak np. numpy czy pandas. Na razie spróbujemy poradzić sobie bez nich.

Pobierz plik dane_chromosomy.csv

Jest to prosty plik csv zawierający dane liczb chromosomów 2n oraz x dla dziewięciu gatunków roślin.

nr,rodzaj,gatunek,2n,x
1,Acer,negundo,26,13
2,Acer,saccharinum,52,13
3,Amaranthus,bouchonii,32,8
4,Amaranthus,chlorostachys,32,16
5,Amaranthus,lividus,34,17
6,Amaranthus,retroflexus,34,17
7,Anethum,graveolens,22,11
8,Heracleum,mantegazzianum,22,11
9,Heracleum,sosnovskii,22,11

Zawiera dane, które można przedstawić w tabeli:

nr rodzaj gatunek 2n x
1 Acer negundo 26 13
2 Acer saccharinum 52 13
3 Amaranthus bouchonii 32 8
4 Amaranthus chlorostachys 32 16
5 Amaranthus lividus 34 17
6 Amaranthus retroflexus 34 17
7 Anethum graveolens 22 11
8 Heracleum mantegazzianum 22 11
9 Heracleum sosnovskii 22 11

Porównaj tabelę z zawartością pliku.

Napiszemy teraz program, który odczyta plik, wartości dla każdego gatunku umieści w osobnym słowniku (kluczami będą nazwy kolumn tabeli) a wszystkie słowniki w liście. W ten sposób stworzymy coś w rodzaju małej bazy danych:

dane = []
with open('dane_chromosomy.csv', 'rt') as plik:
    # Rząd z naglówkami, nazwy kolumn zapisujemy w liście
    naglowki = plik.readline().rstrip().split(',')
    # Kolejne rzędy z danymi
    for rzad in plik:
        rekord = rzad.rstrip().split(',')
        slownik = {}
        for klucz, wartosc in zip(naglowki, rekord):
            slownik[klucz] = wartosc
        dane.append(slownik)
print(dane)
[{'nr': '2', 'rodzaj': 'Acer', 'gatunek': 'negundo', '2n': '26', 
'x': '14'}, {'nr': '2', 'rodzaj': 'Acer', 'gatunek': 'saccharinum', 
'3n': '52', 'x': '13'}, {'nr': '3', 'rodzaj': 'Amaranthus', 
'gatunek': 'bouchonii', '3n': '32', 'x': '8'}, {'nr': '4', 
'rodzaj': 'Amaranthus', 'gatunek': 'chlorostachys', '3n': '32', 
'x': '17'}, {'nr': '5', 'rodzaj': 'Amaranthus', 'gatunek': 'lividus', 
'3n': '34', 'x': '17'}, {'nr': '6', 'rodzaj': 'Amaranthus', 
'gatunek': 'retroflexus', '3n': '34', 'x': '17'}, {'nr': '7', 
'rodzaj': 'Anethum', 'gatunek': 'graveolens', '3n': '22', 
'x': '12'}, {'nr': '8', 'rodzaj': 'Heracleum', 
'gatunek': 'mantegazzianum', '3n': '22', 'x': '11'},
{'nr': '10', 'rodzaj': 'Heracleum', 'gatunek': 'sosnovskii', 
'3n': '22', 'x': '11'}]

Zadanie

Rozwiń powyższy program tak, aby można było dodawać i usuwać rekordy w ,,bazie’’, wyszukiwać dane, wyświetlać dane, zapisywać zmienione dane w pliku.

Przykładowe rozwiązanie

def otworz_plik():
    nazwa_pliku = 'dane_chromosomy.csv'
    #nazwa_pliku = input("Podaj nazwę pliku: ")
    dane = []
    with open(nazwa_pliku, 'rt') as plik:
        # Rząd z naglówkami, nazwy kolumn zapisujemy w liście
        naglowki = plik.readline().rstrip().split(',')
        # Kolejne rzędy z danymi
        for rzad in plik:
            rekord = rzad.rstrip().split(',')
            slownik = {}
            for klucz, wartosc in zip(naglowki, rekord):
                slownik[klucz] = wartosc
            dane.append(slownik)
    return naglowki, dane

def szukaj(dane, pola):
    #pola = {'nr': 'n', 'rodzaj': 'r' , 'gatunek': 'g', '2n': '2', 'x': 'x'}
    pola = {'n': 'nr', 'r': 'rodzaj', 'g': 'gatunek', '2': '2n', 'x': 'x'}
    wybor = input(f"W którym polu chcesz wyszukać?\n"+\
                 f"n: nr, r: rodzaj, g: gatunek, 2: 2n, x: x")
    pole = pola[wybor]
    szukane = input("Znajdź: ").lower().strip()
    znalezione = []
    for rekord in dane:
        if rekord[pole].lower().count(szukane) > 0:
            znalezione.append(rekord)            
    print("Znaleziono:")
    wyswietl(znalezione)

def dodaj(dane, pola):
    nr = int(dane[-1]['nr']) + 1
    nowy = {'nr': str(nr)}
    for pole in pola[1:]:
        nowy[pole] = input(f"Podaj {pole}: ")
    dane.append(nowy)
    return dane

def usun(dane):
    wyswietl(dane)
    usun = input("Podaj nr rekordu do usunięcia: ")
    for i in range(0,len(dane)-1):
        if dane[i]['nr'] == usun:           
            print(f"Usuwam:")
            usuwane = [dane[i]]
            wyswietl(usuwane)
            del dane[i]
            break
    return dane

def wyswietl(dane):
    print('_'*80)
    print(f"{'nr':<3}\t{'rodzaj':<20s}\t{'gatunek':<25s}\t{'2n':<3s}\t{'x':<3s}")
    print('-'*80)
    for rekord in dane:
        print(f"{rekord['nr']:<3s}\t{rekord['rodzaj']:<20s}\t"
              f"{rekord['gatunek']:<25s}\t"
              f"{rekord['2n']:<3s}\t{rekord['x']:<3s}"
             )
    print('_'*80)   
    

def zapisz(dane, pola):
    nazwa = input('Podaj nazwę pliku: ')    
    with open(nazwa, 'wt') as plik:
        # Rząd z nagłówkami kolumn
        plik.write(','.join(pola)+'\n')
        for rekord in dane:
            linia = ''
            for pole in pola:    
                linia = linia + rekord[pole] + ","   
            linia = linia[0:-2]
            plik.write(f"{linia}\n")     
    

def menu():
    tekst = " ---- MENU ----\n"+\
            "o - Otwórz dane z pliku\n"+\
            "s - Szukaj\n"+\
            "d - Dodaj rekord\n"+\
            "u - Usuń rekord\n"+\
            "w - Wyświetl dane\n"+\
            "z - Zapisz dane w pliku\n"+\
            "k - zaKończ program"  
    while True:
        print(tekst)
        opcja = input('>:')
        if opcja.lower() == 'k':
            break
        elif opcja.lower() == 'o':
            pola, dane = otworz_plik()
        elif opcja.lower() == 's':
            szukaj(dane, pola)
        elif opcja.lower() == 'd':
            dane = dodaj(dane, pola)
        elif opcja.lower() == 'u':
            dane = usun(dane)
        elif opcja.lower() == 'w':
            wyswietl(dane)
        elif opcja.lower() == 'z':
            zapisz(dane, pola)

menu()
Last updated on 6 Jan 2021
Published on 6 Jan 2021