Filer og CSV


Skrive til og lese fra fil

Vi kan åpne en fil ved å bruke syntaksen with ... as ... sammen med funksjonen open. Denne 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 metoden write på filobjektet, mens for å lese fra filen bruker vi metoden read på filobjektet.

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

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

En mer moderne måte å gjøre det samme på, hvor vi benytter Path-objekter fra pathlib-modulen i stedet for open-funksjonen, ser slik ut:

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.

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. F. eks. 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 hver verdi representerer en kolonne i tabellen.

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 biblioteker i Python som er spesielt laget for å lese CSV-filer, 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')

# .strip fjerner whitespace på begynnelsen og slutten av strengen
content_string = content_string.strip() 

# .split('\n') klipper opp strengen ved linjeskift, og gir oss en
# liste med bitene som er igjen
content_lines = content_string.split('\n')

# 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
    values = line.split(',')
    table.append(values)


# Vi kan nå aksessere enkeltverdier i tabellen vår ved å bruke
# indeksering på samme måte som vi gjør med andre 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 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_a_year_later.csv').write_text(result_string, encoding='utf-8')