Lektion 7: Arbeta med text#
Begrepp som diskuteras: Strängar, lexikon, sorteringar, reguljära uttryck.
Arbetssätt: Arbeta gärna tillsammans med någon, men skriv egen kod. Diskutera med varandra! Försök göra övningarna innan du tittar på svaren! Fråga handledarna om det är något svar du inte förstår!
Uppskattad arbetstid: 8 timmar.
Redovisning: Obligatorisk redovisning av uppgifter enligt kurssidan.
Textsträngar#
Hittills har vi huvudsakligen använt textsträngar (typen str
) som
variabelvärden och i utskrifter. I denna lektion ska vi igenom strängar mer detaljerat.
Mycket av det vi gjort med listor kan vi göra med strängar. Strängar är dock, precis som tupler, oföränderliga. Vi kan alltså inte ändra en sträng, men vi kan ofta skapa en ny sträng som är en modifierad version av en annan sträng.
Läs igenom minilektionen om strängar för att se några vanliga operationer på strängar.
Vi definierar en sträng att använda i vår exempelkod:
text = """Kvällens gullmoln fästet kransa.
Älvorna på ängen dansa,
och den bladbekrönta näcken
gigan rör i silverbäcken.
"""
Denna sträng innehåller bokstäver, skiljetecken, blanktecken och radslutstecken. Radslutstecknen syns normalt inte men gör så att texten blir raduppdelad vid utskrift:
>>> print(text)
Kvällens gullmoln fästet kransa.
Älvorna på ängen dansa,
och den bladbekrönta näcken
gigan rör i silverbäcken.
>>> text
'Kvällens gullmoln fästet kransa.\nÄlvorna på ängen dansa,\noch den bladbekrönta näcken\ngigan rör i silverbäcken.\n'
Räkna tecken och strängar#
Kod |
Värde |
Kommentar |
---|---|---|
len(text)
|
111 |
Totala antalet tecken. |
text.count(' ')
|
12 |
Antalet blanktecken. |
text.count('n ')
|
4 |
Observera att ’n’ i slutet av raderna inte följs av ett blanktecken. |
text.count('\n')
|
4 |
Antalet rader (egentligen antalet radslutstecken — sista raden behöver inte nödvändigtvis följas av ett radslutstecken). |
Räkna bokstäver#
Antag att vi vill räkna antalet bokstäver i texten. Ett uppenbart sätt att göra det på är att gå igenom strängen tecken för tecken och räkna de tecken som är bokstäver.
Anropet s.isalpha()
returnerar True
om strängen s
bara
innehåller bokstäver.
# Räkna bokstäver
count = 0
for character in text: # character kommer successivt innehålla alla tecken i text
if character.isalpha(): # Räkna om character är en bokstav
count += 1
print(f'Antal bokstäver: {count}')
(Anmärkning: Alternativ kod med ”list comprehension”:
count = len([character for character in text if character.isalpha()])
print(f'Antal bokstäver: {count}')
Prova och se att den fungerar!)
Räkna ”svenska” bokstäver#
Metoden isalpha
fungerade uppenbarligen även för svenska bokstäver och
den fungerar även för ett stort antal andra nationella tecken som t ex
ü, é, á, è, î, Ç, ô, och Á.
Det finns ingen metod ”isswedish
” motsvarande isalpha
utan vi får
använda operatorn in
för att se om ett tecken är å, ä eller ö:
# Räknar 'svenska' bokstäver
count = 0
for character in text:
if character.lower() in 'åäö':
count += 1
print(f'Antal svenska bokstäver: {count}')
Vi har använt metoden lower
som returnerar en sträng där alla
versaler är utbytta mot motsvarande gemena.
Det hade naturligtvis gått lika bra att istället testa mot strängen
'åäöÅÄÖ'
.
Övning: Försök lösa även denna uppgift med hjälp av listbyggare. Du kan utgå från den alternativa lösningen för att räkna bokstäver ovan.
Övning#
Skriv kod som räknar och skriver ut antalet nationella bokstäver dvs
bland annat å, ä, ö, ü, é, á, è, î, Ç, ô, Á, …
Ledning: Det är lättare att känna till de internationella
bokstäverna än alla nationella!
Svar
test_text = 'aVcdåäöÅÄÖüéáèîçÇôÜÁxyZX'
iletters = 0
letters = 0
for character in test_text:
if character.isalpha():
letters += 1
if character.lower() in 'abcdefghijklmnopqrstuvwxyz':
iletters += 1
print(f'Antal nationella bokstäver: {letters - iletters}')
Byta ut tecken#
Antag att vi vill ”internationalisera” texten genom att byta ut å mot aa, ä mot ae och ö mot oe.
Metod 1: Metoden replace
#
txt = text.replace('å', 'aa')
txt = txt.replace('ä', 'ae')
txt = txt.replace('ö', 'oe')
txt = txt.replace('Å', 'Aa')
txt = txt.replace('Ä', 'Ae')
txt = txt.replace('Ö', 'Oe')
print(txt)
Kom ihåg att strängar är oföränderliga! Vi måste alltså ta emot den nya
sträng som replace
returnerar.
Metod 2: Använd ett lexikon#
Vi kan skapa en översättningstabell med hjälp av ett lexikon (”dictionary” - se minilektionen om lexikon!) Vi lägger in de svenska bokstäverna som nycklar och motsvarande internationella teckenkombinationer som värden:
translation_dict = {'å': 'aa', 'ä': 'ae', 'ö': 'oe',
'Å': 'Aa', 'Ä': 'Ae', 'Ö': 'Oe'}
new_text = ''
for character in text: # Iterera över tecknen
if character in translation_dict: # Om tecknet character finns som nyckel
new_text += translation_dict[character] # lägg till översättningen av tecknet
else: # annars
new_text += character # lägg till tecknet som det är
print(new_text)
Räkna bokstavsförekomster#
Antag att vi vill räkna hur många gånger varje bokstav förekommer i en text. Ett lexikon med enskilda bokstäver som nycklar och antal förekomster som värde fungerar bra:
count = {} # Skapa ett tomt lexikon
for character in text: # Iterera över tecknen
if character.isalpha(): # Vi intresserar oss bara för bokstäver
character = character.lower() # Skiljer inte på små och stora
if character in count: # Om denna bokstav redan finns som nyckel
count[character] += 1 # Öka dess frekvens
else: # annars
count[character] = 1 # Lägg in den med frekvensen 1
print(count)
Resultat:
{'k': 5, 'v': 3, 'ä': 6, 'l': 8, 'e': 8, 'n': 13, 's': 5, 'g': 4, 'u': 1, 'm': 1, 'o': 3, 'f': 1, 't': 3, 'r': 6, 'a': 8, 'p': 1, 'å': 1, 'd': 3, 'c': 3, 'h': 1, 'b': 3, 'ö': 2, 'i': 3}
Utskriften är inte särskilt läsbar. Dels borde den vara ordnad antingen i bokstavsordning eller i frekvensordning och dels borde den vara uppdelad på flera rader.
I minilektionen om lexikon beskrivs
flera sätt att iterera över ett lexikon. I detta fall väljer vi att
först skapa en lista av tupler med nyckel-värdepar som vi sorterar med
hjälp av listobjektets sort
-metod:
count_lst = list(count.items())
print('Lista: ', count_lst)
count_lst.sort()
print('Sorterad lista:', count_lst)
vilket ger utskrifterna:
Lista: [('k', 5), ('v', 3), ('ä', 6), ('l', 8), ('e', 8), ('n', 13), ('s', 5), ('g', 4), ('u', 1), ('m', 1), ('o', 3), ('f', 1), ('t', 3), ('r', 6), ('a', 8), ('p', 1), ('å', 1), ('d', 3), ('c', 3), ('h', 1), ('b', 3), ('ö', 2), ('i', 3)]
Sorterad lista: [('a', 8), ('b', 3), ('c', 3), ('d', 3), ('e', 8), ('f', 1), ('g', 4), ('h', 1), ('i', 3), ('k', 5), ('l', 8), ('m', 1), ('n', 13), ('o', 3), ('p', 1), ('r', 6), ('s', 5), ('t', 3), ('u', 1), ('v', 3), ('ä', 6), ('å', 1), ('ö', 2)
I minilektionen om listor beskrivs hur man kan dela upp utskriften av en lista på flera rader:
for index, element in enumerate(count_lst, start=1):
print(element, end=' ')
if index % 8 == 0:
print()
print('\n')
som ger utskriften:
('a', 8) ('b', 3) ('c', 3) ('d', 3) ('e', 8) ('f', 1) ('g', 4) ('h', 1)
('i', 3) ('k', 5) ('l', 8) ('m', 1) ('n', 13) ('o', 3) ('p', 1) ('r', 6)
('s', 5) ('t', 3) ('u', 1) ('v', 3) ('ä', 6) ('å', 1) ('ö', 2)
Det blir dock snyggare om man tar ut komponenterna ur tuplarna:
for index, element in enumerate(count_lst, start=1):
print(f'{element[0]}:{element[1]:2d}', end=' ')
if index % 8 == 0:
print()
print('\n\n')
som ger:
a: 8 b: 3 c: 3 d: 3 e: 8 f: 1 g: 4 h: 1
i: 3 k: 5 l: 8 m: 1 n:13 o: 3 p: 1 r: 6
s: 5 t: 3 u: 1 v: 3 ä: 6 å: 1 ö: 2
Sortering på antal förekomster#
När man, som vi gjort ovan, sorterar en lista med tupler så sker sorteringen på det första värdet i tupeln (dvs på bokstaven i detta fall). För att sortera på antal förekomster måste man, på något sätt, ange att det är antal förekomster som ska användas som sorteringsnyckel, inte bokstaven.
Både till standardfunktionen sorted
och till metoden sort
för
list-objekt kan man skicka med en funktion som anger hur komponenterna
ska jämföras. Om listelementen består av tupler (som i vårt fall)
skriver vi en funktion:
def part2(element):
return element[1]
Funktionen returnerar alltså den andra komponenten i tupeln. För att sortera på antal förekomster kan vi nu skriva:
sorted_by_count = sorted(count_lst, key=part2, reverse=True)
for index, element in enumerate(sorted_by_count, start=1):
print(f'{element[1]:2d} {element[0]}', end='\t')
if index % 6 == 0:
print()
print()
som ger utskriften:
13 n 8 a 8 e 8 l 6 r 6 ä
5 k 5 s 4 g 3 b 3 c 3 d
3 i 3 o 3 t 3 v 2 ö 1 f
1 h 1 m 1 p 1 u 1 å
Observera:
Vi ger funktions-namnet som värde till
key
-parametern. Sorteringsfunktionen kommer att anropa denna funktion för varje element i listan för att få fram ett ”sorteringsvärde” för elementet.Parametern
reverse=True
anger att resultatet ska bli i fallande ordning, dvs. med största värdet först.Vi har använt tabulatorteckenet
'\t'
som värde påend
-parametern tillprint
.
Övningar#
Sätt
phonetic_alphabet = ['alpha', 'bravo', 'charlie', 'delta', 'echo', 'foxtrot']
Vad blir resultatet avphonetic_alphabet.sort(key=part2)
?Svar
En lista sorterad på den andra bokstaven i orden:
['echo', 'delta', 'charlie', 'alpha', 'foxtrot', 'bravo']
Ett annat sätt att åstadkomma sorteringen på antal förekomster är att utifrån variabeln
count_lst
göra en ny lista där man byter ordning på antal förekomster och bokstav och sedan sorterar. Skriv koden för detta!Svar
För den som läst på om ”list comprehensions” går det elegant på detta sätt
swapped = sorted([(elem[1], elem[0]) for elem in count_lst], reverse=True)
Det går naturligtvis också att bygga upp listan med en for-loop:
swapped = [] for elem in count_lst: swapped.append((elem[1], elem[0])) swapped.sort(reverse=True)
Ordanalyser#
Hittills har det mesta handlat om att göra saker med enskilda bokstäver men nu ska vi göra liknande analyser av orden i en text.
Att hitta ord i en text - reguljära uttryck#
Det första problemet är att lokalisera ord i en sträng. I minilektionen
om strängar nämns en metod split
som vid
första anblicken verkar användbar. Minlektionens exempel är koden
'Ta det lugnt'.split(' ')
som returnerar en lista med de ord som ingår i strängen:
['Ta', 'det', 'lugnt']
.
Om vi använder den metoden på Stagnelius text i början av lektionen, dvs
om vi gör print(text.split(' '))
, får vi följande nedslående utskrift:
['Kvällens', 'gullmoln', 'fästet', 'kransa.\nÄlvorna', 'på', 'ängen',
'dansa,\noch', 'den', 'bladbekrönta', 'näcken\ngigan', 'rör', 'i',
'silverbäcken.\n']
Det blev 13 ”ord” i stället för 16. Uppdelningen görs enbart av blanktecken. Punkter, kommatecken och radbyten ingår i ”orden”.
Vi behöver något sätt att tala om att de ”ord” vi är ute efter enbart får innehålla bokstäver. I andra sammanhang, t.ex. vid analys av texten i ett Python-program, vill vi kanske tillåta understrykningstecknen att ingå i orden.
Det finns ett mycket generellt verktyg som kan hjälpa oss med detta: reguljära uttryck. Med sådana kan man definiera mönster och leta efter dessa i en text. Matchande delar av texten kan också bytas ut på intrikata sätt.
Reguljära uttryck är inte alls begränsade till Python utan kan hanteras av många programmeringsspråk och editorer.
I denna kurs tar vi bara upp några mycket enkla exempel men råder den som ska arbeta med text att sätta sig in i den världen. Det finns mycket material på nätet.
För att plocka ut ord i betydelsen ”en följd av en eller flera bokstäver” gör vi på följande sätt:
import re
wordlist = re.findall(r'[a-zA-ZåäöÅÄÖ]+', text)
print(wordlist)
som ger följande utskrift:
['Kvällens', 'gullmoln', 'fästet', 'kransa',
'Älvorna', 'på', 'ängen', 'dansa',
'och', 'den', 'bladbekrönta', 'näcken',
'gigan', 'rör', 'i', 'silverbäcken']
Kommentarer:
Reguljära uttryck definieras i paketet
re
som alltså importeras.Parametern till
re.findall
är en ”rå sträng” dvs den börjar medr'
(ellerr"
) i stället för med bara'
(eller"
). I en rå sträng tolkas inte escape-tecknet på vanligt sätt (tab, newline, …) utan behålls och har andra betydelser i reguljära uttryck.Uttrycket
[a-zA-ZåäöÅÄÖ]
matchar vilket tecken som helst mellan a och z, mellan A och Z samt å, ä, ö och Å, Ä, Ö dvs en internationell eller ’svensk’ bokstav.Tecknet
+
efter[]
-gruppen anger att det kan vara en eller flera bokstäver.Mönstret matchar helt enkelt en obruten följd av en eller flera bokstäver.
Mönstermetoden
findall
returnerar alltså en lista av alla matchande sekvenser.
Obligatorisk uppgift#
Skriv ett program som läser en textfil och producerar statistik. Du kan själv skapa en fil som programmet ska läsa.
Programmet ska skriva ut det totala antalet ord och antalet olika ord.
Programmet ska också ge en lista på de n vanligaste orden och de m
ovanligaste orden. Värdena på m och n ska läsas in med input
. Se
minilektionen in- och utmatning som beskriver hur
man läser en fil.
Nedan finns en dikt skriven av ChatGPT. Kopiera in den i en textfil och spara den (lämpligen med ändelse .txt) i samma mapp som du arbetar. Om du kör ditt program med denna textfil ska du få en utskrift som liknar den nedan.
dikt av ChatGPT
I kodens värld, där rader dansar fria,
Python's makt och styrka härskar som kung,
Ett språk så enkelt, vackert och skönt,
Det leder oss genom varje kodad dröm.
Indenterade block som versens rytm,
Struktur och flyt, i harmonisk ström,
"Om" och "annars", beslutets sång,
I Pythons famn, de hittar sin gång.
Loopar som en flod som aldrig stannar,
Genom datans landskap, Python spannar,
För loopar, medan loopar, de sjunger sin sång,
Itererar genom listor, där värden hör hemma så trång.
Funktioner som stjärnor som lyser klart,
Uppgifter omslutna med konstnärlig fart,
Med "def" och "return", deras essens ren,
Modulär symfoni för alla, vi ser och vi känner.
Objekt och klasser, en värld att skapa,
I Pythons rike, de samverkar, tror jag,
Attribut och metoder, som karaktärer så fina,
Skapar en mosaik, fylld av mening och skina.
Bibliotek rika, en skattkista så stor,
Lösningar och verktyg som öppnar varje dörr,
NumPy, pandas, och matplotlibs prakt,
Fyller Pythons rymd, med kraft som är faktiskt.
Från webb till data, AI och mer,
Python är mångsidigt, som aldrig ger sig,
En kanvas för innovation, gränslös och fri,
Där kodare formar framtid, så långt ögat kan se.
Så låt oss koda, i Pythons famn,
En dans av logik, med elegans och namn,
I detta syntax rike, våra drömmar tar fart,
Guidade av Python, som lyser så starkt.
Hur många av de vanligaste orden vill du se? 5
Hur många av de ovanligaste orden vill du se? 4
Totalt antal ord: 222.
Analet unika ord: 151.
De 5 vanligaste orden var (antal förekomster inom parentes):
1. och (14)
2. som (11)
3. så (7)
4. i (6)
5. en (6)
De 4 ovanligaste orden var (antal förekomster inom parentes):
1. starkt (1)
2. guidade (1)
3. tar (1)
4. drömmar (1)
Ord som förekommer lika många gånger (t.ex. i och en) kan du ordna hur du vill.
Frivillig Uppgift#
Gör en variant av programmet som läser en fil (typiskt ett
datorprogram). Programmet ska producera en lista med radnummer över
filen och en referenslista som för varje ord anger på vilka rader det
förekommit. Python-ord som for
, if
, def
… ska inte tas med i
listan liksom inga ord från kommentarerna.
Exempel: Om koden som räknar bokstavsfrekvenser ovan används som indata ska utskriften bli:
1 count = {} # Skapa ett tomt lexikon
2 for character in text: # Iterera över tecknen
3 if character.isalpha(): # Vi intresserar oss bara för bokstäver
4 character = character.lower() # Skiljer inte på små och stora
5 if character in count: # Om denna bokstav redan finns som nyckel
6 count[character] += 1 # Öka dess frekvens
7 else: # annars
8 count[character] = 1 # Lägg in den med frekvensen 1
9 print(count)
Referenslista:
character [2, 3, 4, 4, 5, 6, 8]
count [1, 5, 6, 8, 9]
text [2]
isalpha [3]
lower [4]
print [9]
Tips: Kommentarerna kan tas bort med vanliga sträng-metoder men det
går också att använda reguljära uttryck. Om line
är en rad med
Python-kod så blir eventuell kommentar borttagen med raden
line = re.sub(r'#.*$', '', line)
Strängen r'#.*$'
tolkas så här:
#
matchar kommentartecknetPunkten
.
matchar vilket tecken som helst (utom radbrytning)*
är ett ”metatecken” som anger att det närmast förgående matchningen upprepas 0 eller flera gånger$
är också ett metatecken som anger strängslut.
Uttrycket re.sub(r'#.*$', '', line)
returnerar alltså en sträng där
allt från och med tecknet #
till slutet är utbytt mot en tom sträng.