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:
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:
Navn | Alder | Høyde |
---|---|---|
Ola | 20 | 1.80 |
Per | 21 | 1.73 |
Eva | 20 | 1.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 finnescsv
-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:
delimiter
: Skilletegnet mellom cellene. Standard er komma (,
). Semikolon, mellomrom og tabulator er også vanlige.quotechar
: Sitattegn som brukes for å omslutte celler som inneholder skilletegnet. Standard er anførselstegn ("
). Noen ganger brukes apostrof.lineterminator
: Skilletegnet mellom radene. Standard er\r\n
for skriving, og enten\r\n
eller\n
for lesing.
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'