Filer og CSV

Tekstfiler generelt

Komma-separerte verdier (CSV)


Skrive til og lese fra fil

Den enkleste måten å lese og skrive filer er ved å bruke den innbygde modulen pathlib. Dette virker i Python 3.4 og nyere, og er den metoden vi anbefaler for de fleste tilfeller.

from pathlib import Path

# Skrive til en fil
content_string_a = 'Hei, verden!'
Path('minfil.txt').write_text(content_string_a, encoding='utf-8')

# Lese fra en fil
content_string_b = Path('minfil.txt').read_text(encoding='utf-8')
print(content_string_b) # Skriver ut 'Hei, verden!'

Den navngitte parameteren encoding= bør alltid spesifiseres, ellers kan du få problemer når programmet kjøres på et annet operativsystem. Dersom du skriver til en fil, bør du alltid spesifisere encoding='utf-8'. Dersom du leser fra en fil, må du benytte samme koding som ble brukt da filen ble skrevet. Les mer i kursnotater om unicode.

Det er fremdeles mange som benytter with ... as ... -strukturen sammen med funksjonen open i stedet for å benytte pathlib -modulen for å lese og skrive til filer. Fordeler med denne metoden er at den er mer fleksibel for ekspert-bruk, og du trenger ikke å importere noe for å bruke den. Ulempen er at den er litt mer kompleks og tar større plass å skrive.

Open-funksjonen tar inn et filnavn/sti til en fil samt en modus og returnerer et «filobjekt». Filobjektet kan vi bruke for å lese fra eller skrive til filen. For å skrive til filen bruker vi modus ‘wt’ og metoden write på filobjektet, mens for å lese fra filen bruker vi modus ‘rt’ og metoden read på filobjektet.

# Skrive til en fil
with open('minfil.txt', 'wt', encoding='utf-8') as fileobj:
    fileobj.write('Hei, verden!')

# Lese fra en fil
with open('minfil.txt', 'rt', encoding='utf-8') as fileobj:
    content = fileobj.read()
print(content) # Skriver ut 'Hei, verden!'

Noen vanlige moduser for filobjektet er:

  • 'r': åpner filen for lesing (read)
  • 'w': åpner filen for skriving (write). Hvis filen ikke eksisterer, opprettes den. Hvis filen eksisterer, overskrives den.
  • 'a': åpner filen for skriving (append). Hvis filen ikke eksisterer, opprettes den. Hvis filen eksisterer, legges det nye innholdet til på slutten av filen.
  • 'x': åpner filen for skriving (exclusive). Hvis filen ikke eksisterer, opprettes den. Hvis filen eksisterer fra før, krasjer programmet.

I tillegg kan vi spesifisere om filen skal åpnes i tekstmodus eller binærmodus ved å legge til 't' eller 'b' i modus-strengen. For eksempel 'wt' eller 'wb'.

  • 't': åpner filen i tekstmodus (text). Dette er standard, og trenger derfor egentlig ikke å spesifiseres. Benytt denne modusen hvis du skal lese eller skrive tekst, som f. eks. .txt, .csv, .json, .html, .xml, .py etc.
  • 'b': åpner filen i binærmodus (binary). Dette er nødvendig hvis du skal lese eller skrive binære filer (f. eks. bilder, lyd, video, etc.).

Hjelp, filen blir ikke funnet

Når du kjører et Python-program, kjører programmet «i» en mappe som kalles current working directory (cwd). Du kan se hvilken mappe dette er med koden:

from pathlib import Path
cwd = Path.cwd()
print(cwd)

Denne mappen blir bestemt av programmet som starter Python. For eksempel hvis du bruker VSCode for å starte Python, vil terminalen være i den samme mappen som VSCode er åpnet i (den som er nevnt med STORBOKSTAVER i filutforskeren til venstre). Cwd har altså ikke noen sammenheng med hvilken mappe filen som kjøres ligger i.

Når Python får beskjed om å åpne en fil, vil den tolke filstien som blir oppgitt relativt til cwd. For eksempel, hvis filstien er kun et filnavn, antas det at filen ligger i cwd.

La oss si at vi har følgende filstruktur:

topfolder/
    foo.txt
    subfolder/
        myscript.py
        qux.txt

I skriptet myscript.py har vi følgende kode:

Path('bar.txt').write_text('Hello from bar.txt!', encoding='utf-8')

Hvor vil da filen bar.txt bli opprettet? Svaret er: det kommer an på.

  • Hvis du kjører myscript.py fra topfolder (altså hvis cwd er topfolder), vil filen bar.txt bli opprettet i topfolder.
  • Hvis du kjører myscript.py fra subfolder (altså hvis cwd er subfolder), vil filen bar.txt bli opprettet i subfolder.

Dersom du kjører programmet fra VSCode, vil cwd være den mappen du har åpnet VSCode i. Hvis vi har åpnet VSCode i topfolder, ser filutforskeren i VSCode slik ut:

Illustrasjon av filutforskeren i VSCode hvis programmet har åpnet topfolder

Da vil filen bar.txt bli opprettet i mappen topfolder – til tross for at kildekoden befinner seg i mappen subfolder.

La oss si at vi har følgende filstruktur:

inf100/
    lab5/
    lab6/
    lab7/
        check_valid_word.py
        wordlist.txt

I skriptet check_valid_word.py har vi følgende kodelinje:

content = Path('wordlist.txt').read_text(encoding='utf-8')

Programmet krasjer med følgende feilmelding:

Traceback (most recent call last):
  File "/path/to/inf100/lab7/check_valid_word.py", line 2, in <module>
    content = Path('wordlist.txt').read_text(encoding='utf-8')
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/torsteins/.pyenv/versions/3.11.4/lib/python3.11/pathlib.py", line 1058, in read_text
    with self.open(mode='r', encoding=encoding, errors=errors) as f:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/torsteins/.pyenv/versions/3.11.4/lib/python3.11/pathlib.py", line 1044, in open
    return io.open(self, mode, buffering, encoding, errors, newline)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'wordlist.txt'

Hva er feilen? Svaret er sannsynligvis: du kjører programmet fra feil mappe (cwd er altså ikke /path/to/inf100/lab7). Kan det for eksempel være at du kjører programmet fra inf100 -mappen?

Feilsøkingssteg:

  • Sjekk hva cwd er ved å legge til følgende linje øverst i programmet: print(Path.cwd())

  • Hvis cwd ikke er /path/to/inf100/lab7: åpne VSCode i lab7 -mappen (File -> Open Folder -> velg lab7 -mappen) og kjør programmet nå.

  • Alternativt kan du navigere til lab7 -mappen med kommandoer i terminalen og kjøre programmet derfra.

Det er mulig å programmatisk endre cwd til å bli samme mappe som filen som kjøres ligger i:

import os
directory_of_current_file = os.path.dirname(__file__)
os.chdir(directory_of_current_file) # endrer cwd

Dette kan kanskje gjøre ting lettere i utviklingsfasen og for raske og enkle formål, men er sannsynligvis ikke noe en erfaren programmerer ville ønsket seg, siden man da må flytte hele programmet hvis man vil bruke det i en annen mappe.

Enkel CSV -håndtering

En CSV-fil er en tekstfil som inneholder tabell-data. CSV står for «comma separated values», og det er nettopp det det er: en tekstfil hvor hver linje inneholder en rekke verdier som er separert med komma (eller et annet symbol). Hver linje i filen representerer en rad i tabellen, og skille-symbolet indikerer hvor skillet mellom de ulike kolonnene i tabllene er.

Regneark i Microsoft Excel eller Google Sheets kan lagres som CSV-filer. Dette er et vanlig format for å utveksle data mellom ulike programmer.

Innholdet i en CSV-fil (people.csv) kan se slik ut:

Navn,Alder,Høyde
Ola,20,1.80
Kari,19,1.65
Per,21,1.73
Oda,20,1.74

Det finnes en modul i standardbiblioteket som er spesielt laget for å lese CSV-filer (se avsnitt lengre nede), men i dette avsnittet skal vi vise hvordan vi kan lese dem helt selv. En CSV-fil er nemlig bare en tekstfil, og vi kan lese den på akkurat samme måte som vi leser andre tekstfiler.

from pathlib import Path

###########################################
### LESE INPUT OG OPPRETTE DATASTRUKTUR ###
###########################################

# Les inn innholdet i filen 'people.csv' som en streng
content_string = Path('people.csv').read_text(encoding='utf-8')

# .splitlines klipper opp strengen ved linjeskift, og gir oss en
# liste med bitene som er igjen; altså radene i tabellen
content_lines = content_string.splitlines()

# Vi oppretter en 2D-liste (en liste av lister) som skal inneholde
# tabellen vår
table = []
for line in content_lines:
    # .split(',') klipper opp strengen ved komma, og gir oss en
    # liste med bitene som er igjen
    row = line.split(',')
    table.append(row)


# Vi kan nå aksessere enkeltverdier i tabellen vår ved å bruke
# indeksering på samme måte som vi gjør med andre 2D-lister
print(table[0][1]) # Alder
print(table[1][0]) # Ola
print(table[3][2]) # 1.73

# Ofte gir det mening å ha overskriftene og selve dataene i separate
# variabler.
headers = table[0] # første rad
data = table[1:]  # alle rader unntatt den første

##############################################
### UTFØR SELVE DATABEHANDLINGEN VI ØNSKER ###
##############################################

# Et år har passert! Øk alle aldre med 1 i datasettet.
for row in data:
    row[1] = 1 + int(row[1]) # PS: dette endrer typen fra string til int


############################
### PRESENTER RESULTATET ###
############################
    
result_headers = headers
result_headers_string = ','.join(result_headers)
result_lines = [result_headers_string]
for row in data:
    string_row = [str(x) for x in row]
    line_string = ','.join(string_row)
    result_lines.append(line_string)
result_string = '\n'.join(result_lines) + '\n'

Path('people_one_year_later.csv').write_text(result_string, encoding='utf-8')
CSV som 2D -liste eller liste av oppslagsverk

Når vi skal representere tabell-data i våre Python-programmer, finnes det i hovedsak to muligheter som ofte brukes. Vi illustrerer ved eksempel. La oss anta at vi har følgende tabell:

NavnAlderHøyde
Ola201.80
Per211.73
Eva201.74

Vi kan representere tabellen som en CSV-fil:

'Navn','Alder','Høyde'
'Ola',20,1.80
'Per',21,1.73
'Eva',20,1.74

I Python -programmet vårt kan vi representere samme data som en 2D-liste:

table = [
    ['Navn', 'Alder', 'Høyde'],
    ['Ola', 20, 1.80],
    ['Per', 21, 1.73],
    ['Eva', 20, 1.74],
]

eller (mer hensiktsmessig) en 2D-liste med data pluss en vanlig liste med overskriftene:

headers = ['Navn', 'Alder', 'Høyde']
data = [
    ['Ola', 20, 1.80],
    ['Per', 21, 1.73],
    ['Eva', 20, 1.74],
]

Den siste representasjonen vi vil vise, er å representere innholdet som en liste av oppslagsverk:

headers = ['Navn', 'Alder', 'Høyde']
data = [
    {'Navn': 'Ola', 'Alder': 20, 'Høyde': 1.80},
    {'Navn': 'Per', 'Alder': 21, 'Høyde': 1.73},
    {'Navn': 'Eva', 'Alder': 20, 'Høyde': 1.74},
]

I mange tilfeller er det denne sistnevnte representasjonen som er å foretrekke. Denne representasjonen er lettere å lese og mer robust for endringer. Det er litt knotete (men slett ikke umulig) å konvertere en string til dette formatet på egen hånd; men vi kan også bruke csv-modulen fra Python sitt standardbibliotek.

CSV -modulen

Grunnleggende håndtering av enkle CSV-filene kan vi gjøre med .split og .join som vi viser i notatene om enkel CSV-håndtering over. Men i noen tilfeller kan det bli mer komplisert: for eksempel når innholdet i en celle inkluderer selve skillesymbolet (komma). For å løse dette, blir innholdet i en celle som inneholder skillesymbolet omsluttet av et sitattegn for å angi hvor en celle begynner og slutter. Og hva om innholdet i cellen også inneholder selve sitattegnet? Dette løses ved å la to sitattegn på rad regnes som selve sitattegn-symbolet. Uansett: koden for å tolke slike kompliserte CSV-filer blir nokså komplisert. Heldigvis finnes csv-modulen som en del standardbiblioteket i Python, slik at vi slipper å forholde oss til detaljene

Når vi tolker en CSV-fil må vi ta hensyn til at følgende parametre er konfigurert riktig:

Merk at parametrene delimiter, quotechar og lineterminator skal angis når «leseobjektet» (eller «skriveobjektet») opprettes. Dersom de ikke angis, benyttes standardverdiene som nevnt over.

CSV <—> Liste med oppslagsverk

# Fra CSV-formattert streng til liste med oppslagsverk
import csv
import io

# Eksempelstreng
my_string = '''\
'Navn','Alder','Høyde'
'Ola',20,1.80
'Per',21,1.73
'Eva',20,1.74
'''

# Konvertering til liste av oppslagsverk
reader = csv.DictReader(io.StringIO(my_string), delimiter=',', quotechar="'")
headers = reader.fieldnames
data = list(reader)

# headers = ['Navn', 'Alder', 'Høyde']
# data = [
#   {'Navn': 'Ola', 'Alder': '20', 'Høyde': '1.80'},
#   {'Navn': 'Per', 'Alder': '21', 'Høyde': '1.73'},
#   {'Navn': 'Eva', 'Alder': '20', 'Høyde': '1.74'},
# ]

# PS: merk at alle nøkler og verdier alle er strenger
# Fra liste med oppslagsverk til CSV-formattert streng
import csv
import io

# Eksempeldata
headers = ['Navn', 'Alder', 'Høyde']
data = [
    {'Navn': 'Ola', 'Alder': 20, 'Høyde': 1.80},
    {'Navn': 'Per', 'Alder': 21, 'Høyde': 1.73},
    {'Navn': 'Eva', 'Alder': 20, 'Høyde': 1.74},
]

# Konvertering til CSV-streng
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=headers, lineterminator='\n')
writer.writeheader()
writer.writerows(data)
my_string = output.getvalue()

# my_string = 'Navn,Alder,Høyde\nOla,20,1.8\nPer,21,1.73\nEva,20,1.74\n'

CSV <—> 2D-liste

# Fra CSV-formattert streng til 2D-liste
import csv
import io

# Eksempelstreng
my_string = '''\
'Navn','Alder','Høyde'
'Ola',20,1.80
'Per',21,1.73
'Eva',20,1.74
'''

# Konvertering til 2D-liste
reader = csv.reader(io.StringIO(my_string), delimiter=',', quotechar="'")
table = list(reader)
headers = table[0]
data = table[1:]

# headers = ['Navn', 'Alder', 'Høyde']
# data = [
#   ['Ola', '20', '1.80'],
#   ['Per', '21', '1.73'], 
#   ['Eva', '20', '1.74'],
# ]

# PS: merk at alle verdier alle er strenger
# Fra 2D-liste til CSV-formattert streng
import csv
import io

# Eksempeldata
headers = ['Navn', 'Alder', 'Høyde']
data = [
    ['Ola', 20, 1.80],
    ['Per', 21, 1.73],
    ['Eva', 20, 1.74],
]

# Konvertering til CSV-streng
output = io.StringIO()
writer = csv.writer(output, delimiter='&', lineterminator='\n')
writer.writerow(headers)
writer.writerows(data)
my_string = output.getvalue()

# my_string = 'Navn&Alder&Høyde\nOla&20&1.8\nPer&21&1.73\nEva&20&1.74\n'